diff --git a/.env b/.env deleted file mode 100644 index 0e52df3d..00000000 --- a/.env +++ /dev/null @@ -1 +0,0 @@ -# default env file diff --git a/.env.test b/.env.test deleted file mode 100644 index 9e7162f0..00000000 --- a/.env.test +++ /dev/null @@ -1,6 +0,0 @@ -# define your env variables for the test env here -KERNEL_CLASS='App\Kernel' -APP_SECRET='$ecretf0rt3st' -SYMFONY_DEPRECATIONS_HELPER=999999 -PANTHER_APP_ENV=panther -PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots diff --git a/.github/workflows/run-test.yml b/.github/workflows/run-test.yml index 189e3a55..3b17d56e 100644 --- a/.github/workflows/run-test.yml +++ b/.github/workflows/run-test.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-version: ['8.1', '8.2', '8.3'] + php-version: ['8.0', '8.1'] steps: - uses: shivammathur/setup-php@v2 with: diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..316f0e9e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: php +php: + - '8.0' + - '8.1' + - 'nightly' +jobs: + allow_failures: + - php: 'nightly' +install: + - composer install --dev --no-scripts --no-suggest + +script: + - vendor/bin/phpcs -p ./src + - vendor/bin/phpstan analyse -c phpstan.neon diff --git a/LICENSE.md b/LICENSE.md index 8e18fa63..d4d8a009 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright © 2024 Ambroise Maupate +Copyright © 2023 Ambroise Maupate Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/composer.json b/composer.json index 5dc81bc4..f98a3974 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,6 @@ "keywords": [ "cms", "backoffice", - "roadiz", "rezo zero" ], "authors": [ @@ -16,79 +15,79 @@ } ], "type": "symfony-bundle", - "prefer-stable": true, "require": { - "php": ">=8.1", + "php": ">=8.0", "ext-ctype": "*", "ext-iconv": "*", "ext-zip": "*", "ext-json": "*", - "api-platform/core": "~3.2.14", + "api-platform/core": "~2.7.0", "doctrine/annotations": "^1.0", "doctrine/doctrine-bundle": "^2.8.1", "doctrine/doctrine-migrations-bundle": "^3.1", - "doctrine/orm": "~2.19.0", + "doctrine/orm": "<2.17", "gedmo/doctrine-extensions": "^3.10.0", "inlinestyle/inlinestyle": "~1.2.7", "james-heinrich/getid3": "^1.9", "jms/serializer": "^3.9.0", - "jms/serializer-bundle": "^4.2.0", + "jms/serializer-bundle": "^3.10.0", "league/flysystem": "^3.0", "league/flysystem-bundle": "^3.0", - "lexik/jwt-authentication-bundle": "^2.19", + "lexik/jwt-authentication-bundle": "^2.13", "phpdocumentor/reflection-docblock": "^5.2", "phpoffice/phpspreadsheet": "^1.15", "ramsey/uuid": "^4.7", + "rezozero/crypto": "^1.0.0", "rezozero/intervention-request-bundle": "~3.0.0", - "rezozero/liform-bundle": "^0.19", + "rezozero/liform-bundle": "^0.18.1", "rezozero/tree-walker": "^1.3.0", - "roadiz/doc-generator": "2.3.*", - "roadiz/documents": "2.3.*", - "roadiz/dts-generator": "2.3.*", - "roadiz/entity-generator": "2.3.*", - "roadiz/jwt": "2.3.*", - "roadiz/markdown": "2.3.*", - "roadiz/models": "2.3.*", + "roadiz/doc-generator": "2.1.*", + "roadiz/documents": "2.1.*", + "roadiz/dts-generator": "2.1.*", + "roadiz/entity-generator": "2.1.*", + "roadiz/jwt": "2.1.*", + "roadiz/markdown": "2.1.*", + "roadiz/models": "2.1.*", "roadiz/nodetype-contracts": "~1.1.2", - "roadiz/random": "2.3.*", + "roadiz/random": "2.1.*", "rollerworks/password-common-list": "^0.2.0", "rollerworks/password-strength-bundle": "^2.2", "scienta/doctrine-json-functions": "^4.2", "sensio/framework-extra-bundle": "^6.1", "solarium/solarium": "^6.0.4", - "symfony-cmf/routing-bundle": "^3.0.2", - "symfony/asset": "6.4.*", - "symfony/cache": "6.4.*", - "symfony/console": "6.4.*", - "symfony/dotenv": "6.4.*", - "symfony/expression-language": "6.4.*", - "symfony/flex": "^2.2.3", - "symfony/form": "6.4.*", - "symfony/framework-bundle": "6.4.*", - "symfony/http-client": "6.4.*", - "symfony/intl": "6.4.*", - "symfony/lock": "6.4.*", - "symfony/mailer": "6.4.*", - "symfony/messenger": "6.4.*", - "symfony/mime": "6.4.*", + "symfony-cmf/routing": "^2.3.3", + "symfony-cmf/routing-bundle": "^2.5", + "symfony/asset": "5.4.*", + "symfony/cache": "5.4.*", + "symfony/console": "5.4.*", + "symfony/dotenv": "5.4.*", + "symfony/expression-language": "5.4.*", + "symfony/flex": "^v1.19.4 || ^2.2.3", + "symfony/form": "5.4.*", + "symfony/framework-bundle": "5.4.*", + "symfony/http-client": "5.4.*", + "symfony/intl": "5.4.*", + "symfony/mailer": "5.4.*", + "symfony/messenger": "5.4.*", + "symfony/mime": "5.4.*", "symfony/monolog-bundle": "^3.1", - "symfony/notifier": "6.4.*", - "symfony/process": "6.4.*", - "symfony/property-access": "6.4.*", - "symfony/property-info": "6.4.*", - "symfony/proxy-manager-bridge": "6.4.*", - "symfony/rate-limiter": "6.4.*", - "symfony/runtime": "6.4.*", - "symfony/security-core": "6.4.*", - "symfony/serializer": "6.4.*", - "symfony/string": "6.4.*", - "symfony/translation": "6.4.*", - "symfony/twig-bundle": "6.4.*", - "symfony/validator": "6.4.*", - "symfony/web-link": "6.4.*", - "symfony/workflow": "6.4.*", - "symfony/yaml": "6.4.*", - "twig/extra-bundle": "^3.0", + "symfony/notifier": "5.4.*", + "symfony/process": "5.4.*", + "symfony/property-access": "5.4.*", + "symfony/property-info": "5.4.*", + "symfony/proxy-manager-bridge": "5.4.*", + "symfony/rate-limiter": "5.4.*", + "symfony/runtime": "5.4.*", + "symfony/security-core": "5.4.*", + "symfony/serializer": "5.4.*", + "symfony/string": "5.4.*", + "symfony/translation": "5.4.*", + "symfony/twig-bundle": "5.4.*", + "symfony/validator": "5.4.*", + "symfony/web-link": "5.4.*", + "symfony/workflow": "5.4.*", + "symfony/yaml": "5.4.*", + "twig/extra-bundle": "^2.12|^3.0", "twig/intl-extra": "*", "twig/string-extra": "*", "twig/twig": "^3.1" @@ -102,9 +101,9 @@ "phpstan/phpstan-doctrine": "^1.3", "phpunit/phpunit": "^9.5", "squizlabs/php_codesniffer": "^3.5", - "symfony/browser-kit": "6.4.*", - "symfony/phpunit-bridge": "^7.0", - "symfony/stopwatch": "6.4.*" + "symfony/browser-kit": "5.4.*", + "symfony/phpunit-bridge": "5.4.*", + "symfony/stopwatch": "5.4.*" }, "config": { "optimize-autoloader": true, @@ -131,8 +130,8 @@ }, "extra": { "branch-alias": { - "dev-main": "2.3.x-dev", - "dev-develop": "2.4.x-dev" + "dev-main": "2.1.x-dev", + "dev-develop": "2.2.x-dev" } } } diff --git a/config/api_resources/attribute.yml b/config/api_resources/attribute.yml index 07ccee13..11a3c2d6 100644 --- a/config/api_resources/attribute.yml +++ b/config/api_resources/attribute.yml @@ -1,4 +1,5 @@ -#resources: -# RZ\Roadiz\CoreBundle\Entity\Attribute: -# operations: [] -# +--- +RZ\Roadiz\CoreBundle\Entity\Attribute: + collectionOperations: [] + itemOperations: [] + diff --git a/config/api_resources/attribute_value.yml b/config/api_resources/attribute_value.yml index 50fd10fe..aba402ae 100644 --- a/config/api_resources/attribute_value.yml +++ b/config/api_resources/attribute_value.yml @@ -1,14 +1,15 @@ -#resources: -# RZ\Roadiz\CoreBundle\Entity\AttributeValue: -# operations: -# ApiPlatform\Metadata\GetCollection: -# method: "GET" -# normalizationContext: -# groups: ["urls", "attribute", "document_display", "attribute_node", "attribute_documents"] -# enable_max_depth: true -# ApiPlatform\Metadata\Get: -# method: 'GET' -# normalizationContext: -# groups: ["urls", "attribute", "document_display", "attribute_node", "attribute_documents"] -# enable_max_depth: true -# +--- +RZ\Roadiz\CoreBundle\Entity\AttributeValue: + collectionOperations: + get: + method: "GET" + normalization_context: + groups: ["urls", "attribute", "document_display", "attribute_node", "attribute_documents"] + enable_max_depth: true + itemOperations: + get: + method: 'GET' + normalization_context: + groups: ["urls", "attribute", "document_display", "attribute_node", "attribute_documents"] + enable_max_depth: true + diff --git a/config/api_resources/common_content.yml b/config/api_resources/common_content.yml deleted file mode 100644 index a02a765a..00000000 --- a/config/api_resources/common_content.yml +++ /dev/null @@ -1,29 +0,0 @@ -#resources: -# App\Api\Model\CommonContent: -# operations: -# getCommonContent: -# class: ApiPlatform\Metadata\Get -# method: 'GET' -# uriTemplate: '/common_content' -# read: false -# controller: App\Controller\GetCommonContentController -# pagination_enabled: false -# normalizationContext: -# enable_max_depth: true -# pagination_enabled: false -# groups: -# - get -# - common_content -# - web_response -# - walker -# - walker_level -# - children -# - children_count -# - nodes_sources_base -# - nodes_sources_default -# - urls -# - blocks_urls -# - tag_base -# - translation_base -# - document_display -# - document_folders diff --git a/config/api_resources/custom_form.yml b/config/api_resources/custom_form.yml index 0a4f26de..a1db8c54 100644 --- a/config/api_resources/custom_form.yml +++ b/config/api_resources/custom_form.yml @@ -1,110 +1,205 @@ -#resources: -# RZ\Roadiz\CoreBundle\Entity\CustomForm: -# operations: -# ApiPlatform\Metadata\GetCollection: -# method: "GET" -# normalizationContext: -# enable_max_depth: true -# -# ApiPlatform\Metadata\Get: -# method: 'GET' -# normalizationContext: -# enable_max_depth: true -# -# api_custom_forms_item_post: -# method: 'POST' -# class: ApiPlatform\Metadata\Post -# routeName: api_custom_forms_item_post -# normalizationContext: -# enable_max_depth: true -# openapiContext: -# summary: Post a user custom form -# description: | -# Post a user custom form -# requestBody: -# content: -# multipart/form-data: -# schema: -# type: object -# properties: -# custom_form_slug[email]: -# type: string -# example: test@test.test -# custom_form_slug[first_name]: -# type: string -# example: John -# custom_form_slug[last_name]: -# type: string -# example: Doe -# responses: -# 201: ~ -# 400: -# description: Posted custom form has errors -# content: -# application/json: -# schema: -# type: object -# properties: -# email: -# type: object -# example: -# email: This value is not a valid email address. -# 202: -# description: Posted custom form was accepted -# content: -# application/json: -# schema: -# type: object -# properties: { } -# -# api_custom_forms_item_definition: -# method: 'GET' -# class: ApiPlatform\Metadata\Get -# routeName: api_custom_forms_item_definition -# normalizationContext: -# enable_max_depth: true -# openapiContext: -# summary: Get a custom form definition for frontend -# description: | -# Get a custom form definition for frontend -# responses: -# 200: -# description: Custom form definition object -# content: -# application/json: -# schema: -# type: object -# properties: -# title: -# type: string -# description: Form inputs prefix -# example: reiciendis_natus_ducimus_nostrum -# type: -# type: string -# description: Form definition type -# example: object -# properties: -# type: object -# description: Form definition fields -# example: -# email: -# type: string -# title: Email -# attr: -# data-group: null -# placeholder: null -# widget: email -# propertyOrder: 1 -# first_name: -# type: string -# title: Firstname -# attr: -# data-group: null -# placeholder: null -# widget: string -# propertyOrder: 2 -# required: -# type: array -# description: Required fields names -# example: -# - 'email' +--- +RZ\Roadiz\CoreBundle\Entity\CustomForm: + collectionOperations: + get: + method: "GET" + normalization_context: + enable_max_depth: true + + itemOperations: + get: + method: 'GET' + normalization_context: + enable_max_depth: true + + api_custom_forms_item_post: + method: 'POST' + route_name: api_custom_forms_item_post + normalization_context: + enable_max_depth: true + openapi_context: + summary: Post a user custom form + description: | + Post a user custom form + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + custom_form_slug[email]: + type: string + example: test@test.test + custom_form_slug[first_name]: + type: string + example: John + custom_form_slug[last_name]: + type: string + example: Doe + responses: + 201: ~ + 400: + description: Posted custom form has errors + content: + application/json: + schema: + type: object + properties: + email: + type: object + example: + email: This value is not a valid email address. + 202: + description: Posted custom form was accepted + content: + application/json: + schema: + type: object + properties: { } + + api_custom_forms_item_definition: + method: 'GET' + route_name: api_custom_forms_item_definition + normalization_context: + enable_max_depth: true + openapi_context: + summary: Get a custom form definition for frontend + description: | + Get a custom form definition for frontend + responses: + 200: + description: Custom form definition object + content: + application/json: + schema: + type: object + properties: + title: + type: string + description: Form inputs prefix + example: reiciendis_natus_ducimus_nostrum + type: + type: string + description: Form definition type + example: object + properties: + type: object + description: Form definition fields + example: + email: + type: string + title: Email + attr: + data-group: null + placeholder: null + widget: email + propertyOrder: 1 + first_name: + type: string + title: Firstname + attr: + data-group: null + placeholder: null + widget: string + propertyOrder: 2 + required: + type: array + description: Required fields names + example: + - 'email' + + api_contact_form_post: + method: 'POST' + route_name: api_contact_form_post + normalization_context: + enable_max_depth: true + openapi_context: + summary: Post a user contact form + description: | + Post a user contact form + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + email: + type: string + example: test@test.test + first_name: + type: string + example: John + last_name: + type: string + example: Doe + responses: + 201: ~ + 400: + description: Posted contact form has errors + content: + application/json: + schema: + type: object + properties: + email: + type: object + example: + email: This value is not a valid email address. + 202: + description: Posted contact form was accepted + content: + application/json: + schema: + type: object + properties: { } + + api_contact_form_definition: + method: 'GET' + route_name: api_contact_form_definition + normalization_context: + enable_max_depth: true + openapi_context: + summary: Get a contact form definition for frontend + description: | + Get a contact form definition for frontend + responses: + 200: + description: Contact form definition object + content: + application/json: + schema: + type: object + properties: + title: + type: string + description: Form inputs prefix + example: reiciendis_natus_ducimus_nostrum + type: + type: string + description: Form definition type + example: object + properties: + type: object + description: Form definition fields + example: + email: + type: string + title: Email + attr: + data-group: null + placeholder: null + widget: email + propertyOrder: 1 + first_name: + type: string + title: Firstname + attr: + data-group: null + placeholder: null + widget: string + propertyOrder: 2 + required: + type: array + description: Required fields names + example: + - 'email' diff --git a/config/api_resources/document.yml b/config/api_resources/document.yml index db91f92b..9f3f075c 100644 --- a/config/api_resources/document.yml +++ b/config/api_resources/document.yml @@ -1,15 +1,15 @@ -#resources: -# RZ\Roadiz\CoreBundle\Entity\Document: -# operations: -# ApiPlatform\Metadata\GetCollection: -# method: "GET" -# normalizationContext: -# groups: ["urls", "document_display", "document_folders", "document_folders_all", "document_display_sources"] -# enable_max_depth: true -# -# ApiPlatform\Metadata\Get: -# method: 'GET' -# normalizationContext: -# groups: ["urls", "document", "document_display", "document_folders", "document_folders_all", "document_display_sources"] -# enable_max_depth: true -# +--- +RZ\Roadiz\CoreBundle\Entity\Document: + collectionOperations: + get: + method: "GET" + normalization_context: + groups: ["urls", "document_display", "document_display_sources", "position"] + enable_max_depth: true + itemOperations: + get: + method: 'GET' + normalization_context: + groups: ["urls", "document", "document_display", "document_folders", "document_display_sources", "position"] + enable_max_depth: true + diff --git a/config/api_resources/folder.yml b/config/api_resources/folder.yml index 01016890..c29268fd 100644 --- a/config/api_resources/folder.yml +++ b/config/api_resources/folder.yml @@ -1,13 +1,11 @@ -#resources: -# RZ\Roadiz\CoreBundle\Entity\Folder: -# operations: -# ApiPlatform\Metadata\GetCollection: -# method: "GET" -# normalizationContext: -# groups: [ "folder" ] -# enable_max_depth: true -# ApiPlatform\Metadata\Get: -# method: "GET" -# normalizationContext: -# groups: [ "folder" ] -# enable_max_depth: true +--- +RZ\Roadiz\CoreBundle\Entity\Folder: + iri: Folder + shortName: Folder + collectionOperations: {} + itemOperations: + get: + method: "GET" + normalization_context: + groups: [ "folder", "position" ] + enable_max_depth: true diff --git a/config/api_resources/node.yml b/config/api_resources/node.yml index ce20501e..4a3deb0b 100644 --- a/config/api_resources/node.yml +++ b/config/api_resources/node.yml @@ -1,8 +1,10 @@ -#resources: -# RZ\Roadiz\CoreBundle\Entity\Node: -# operations: -# ApiPlatform\Metadata\Get: -# method: 'GET' -# normalizationContext: -# groups: ["node", "document_display"] -# enable_max_depth: true + +RZ\Roadiz\CoreBundle\Entity\Node: + shortName: Node + collectionOperations: {} + itemOperations: + get: + method: 'GET' + normalization_context: + enable_max_depth: true + groups: ["node", "tag_base", "translation_base", "document_display"] diff --git a/config/api_resources/nodes_sources.yml b/config/api_resources/nodes_sources.yml index 9b379c70..d6f10d0f 100644 --- a/config/api_resources/nodes_sources.yml +++ b/config/api_resources/nodes_sources.yml @@ -1,66 +1,55 @@ -#resources: -# RZ\Roadiz\CoreBundle\Entity\NodesSources: -# operations: -# ApiPlatform\Metadata\GetCollection: -# method: "GET" -# normalizationContext: -# groups: -# - nodes_sources_base -# - nodes_sources_default -# - user -# - urls -# - tag_base -# - translation_base -# - document_display -# -# api_nodes_sources_archives: -# class: ApiPlatform\Metadata\GetCollection -# method: 'GET' -# uriTemplate: '/nodes_sources/archives' -# pagination_enabled: false -# pagination_client_enabled: false -# extraProperties: -# archive_enabled: true -# archive_publication_field_name: publishedAt -# normalizationContext: -# groups: -# - get -# - archives -# openapiContext: -# summary: Get available NodesSources archives -# parameters: ~ -# description: | -# Get available NodesSources archives (years and months) based on their `publishedAt` field -# -# api_nodes_sources_search: -# class: ApiPlatform\Metadata\GetCollection -# method: 'GET' -# uriTemplate: '/nodes_sources/search' -# controller: RZ\Roadiz\CoreBundle\Api\Controller\NodesSourcesSearchController -# read: false -# normalizationContext: -# groups: -# - get -# - nodes_sources_base -# - nodes_sources_default -# - urls -# - tag_base -# - translation_base -# - document_display -# openapiContext: -# summary: Search NodesSources resources -# description: | -# Search all website NodesSources resources using **Solr** full-text search engine -# parameters: -# - type: string -# name: search -# in: query -# required: true -# description: Search pattern -# schema: -# type: string -# -# ApiPlatform\Metadata\Get: -# method: 'GET' -# normalizationContext: -# groups: ["nodes_sources", "urls", "tag_base", "translation_base", "document_display"] +--- +RZ\Roadiz\CoreBundle\Entity\NodesSources: + iri: NodesSources + shortName: NodesSources + collectionOperations: + # Get operation is needed for sitemap generation + get: + method: "GET" + normalization_context: + enable_max_depth: true + groups: + - nodes_sources_base + - nodes_sources_default + - urls + - tag_base + - translation_base + - document_display + - position +# search: +# method: 'GET' +# path: '/nodes_sources/search' +# controller: App\Controller\SearchNodesSourcesController +# normalization_context: +# groups: +# - nodes_sources_base +# - nodes_sources_default +# - urls +# - tag_base +# - translation_base +# - document_display +# - position +# openapi_context: +# summary: Search NodesSources resources +# description: | +# Search NodesSources resources using **Solr** full-text search engine +# parameters: +# - type: string +# name: search +# in: query +# required: true +# description: Search pattern +# schema: +# type: string + + itemOperations: + get: + method: 'GET' + normalization_context: + enable_max_depth: true + groups: + - nodes_sources + - urls + - tag_base + - translation_base + - document_display diff --git a/config/api_resources/realm.yml b/config/api_resources/realm.yml index f0bc2faa..45da623a 100644 --- a/config/api_resources/realm.yml +++ b/config/api_resources/realm.yml @@ -1,14 +1,11 @@ -#resources: -# RZ\Roadiz\CoreBundle\Entity\Realm: -# operations: -# ApiPlatform\Metadata\GetCollection: -# method: "GET" -# normalizationContext: -# groups: [ "realm" ] -# enable_max_depth: true -# -# ApiPlatform\Metadata\Get: -# method: "GET" -# normalizationContext: -# groups: [ "realm" ] -# enable_max_depth: true +--- +RZ\Roadiz\CoreBundle\Entity\Realm: + iri: Realm + shortName: Realm + collectionOperations: {} + itemOperations: + get: + method: "GET" + normalization_context: + groups: [ "get", "realm" ] + enable_max_depth: true diff --git a/config/api_resources/tag.yml b/config/api_resources/tag.yml index e9b64d16..dc516bb0 100644 --- a/config/api_resources/tag.yml +++ b/config/api_resources/tag.yml @@ -1,18 +1,20 @@ -#resources: -# RZ\Roadiz\CoreBundle\Entity\Tag: -# operations: -# ApiPlatform\Metadata\GetCollection: -# method: "GET" -# normalizationContext: -# enable_max_depth: true -# groups: -# - tag_base -# -# ApiPlatform\Metadata\Get: -# method: 'GET' -# normalizationContext: -# enable_max_depth: true -# groups: -# - tag -# - tag_base -# - tag_parent + +RZ\Roadiz\CoreBundle\Entity\Tag: + iri: Tag + shortName: Tag + collectionOperations: + get: + method: "GET" + normalization_context: + enable_max_depth: true + groups: + - tag + - tag_base + itemOperations: + get: + method: 'GET' + normalization_context: + enable_max_depth: true + groups: + - tag + - tag_base diff --git a/config/api_resources/translation.yml b/config/api_resources/translation.yml index c0d56c1a..304ffa9e 100644 --- a/config/api_resources/translation.yml +++ b/config/api_resources/translation.yml @@ -1,17 +1,9 @@ -#resources: -# RZ\Roadiz\CoreBundle\Entity\Translation: -# operations: -# ApiPlatform\Metadata\GetCollection: -# method: "GET" -# normalizationContext: -# enable_max_depth: true -# groups: -# - translation_base -# -# ApiPlatform\Metadata\Get: -# method: 'GET' -# normalizationContext: -# enable_max_depth: true -# groups: -# - translation_base -# + +RZ\Roadiz\CoreBundle\Entity\Translation: + collectionOperations: + get: + method: "GET" + itemOperations: + get: + method: 'GET' + diff --git a/config/api_resources/web_response.yml b/config/api_resources/web_response.yml index 1c2a1d41..2353a4c1 100644 --- a/config/api_resources/web_response.yml +++ b/config/api_resources/web_response.yml @@ -1,3 +1,39 @@ -#resources: -# RZ\Roadiz\CoreBundle\Api\Model\WebResponse: -# operations: [] +RZ\Roadiz\CoreBundle\Api\Model\WebResponse: + collectionOperations: {} + itemOperations: + getByPath: + method: 'GET' + path: '/web_response_by_path' + read: false + controller: RZ\Roadiz\CoreBundle\Api\Controller\GetWebResponseByPathController + pagination_enabled: false + normalization_context: + enable_max_depth: true + pagination_enabled: false + groups: + - get + - web_response + - position + - walker + - walker_level + - meta + - children + - children_count + - nodes_sources + - urls + - tag_base + - translation_base + - document_display + - node_attributes + openapi_context: + summary: Get a resource by its path wrapped in a WebResponse object + description: | + Get a resource by its path wrapped in a WebResponse + parameters: + - type: string + name: path + in: query + required: true + description: Resource path, or `/` for home page + schema: + type: string diff --git a/config/fixtures/settings.json b/config/fixtures/settings.json index ee93f247..137890ad 100644 --- a/config/fixtures/settings.json +++ b/config/fixtures/settings.json @@ -2,6 +2,7 @@ { "name": "force_locale", "visible": true, + "encrypted": false, "description": "Force displaying translation locale in every node’ paths. This should be *ON* if you redirect users based on their language on homepage.", "setting_group": { "name": "Development", @@ -14,6 +15,7 @@ "name": "force_locale_with_urlaliases", "description": "force_locale_with_urlaliases.help", "visible": true, + "encrypted": false, "setting_group": { "name": "Development", "in_menu": true @@ -23,6 +25,7 @@ { "name": "leaflet_map_tile_url", "value": "https:\/\/{s}.tile.osm.org\/{z}\/{x}\/{y}.png", + "encrypted": false, "description": "Default maps tiles layout when using *Leaflet*.", "visible": true, "setting_group": { @@ -35,6 +38,7 @@ { "name": "maps_default_location", "value": "{\"lat\":45.769785, \"lng\":4.833967, \"zoom\":14}", + "encrypted": false, "description": "Default maps marker location.", "visible": true, "setting_group": { @@ -48,6 +52,7 @@ "name": "openid_button_label", "description": "openid_button_label.help", "visible": true, + "encrypted": false, "setting_group": { "name": "OpenId", "in_menu": true @@ -57,6 +62,7 @@ { "name": "support_email_address", "visible": true, + "encrypted": false, "description": "Support email address, used in every system emails footer", "setting_group": { "name": "Emailings", @@ -68,6 +74,7 @@ { "name": "email_sender", "visible": true, + "encrypted": false, "description": "Default sender email, used as origin for every system email sent. This email **must be allowed by your SMTP server.**", "setting_group": { "name": "Emailings", @@ -79,6 +86,7 @@ { "name": "email_sender_name", "visible": true, + "encrypted": false, "setting_group": { "name": "Emailings", "in_menu": true @@ -89,6 +97,7 @@ { "name": "universal_analytics_id", "visible": true, + "encrypted": false, "setting_group": { "name": "APIs", "in_menu": true @@ -99,6 +108,18 @@ { "name": "google_tag_manager_id", "visible": true, + "encrypted": false, + "setting_group": { + "name": "APIs", + "in_menu": true + }, + "type": 0, + "default_values": "" + }, + { + "name": "instagram_access_token", + "visible": true, + "encrypted": true, "setting_group": { "name": "APIs", "in_menu": true @@ -109,6 +130,7 @@ { "name": "seo_description", "visible": true, + "encrypted": false, "setting_group": { "name": "Site information", "in_menu": true @@ -119,6 +141,7 @@ { "name": "site_name", "visible": true, + "encrypted": false, "setting_group": { "name": "Site information", "in_menu": true @@ -129,6 +152,7 @@ { "name": "maintenance_mode", "visible": true, + "encrypted": false, "description": "Switch maintenance mode. Only login page will be available for public requests.", "setting_group": { "name": "Site information", @@ -140,6 +164,7 @@ { "name": "site_copyright", "visible": true, + "encrypted": false, "setting_group": { "name": "Site information", "in_menu": true @@ -149,6 +174,7 @@ }, { "name": "main_color", + "encrypted": false, "visible": true, "setting_group": { "name": "Site information", @@ -160,6 +186,7 @@ { "name": "admin_image", "visible": true, + "encrypted": false, "setting_group": { "name": "Site information", "in_menu": true @@ -170,6 +197,7 @@ { "name": "login_image", "visible": true, + "encrypted": false, "description": "Replace random *Splashbase* login images with your own.", "setting_group": { "name": "Site information", @@ -181,6 +209,7 @@ { "name": "facebook_url", "visible": true, + "encrypted": false, "setting_group": { "name": "Social networks", "in_menu": true @@ -191,6 +220,7 @@ { "name": "instagram_url", "visible": true, + "encrypted": false, "setting_group": { "name": "Social networks", "in_menu": true @@ -201,6 +231,7 @@ { "name": "pinterest_url", "visible": true, + "encrypted": false, "setting_group": { "name": "Social networks", "in_menu": true @@ -211,6 +242,7 @@ { "name": "twitter_url", "visible": true, + "encrypted": false, "setting_group": { "name": "Social networks", "in_menu": true @@ -221,6 +253,7 @@ { "name": "linkedin_url", "visible": true, + "encrypted": false, "setting_group": { "name": "Social networks", "in_menu": true @@ -231,6 +264,7 @@ { "name": "youtube_url", "visible": true, + "encrypted": false, "setting_group": { "name": "Social networks", "in_menu": true @@ -242,6 +276,7 @@ "name": "custom_preview_scheme", "description": "custom_preview_scheme.help", "visible": true, + "encrypted": false, "setting_group": { "name": "Site information", "in_menu": true @@ -252,6 +287,7 @@ "name": "custom_public_scheme", "description": "custom_public_scheme.help", "visible": true, + "encrypted": false, "setting_group": { "name": "Site information", "in_menu": true @@ -262,6 +298,7 @@ "name": "dashboard_iframe", "description": "dashboard_iframe.help", "visible": true, + "encrypted": false, "setting_group": { "name": "Site information", "in_menu": true diff --git a/config/packages/api_platform.yaml b/config/packages/api_platform.yaml index f9551bd7..0fc9e246 100644 --- a/config/packages/api_platform.yaml +++ b/config/packages/api_platform.yaml @@ -9,8 +9,8 @@ api_platform: show_webby: false swagger: versions: [3] - title: "%env(string:APP_TITLE)%" - description: "%env(string:APP_DESCRIPTION)%" + title: "My Roadiz website API" + description: "My Roadiz website API" version: '%env(string:APP_VERSION)%' mapping: paths: @@ -19,27 +19,32 @@ api_platform: - '%kernel.project_dir%/vendor/roadiz/core-bundle/src/Entity' - '%kernel.project_dir%/vendor/rezozero/tree-walker/src' - '%kernel.project_dir%/config/api_resources' - - collection: - pagination: - page_parameter_name: page - items_per_page_parameter_name: itemsPerPage - - http_cache: - invalidation: - enabled: true - varnish_urls: [ '%env(VARNISH_URL)%' ] - +# http_cache: +# # Automatically generate etags for API responses. +# etag: true +# public: true +# # Default value for the response max age. +# max_age: '%env(int:HTTP_CACHE_MAX_AGE)%' +# # Default value for the response shared (proxy) max age. +# shared_max_age: '%env(int:HTTP_CACHE_SHARED_MAX_AGE)%' +# # Default values of the "Vary" HTTP header. +# vary: ['Accept', 'Authorization', 'Origin', 'Accept-Encoding', 'Content-Type'] +# invalidation: +# enabled: true +# varnish_urls: ['%env(VARNISH_URL)%'] defaults: - enable_max_depth: true - normalization_context: - skip_null_values: true pagination_client_items_per_page: true pagination_items_per_page: 15 pagination_maximum_items_per_page: 50 - cache_headers: - etag: true - public: true - max_age: '%env(int:HTTP_CACHE_MAX_AGE)%' - shared_max_age: '%env(int:HTTP_CACHE_SHARED_MAX_AGE)%' - vary: [ 'Accept', 'Authorization', 'Origin', 'Accept-Encoding', 'Content-Type' ] +# cache_headers: +# etag: true +# public: true +# max_age: '%env(int:HTTP_CACHE_MAX_AGE)%' +# shared_max_age: '%env(int:HTTP_CACHE_SHARED_MAX_AGE)%' +# vary: ['Accept', 'Authorization', 'Origin', 'Accept-Encoding', 'Content-Type'] + collection: + pagination: + items_per_page: 15 # Default value + maximum_items_per_page: 50 + client_items_per_page: true + items_per_page_parameter_name: itemsPerPage diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index 8165a234..092fadc1 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -5,67 +5,43 @@ doctrine: # either here or in the DATABASE_URL env var (see .env file) #server_version: '13' orm: + dql: + string_functions: + JSON_CONTAINS: Scienta\DoctrineJsonFunctions\Query\AST\Functions\Mysql\JsonContains auto_generate_proxy_classes: true - default_entity_manager: default - entity_managers: - # Put `logger` entity manager first to select it as default for Log entity - logger: - naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware - mappings: - ## Just sharding EM to avoid having Logs in default EM - ## and flushing bad entities when storing log entries. - RoadizCoreLogger: - is_bundle: false - type: attribute - dir: '%kernel.project_dir%/vendor/roadiz/core-bundle/src/Logger/Entity' - prefix: 'RZ\Roadiz\CoreBundle\Logger\Entity' - alias: RoadizCoreLogger - default: - dql: - string_functions: - JSON_CONTAINS: Scienta\DoctrineJsonFunctions\Query\AST\Functions\Mysql\JsonContains - naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware - auto_mapping: true - mappings: - ## Keep RoadizCoreLogger to avoid creating different migrations since we are using - ## the same database for both entity managers. Just sharding EM to avoid - ## having Logs in default EM and flushing bad entities when storing log entries. - RoadizCoreLogger: - is_bundle: false - type: attribute - dir: '%kernel.project_dir%/vendor/roadiz/core-bundle/src/Logger/Entity' - prefix: 'RZ\Roadiz\CoreBundle\Logger\Entity' - alias: RoadizCoreLogger - App: - is_bundle: false - type: attribute - dir: '%kernel.project_dir%/src/Entity' - prefix: 'App\Entity' - alias: App - RoadizCoreBundle: - is_bundle: true - type: attribute - dir: 'src/Entity' - prefix: 'RZ\Roadiz\CoreBundle\Entity' - alias: RoadizCoreBundle - RZ\Roadiz\Core: - is_bundle: false - type: annotation - dir: '%kernel.project_dir%/vendor/roadiz/models/src/Core/AbstractEntities' - prefix: 'RZ\Roadiz\Core\AbstractEntities' - alias: AbstractEntities - App\GeneratedEntity: - is_bundle: false - type: attribute - dir: '%kernel.project_dir%/src/GeneratedEntity' - prefix: 'App\GeneratedEntity' - alias: App\GeneratedEntity - gedmo_loggable: - type: attribute - prefix: Gedmo\Loggable\Entity\MappedSuperclass - dir: "%kernel.project_dir%/vendor/gedmo/doctrine-extensions/src/Loggable/Entity/MappedSuperclass" - alias: GedmoLoggableMappedSuperclass - is_bundle: false + naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware + auto_mapping: true + mappings: + App: + is_bundle: false + type: attribute + dir: '%kernel.project_dir%/src/Entity' + prefix: 'App\Entity' + alias: App + RoadizCoreBundle: + is_bundle: true + type: attribute + dir: 'src/Entity' + prefix: 'RZ\Roadiz\CoreBundle\Entity' + alias: RoadizCoreBundle + RZ\Roadiz\Core: + is_bundle: false + type: annotation + dir: '%kernel.project_dir%/vendor/roadiz/models/src/Roadiz/Core/AbstractEntities' + prefix: 'RZ\Roadiz\Core\AbstractEntities' + alias: AbstractEntities + App\GeneratedEntity: + is_bundle: false + type: attribute + dir: '%kernel.project_dir%/src/GeneratedEntity' + prefix: 'App\GeneratedEntity' + alias: App\GeneratedEntity + gedmo_loggable: + type: attribute + prefix: Gedmo\Loggable\Entity\MappedSuperclass + dir: "%kernel.project_dir%/vendor/gedmo/doctrine-extensions/src/Loggable/Entity/MappedSuperclass" + alias: GedmoLoggableMappedSuperclass + is_bundle: false resolve_target_entities: Symfony\Component\Security\Core\User\UserInterface: RZ\Roadiz\CoreBundle\Entity\User diff --git a/config/packages/roadiz_core.yaml b/config/packages/roadiz_core.yaml index 1f5bf32d..818584c3 100644 --- a/config/packages/roadiz_core.yaml +++ b/config/packages/roadiz_core.yaml @@ -11,6 +11,9 @@ roadiz_core: # Be careful if you are using a reverse-proxy cache, YOU MUST vary on Accept-Language header and normalize it. # @see https://varnish-cache.org/docs/6.3/users-guide/increasing-your-hitrate.html#http-vary useAcceptLanguageHeader: '%env(bool:APP_USE_ACCEPT_LANGUAGE_HEADER)%' + security: + private_key_name: default + themes: [] medias: unsplash_client_id: '%env(string:APP_UNSPLASH_CLIENT_ID)%' soundcloud_client_id: '%env(string:APP_SOUNDCLOUD_CLIENT_ID)%' diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 8324dc43..4c2ce00a 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -8,8 +8,6 @@ security: # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers providers: - jwt: - lexik_jwt: ~ roadiz_user_provider: entity: class: RZ\Roadiz\CoreBundle\Entity\User @@ -22,12 +20,12 @@ security: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false - - # https://symfony.com/bundles/LexikJWTAuthenticationBundle/current/index.html#configure-application-routing - api_login: + # JWT for API + api: + pattern: ^/api stateless: true - pattern: ^/api/token provider: all_users + user_checker: RZ\Roadiz\CoreBundle\Security\UserChecker login_throttling: max_attempts: 3 json_login: @@ -36,21 +34,7 @@ security: password_path: password success_handler: lexik_jwt_authentication.handler.authentication_success failure_handler: lexik_jwt_authentication.handler.authentication_failure - user_checker: RZ\Roadiz\CoreBundle\Security\UserChecker - - # https://symfony.com/bundles/LexikJWTAuthenticationBundle/current/8-jwt-user-provider.html#symfony-5-3-and-higher - api: - pattern: ^/api - stateless: true - # Do not reload user from database, trust JWT roles in order to restrict PreviewUsers - # Only drawback is when you want to disable / block / expire a user, you'll have to - # wait for JWT token to expire. - provider: jwt - # If you really want to reload user from database, uncomment this line, but Preview JWT - # will be reloaded as full user and not as PreviewUser. - #provider: all_users jwt: ~ - # disables session creation for assets and healthcheck controllers assets: pattern: ^/assets @@ -79,16 +63,18 @@ security: max_attempts: 3 logout: path: logoutPage + guard: + authenticators: + - lexik_jwt_authentication.jwt_token_authenticator custom_authenticator: - RZ\Roadiz\RozierBundle\Security\RozierAuthenticator # 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: ^/rz-admin/login, roles: PUBLIC_ACCESS } - - { path: ^/rz-admin/logout, roles: PUBLIC_ACCESS } + - { path: ^/rz-admin/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/rz-admin, roles: ROLE_BACKEND_USER } - - { path: ^/api/token, roles: PUBLIC_ACCESS } - - { path: "^/api/custom_forms/(?:[0-9]+)/post", methods: [ POST ], roles: PUBLIC_ACCESS } + - { path: ^/api/token, roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: "^/api/custom_forms/(?:[0-9]+)/post", methods: [ POST ], roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/api, roles: ROLE_BACKEND_USER, methods: [ POST, PUT, PATCH, DELETE ] } # - { path: ^/profile, roles: ROLE_USER } diff --git a/config/routing.yaml b/config/routing.yaml index e3a597bc..361fab01 100644 --- a/config/routing.yaml +++ b/config/routing.yaml @@ -5,14 +5,12 @@ api_custom_forms_item_definition: methods: [GET] path: /api/custom_forms/{id}/definition controller: RZ\Roadiz\CoreBundle\Controller\CustomFormController::definitionAction - stateless: true requirements: id: "[0-9]+" api_custom_forms_item_post: methods: [POST] path: /api/custom_forms/{id}/post controller: RZ\Roadiz\CoreBundle\Controller\CustomFormController::postAction - stateless: true requirements: id: "[0-9]+" @@ -30,10 +28,12 @@ customFormSentAction: healthCheckAction: methods: [GET] path: /health-check - stateless: true controller: RZ\Roadiz\CoreBundle\Controller\HealthCheckController +roadiz_core_themes: + resource: . + type: themes + api_login_check: methods: [POST] - stateless: true path: /api/token diff --git a/config/services.yaml b/config/services.yaml index aea5792a..64727a6d 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -1,6 +1,6 @@ --- parameters: - roadiz_core.cms_version: '2.3.13' + roadiz_core.cms_version: '2.1.66' roadiz_core.cms_version_prefix: 'main' env(APP_NAMESPACE): "roadiz" env(APP_VERSION): "0.1.0" @@ -27,21 +27,21 @@ services: $cmsVersion: '%roadiz_core.cms_version%' $appVersion: '%roadiz_core.app_version%' $cmsVersionPrefix: '%roadiz_core.cms_version_prefix%' + $staticDomain: '%roadiz_core.static_domain_name%' $hideRoadizVersion: '%roadiz_core.hide_roadiz_version%' $inheritanceType: '%roadiz_core.inheritance_type%' $maxPixelSize: '%rz_intervention_request.max_pixel_size%' $appNamespace: '%roadiz_core.app_namespace%' $projectDir: '%kernel.project_dir%' $exportDir: '%kernel.project_dir%/var/export' + $privateKeyName: '%roadiz_core.private_key_name%' $generatedEntitiesDir: '%roadiz_core.generated_entities_dir%' $serializedNodeTypesDir: '%roadiz_core.serialized_node_types_dir%' $importFilesConfigPath: '%roadiz_core.import_files_config_path%' $kernelProjectDir: '%kernel.project_dir%' $apiResourcesDir: '%kernel.project_dir%/config/api_resources' $debug: '%kernel.debug%' - $kernelEnvironment: '%kernel.environment%' $defaultControllerClass: '%roadiz_core.default_node_source_controller%' - $defaultLocale: '%kernel.default_locale%' $webhookMessageTypes: '%roadiz_core.webhook.message_types%' $useAcceptLanguageHeader: '%roadiz_core.use_accept_language_header%' $healthCheckToken: '%roadiz_core.health_check_token%' @@ -50,8 +50,6 @@ services: $maxVersionsShowed: '%roadiz_core.max_versions_showed%' $recaptchaPublicKey: '%roadiz_core.medias.recaptcha_public_key%' $recaptchaPrivateKey: '%roadiz_core.medias.recaptcha_private_key%' - $webResponseClass: '%roadiz_core.web_response_class%' - $useGravatar: '%roadiz_core.use_gravatar%' RZ\Roadiz\CoreBundle\: resource: '../src/' @@ -115,21 +113,6 @@ services: # Extension must be called after all filtering BUT before default pagination extension tags: [ { name: 'api_platform.doctrine.orm.query_extension.collection', priority: -40 } ] - # - # These API doctrine extension must be called last before pagination - # to perform on existing JOIN with node entities (found after filtering) - # - RZ\Roadiz\CoreBundle\Api\Extension\AttributeValueQueryExtension: - tags: [ - { name: 'api_platform.doctrine.orm.query_extension.collection', priority: -40 }, - { name: 'api_platform.doctrine.orm.query_extension.item', priority: -40 }, - ] - RZ\Roadiz\CoreBundle\Api\Extension\NodesTagsQueryExtension: - tags: [ - { name: 'api_platform.doctrine.orm.query_extension.collection', priority: -40 }, - { name: 'api_platform.doctrine.orm.query_extension.item', priority: -40 }, - ] - RZ\Roadiz\CoreBundle\Bag\: resource: '../src/Bag/' autowire: true @@ -387,16 +370,11 @@ services: RZ\Roadiz\CoreBundle\Preview\RequestPreviewRevolver: arguments: - '@Symfony\Component\HttpFoundation\RequestStack' - - '%roadiz_core.preview_required_role_name%' + - 'ROLE_BACKEND_USER' RZ\Roadiz\CoreBundle\SearchEngine\ClientRegistry: arguments: ['@service_container'] - RZ\Roadiz\CoreBundle\SearchEngine\SolariumLogger: - tags: - - { name: data_collector, template: '@RoadizCore/DataCollector/solarium.html.twig', id: 'solarium' } - - { name: monolog.logger, channel: solr } - RZ\Roadiz\CoreBundle\SearchEngine\Indexer\IndexerFactory: arguments: ['@service_container'] @@ -437,6 +415,12 @@ services: RZ\Roadiz\Random\PasswordGenerator: ~ + RZ\Crypto\KeyChain\KeyChainInterface: + alias: RZ\Crypto\KeyChain\AsymmetricFilesystemKeyChain + + RZ\Crypto\KeyChain\AsymmetricFilesystemKeyChain: + arguments: ['%kernel.project_dir%/var/secret', true] + JMS\Serializer\Construction\ObjectConstructorInterface: alias: RZ\Roadiz\CoreBundle\Serializer\ObjectConstructor\ObjectConstructor @@ -447,10 +431,10 @@ services: - '@RZ\Roadiz\CoreBundle\Serializer\ObjectConstructor\ObjectConstructor' - [ '@RZ\Roadiz\CoreBundle\Serializer\ObjectConstructor\TranslationObjectConstructor', - '@RZ\Roadiz\CoreBundle\Serializer\ObjectConstructor\AttributeObjectConstructor', '@RZ\Roadiz\CoreBundle\Serializer\ObjectConstructor\TagObjectConstructor', '@RZ\Roadiz\CoreBundle\Serializer\ObjectConstructor\NodeObjectConstructor', '@RZ\Roadiz\CoreBundle\Serializer\ObjectConstructor\NodeTypeObjectConstructor', + '@RZ\Roadiz\CoreBundle\Serializer\ObjectConstructor\NodeTypeFieldObjectConstructor', '@RZ\Roadiz\CoreBundle\Serializer\ObjectConstructor\RoleObjectConstructor', '@RZ\Roadiz\CoreBundle\Serializer\ObjectConstructor\GroupObjectConstructor', '@RZ\Roadiz\CoreBundle\Serializer\ObjectConstructor\SettingObjectConstructor', @@ -460,6 +444,7 @@ services: Solarium\Core\Client\Client: factory: ['RZ\Roadiz\CoreBundle\SearchEngine\ClientRegistry', 'getClient'] + # Provides LoginAttemptManager aware authentication RZ\Roadiz\CoreBundle\Security\Authentication\JwtAuthenticationSuccessHandler: decorates: 'lexik_jwt_authentication.handler.authentication_success' @@ -528,9 +513,9 @@ services: RZ\Roadiz\Documents\Renderer\PdfRenderer: tags: [ 'roadiz_core.document_renderer' ] RZ\Roadiz\Documents\Renderer\SvgRenderer: - tags: [ { name: 'roadiz_core.document_renderer', priority: 10 } ] + tags: [ 'roadiz_core.document_renderer' ] RZ\Roadiz\Documents\Renderer\InlineSvgRenderer: - tags: [ { name: 'roadiz_core.document_renderer', priority: 11 } ] + tags: [ 'roadiz_core.document_renderer' ] RZ\Roadiz\Documents\Renderer\EmbedRenderer: tags: [ { name: 'roadiz_core.document_renderer', priority: -128 } ] RZ\Roadiz\Documents\Renderer\ThumbnailRenderer: @@ -546,6 +531,7 @@ services: alias: RZ\Roadiz\CoreBundle\Filesystem\RoadizFileDirectories public: true + RZ\Roadiz\Documents\Packages: ~ RZ\Roadiz\Documents\DownscaleImageManager: ~ RZ\Roadiz\Documents\DocumentArchiver: ~ # diff --git a/migrations/Version20201203004857.php b/migrations/Version20201203004857.php index 48ccc417..746639af 100644 --- a/migrations/Version20201203004857.php +++ b/migrations/Version20201203004857.php @@ -7,7 +7,7 @@ use Doctrine\Migrations\AbstractMigration; /** - * Database initialization migration. + * Database initialization migration for MySQL/MariaDB. * * @package RZ\Roadiz\Migrations */ @@ -15,22 +15,17 @@ final class Version20201203004857 extends AbstractMigration { public function getDescription() : string { - return 'Database initialization migration.'; + return 'Database initialization migration for MySQL/MariaDB.'; } public function up(Schema $schema) : void { + $this->skipIf( + $this->connection->getDatabasePlatform()->getName() !== 'mysql', + 'Migration can only be executed safely on \'mysql\'.' + ); $this->skipIf($schema->hasTable('nodes'), 'Database has been initialized before Doctrine Migration tool.'); - if ($this->connection->getDatabasePlatform()->getName() === 'mysql') { - $this->mysqlUp(); - } elseif ($this->connection->getDatabasePlatform()->getName() === 'postgresql') { - $this->postgresUp(); - } - } - - private function mysqlUp(): void - { $this->addSql('CREATE TABLE attribute_group_translations (id INT AUTO_INCREMENT NOT NULL, attribute_group_id INT DEFAULT NULL, translation_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, INDEX IDX_5C704A6862D643B7 (attribute_group_id), INDEX IDX_5C704A689CAA2B25 (translation_id), INDEX IDX_5C704A685E237E06 (name), UNIQUE INDEX UNIQ_5C704A6862D643B79CAA2B25 (attribute_group_id, translation_id), UNIQUE INDEX UNIQ_5C704A685E237E069CAA2B25 (name, translation_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); $this->addSql('CREATE TABLE attribute_groups (id INT AUTO_INCREMENT NOT NULL, canonical_name VARCHAR(255) NOT NULL, UNIQUE INDEX UNIQ_D28C172A674D812 (canonical_name), INDEX IDX_D28C172A674D812 (canonical_name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); $this->addSql('CREATE TABLE attribute_translations (id INT AUTO_INCREMENT NOT NULL, attribute_id INT DEFAULT NULL, translation_id INT DEFAULT NULL, label VARCHAR(255) NOT NULL, options LONGTEXT DEFAULT NULL COMMENT \'(DC2Type:simple_array)\', INDEX IDX_4059D4A0B6E62EFA (attribute_id), INDEX IDX_4059D4A09CAA2B25 (translation_id), INDEX IDX_4059D4A0EA750E8 (label), UNIQUE INDEX UNIQ_4059D4A0B6E62EFA9CAA2B25 (attribute_id, translation_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); @@ -138,348 +133,6 @@ private function mysqlUp(): void $this->addSql('ALTER TABLE users_groups ADD CONSTRAINT FK_FF8AB7E0FE54D947 FOREIGN KEY (group_id) REFERENCES `groups` (id)'); } - private function postgresUp(): void - { - $this->addSql('CREATE SEQUENCE attribute_group_translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE attribute_groups_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE attribute_translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE attribute_value_translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE attribute_values_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE attributes_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE attributes_documents_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE custom_form_answers_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE custom_form_field_attributes_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE custom_form_fields_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE custom_forms_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE documents_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE documents_translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE folders_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE folders_translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE log_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE login_attempts_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE node_type_fields_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE node_types_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE nodes_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE nodes_custom_forms_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE nodes_sources_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE nodes_sources_documents_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE nodes_to_nodes_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE redirections_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE roles_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE settings_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE settings_groups_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE tags_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE tags_translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE tags_translations_documents_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE url_aliases_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE user_log_entries_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE usergroups_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE users_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE TABLE attribute_group_translations (id INT NOT NULL, attribute_group_id INT DEFAULT NULL, translation_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_5C704A6862D643B7 ON attribute_group_translations (attribute_group_id)'); - $this->addSql('CREATE INDEX IDX_5C704A689CAA2B25 ON attribute_group_translations (translation_id)'); - $this->addSql('CREATE INDEX IDX_5C704A685E237E06 ON attribute_group_translations (name)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_5C704A6862D643B79CAA2B25 ON attribute_group_translations (attribute_group_id, translation_id)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_5C704A685E237E069CAA2B25 ON attribute_group_translations (name, translation_id)'); - $this->addSql('CREATE TABLE attribute_groups (id INT NOT NULL, canonical_name VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_D28C172A674D812 ON attribute_groups (canonical_name)'); - $this->addSql('CREATE INDEX IDX_D28C172A674D812 ON attribute_groups (canonical_name)'); - $this->addSql('CREATE TABLE attribute_translations (id INT NOT NULL, attribute_id INT DEFAULT NULL, translation_id INT DEFAULT NULL, label VARCHAR(255) NOT NULL, options TEXT DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_4059D4A0B6E62EFA ON attribute_translations (attribute_id)'); - $this->addSql('CREATE INDEX IDX_4059D4A09CAA2B25 ON attribute_translations (translation_id)'); - $this->addSql('CREATE INDEX IDX_4059D4A0EA750E8 ON attribute_translations (label)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_4059D4A0B6E62EFA9CAA2B25 ON attribute_translations (attribute_id, translation_id)'); - $this->addSql('COMMENT ON COLUMN attribute_translations.options IS \'(DC2Type:simple_array)\''); - $this->addSql('CREATE TABLE attribute_value_translations (id INT NOT NULL, translation_id INT DEFAULT NULL, attribute_value INT DEFAULT NULL, value VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_1293849B9CAA2B25 ON attribute_value_translations (translation_id)'); - $this->addSql('CREATE INDEX IDX_1293849BFE4FBB82 ON attribute_value_translations (attribute_value)'); - $this->addSql('CREATE INDEX IDX_1293849B1D775834 ON attribute_value_translations (value)'); - $this->addSql('CREATE INDEX IDX_1293849B9CAA2B25FE4FBB82 ON attribute_value_translations (translation_id, attribute_value)'); - $this->addSql('CREATE TABLE attribute_values (id INT NOT NULL, attribute_id INT DEFAULT NULL, node_id INT DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_184662BCB6E62EFA ON attribute_values (attribute_id)'); - $this->addSql('CREATE INDEX IDX_184662BC460D9FD7 ON attribute_values (node_id)'); - $this->addSql('CREATE INDEX IDX_184662BCB6E62EFA460D9FD7 ON attribute_values (attribute_id, node_id)'); - $this->addSql('CREATE TABLE attributes (id INT NOT NULL, group_id INT DEFAULT NULL, code VARCHAR(255) NOT NULL, searchable BOOLEAN DEFAULT \'false\' NOT NULL, type INT NOT NULL, color VARCHAR(7) DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_319B9E7077153098 ON attributes (code)'); - $this->addSql('CREATE INDEX IDX_319B9E7077153098 ON attributes (code)'); - $this->addSql('CREATE INDEX IDX_319B9E708CDE5729 ON attributes (type)'); - $this->addSql('CREATE INDEX IDX_319B9E7094CD8C0D ON attributes (searchable)'); - $this->addSql('CREATE INDEX IDX_319B9E70FE54D947 ON attributes (group_id)'); - $this->addSql('CREATE TABLE attributes_documents (id INT NOT NULL, attribute_id INT DEFAULT NULL, document_id INT DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_67CCC9E0B6E62EFA ON attributes_documents (attribute_id)'); - $this->addSql('CREATE INDEX IDX_67CCC9E0C33F7837 ON attributes_documents (document_id)'); - $this->addSql('CREATE INDEX IDX_67CCC9E0462CE4F5 ON attributes_documents (position)'); - $this->addSql('CREATE INDEX IDX_67CCC9E0B6E62EFA462CE4F5 ON attributes_documents (attribute_id, position)'); - $this->addSql('CREATE TABLE custom_form_answers (id INT NOT NULL, custom_form_id INT DEFAULT NULL, ip VARCHAR(255) NOT NULL, submitted_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_1A3BB12658AFF2B0 ON custom_form_answers (custom_form_id)'); - $this->addSql('CREATE INDEX IDX_1A3BB126A5E3B32D ON custom_form_answers (ip)'); - $this->addSql('CREATE INDEX IDX_1A3BB1263182C73C ON custom_form_answers (submitted_at)'); - $this->addSql('CREATE TABLE custom_form_field_attributes (id INT NOT NULL, custom_form_answer_id INT DEFAULT NULL, custom_form_field_id INT DEFAULT NULL, value TEXT DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_B7133605F1D6C2D1 ON custom_form_field_attributes (custom_form_answer_id)'); - $this->addSql('CREATE INDEX IDX_B71336057F13CC0F ON custom_form_field_attributes (custom_form_field_id)'); - $this->addSql('CREATE TABLE custom_form_answers_documents (customformfieldattribute_id INT NOT NULL, document_id INT NOT NULL, PRIMARY KEY(customformfieldattribute_id, document_id))'); - $this->addSql('CREATE INDEX IDX_E979F877C84CA2FC ON custom_form_answers_documents (customformfieldattribute_id)'); - $this->addSql('CREATE INDEX IDX_E979F877C33F7837 ON custom_form_answers_documents (document_id)'); - $this->addSql('CREATE TABLE custom_form_fields (id INT NOT NULL, custom_form_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, label VARCHAR(255) NOT NULL, placeholder VARCHAR(255) DEFAULT NULL, description TEXT DEFAULT NULL, default_values TEXT DEFAULT NULL, type INT NOT NULL, expanded BOOLEAN DEFAULT \'false\' NOT NULL, field_required BOOLEAN DEFAULT \'false\' NOT NULL, group_name VARCHAR(255) DEFAULT NULL, group_name_canonical VARCHAR(255) DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_4A3782EC58AFF2B0 ON custom_form_fields (custom_form_id)'); - $this->addSql('CREATE INDEX IDX_4A3782EC462CE4F5 ON custom_form_fields (position)'); - $this->addSql('CREATE INDEX IDX_4A3782EC77792576 ON custom_form_fields (group_name)'); - $this->addSql('CREATE INDEX IDX_4A3782EC8CDE5729 ON custom_form_fields (type)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_4A3782EC5E237E0658AFF2B0 ON custom_form_fields (name, custom_form_id)'); - $this->addSql('CREATE TABLE custom_forms (id INT NOT NULL, color VARCHAR(255) DEFAULT NULL, name VARCHAR(255) NOT NULL, display_name VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, email TEXT DEFAULT NULL, open BOOLEAN DEFAULT \'true\' NOT NULL, close_date TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_3E32E39E5E237E06 ON custom_forms (name)'); - $this->addSql('CREATE INDEX IDX_3E32E39E8B8E8428 ON custom_forms (created_at)'); - $this->addSql('CREATE INDEX IDX_3E32E39E43625D9F ON custom_forms (updated_at)'); - $this->addSql('CREATE TABLE documents (id INT NOT NULL, raw_document INT DEFAULT NULL, original INT DEFAULT NULL, raw BOOLEAN DEFAULT \'false\' NOT NULL, embedId VARCHAR(255) DEFAULT NULL, embedPlatform VARCHAR(255) DEFAULT NULL, filename VARCHAR(255) DEFAULT NULL, mime_type VARCHAR(255) DEFAULT NULL, folder VARCHAR(255) NOT NULL, private BOOLEAN DEFAULT \'false\' NOT NULL, imageWidth INT DEFAULT 0 NOT NULL, imageHeight INT DEFAULT 0 NOT NULL, average_color VARCHAR(7) DEFAULT NULL, filesize INT DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_A2B0728826CBD5A5 ON documents (raw_document)'); - $this->addSql('CREATE INDEX IDX_A2B072882F727085 ON documents (original)'); - $this->addSql('CREATE INDEX IDX_A2B072881AB3DB55 ON documents (raw)'); - $this->addSql('CREATE INDEX IDX_A2B07288D206C1D1 ON documents (private)'); - $this->addSql('CREATE INDEX IDX_A2B072881AB3DB55D206C1D1 ON documents (raw, private)'); - $this->addSql('CREATE INDEX IDX_A2B072882100AA2E ON documents (mime_type)'); - $this->addSql('CREATE TABLE documents_translations (id INT NOT NULL, translation_id INT DEFAULT NULL, document_id INT DEFAULT NULL, name VARCHAR(255) DEFAULT NULL, description TEXT DEFAULT NULL, copyright TEXT DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_5CD2F5509CAA2B25 ON documents_translations (translation_id)'); - $this->addSql('CREATE INDEX IDX_5CD2F550C33F7837 ON documents_translations (document_id)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_5CD2F550C33F78379CAA2B25 ON documents_translations (document_id, translation_id)'); - $this->addSql('CREATE TABLE folders (id INT NOT NULL, parent_id INT DEFAULT NULL, folder_name VARCHAR(255) NOT NULL, visible BOOLEAN DEFAULT \'true\' NOT NULL, position DOUBLE PRECISION NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_FE37D30F47BC5813 ON folders (folder_name)'); - $this->addSql('CREATE INDEX IDX_FE37D30F727ACA70 ON folders (parent_id)'); - $this->addSql('CREATE INDEX IDX_FE37D30F7AB0E859 ON folders (visible)'); - $this->addSql('CREATE INDEX IDX_FE37D30F462CE4F5 ON folders (position)'); - $this->addSql('CREATE INDEX IDX_FE37D30F8B8E8428 ON folders (created_at)'); - $this->addSql('CREATE INDEX IDX_FE37D30F43625D9F ON folders (updated_at)'); - $this->addSql('CREATE TABLE documents_folders (folder_id INT NOT NULL, document_id INT NOT NULL, PRIMARY KEY(folder_id, document_id))'); - $this->addSql('CREATE INDEX IDX_617BB29C162CB942 ON documents_folders (folder_id)'); - $this->addSql('CREATE INDEX IDX_617BB29CC33F7837 ON documents_folders (document_id)'); - $this->addSql('CREATE TABLE folders_translations (id INT NOT NULL, folder_id INT DEFAULT NULL, translation_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_9F6A68B2162CB942 ON folders_translations (folder_id)'); - $this->addSql('CREATE INDEX IDX_9F6A68B29CAA2B25 ON folders_translations (translation_id)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_9F6A68B2162CB9429CAA2B25 ON folders_translations (folder_id, translation_id)'); - $this->addSql('CREATE TABLE log (id INT NOT NULL, user_id INT DEFAULT NULL, node_source_id INT DEFAULT NULL, username VARCHAR(255) DEFAULT NULL, message TEXT NOT NULL, level INT NOT NULL, datetime TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, client_ip VARCHAR(255) DEFAULT NULL, channel VARCHAR(255) DEFAULT NULL, additional_data JSON DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_8F3F68C5A76ED395 ON log (user_id)'); - $this->addSql('CREATE INDEX IDX_8F3F68C58E831402 ON log (node_source_id)'); - $this->addSql('CREATE INDEX IDX_8F3F68C593F3C6CA ON log (datetime)'); - $this->addSql('CREATE INDEX IDX_8F3F68C59AEACC13 ON log (level)'); - $this->addSql('CREATE INDEX IDX_8F3F68C5F85E0677 ON log (username)'); - $this->addSql('CREATE INDEX IDX_8F3F68C5A2F98E47 ON log (channel)'); - $this->addSql('CREATE TABLE login_attempts (id INT NOT NULL, ip_address VARCHAR(50) DEFAULT NULL, date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, blocks_login_until TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, username VARCHAR(255) NOT NULL, attempt_count INT DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_9163C7FBF85E0677 ON login_attempts (username)'); - $this->addSql('CREATE INDEX IDX_9163C7FBEFF8A4EEF85E0677 ON login_attempts (blocks_login_until, username)'); - $this->addSql('CREATE INDEX IDX_9163C7FBEFF8A4EEF85E067722FFD58C ON login_attempts (blocks_login_until, username, ip_address)'); - $this->addSql('COMMENT ON COLUMN login_attempts.date IS \'(DC2Type:datetime_immutable)\''); - $this->addSql('CREATE TABLE node_type_fields (id INT NOT NULL, node_type_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, label VARCHAR(255) NOT NULL, placeholder VARCHAR(255) DEFAULT NULL, description TEXT DEFAULT NULL, default_values TEXT DEFAULT NULL, type INT NOT NULL, expanded BOOLEAN DEFAULT \'false\' NOT NULL, universal BOOLEAN DEFAULT \'false\' NOT NULL, exclude_from_search BOOLEAN DEFAULT \'false\' NOT NULL, min_length INT DEFAULT NULL, max_length INT DEFAULT NULL, indexed BOOLEAN DEFAULT \'false\' NOT NULL, visible BOOLEAN DEFAULT \'true\' NOT NULL, group_name VARCHAR(255) DEFAULT NULL, group_name_canonical VARCHAR(255) DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_1D3923596344C9E1 ON node_type_fields (node_type_id)'); - $this->addSql('CREATE INDEX IDX_1D3923597AB0E859 ON node_type_fields (visible)'); - $this->addSql('CREATE INDEX IDX_1D392359D9416D95 ON node_type_fields (indexed)'); - $this->addSql('CREATE INDEX IDX_1D392359462CE4F5 ON node_type_fields (position)'); - $this->addSql('CREATE INDEX IDX_1D39235977792576 ON node_type_fields (group_name)'); - $this->addSql('CREATE INDEX IDX_1D3923594BAF07A4 ON node_type_fields (group_name_canonical)'); - $this->addSql('CREATE INDEX IDX_1D3923598CDE5729 ON node_type_fields (type)'); - $this->addSql('CREATE INDEX IDX_1D392359A4B8F6E1 ON node_type_fields (universal)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_1D3923595E237E066344C9E1 ON node_type_fields (name, node_type_id)'); - $this->addSql('CREATE TABLE node_types (id INT NOT NULL, name VARCHAR(255) NOT NULL, display_name VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, visible BOOLEAN DEFAULT \'true\' NOT NULL, publishable BOOLEAN DEFAULT \'false\' NOT NULL, reachable BOOLEAN DEFAULT \'true\' NOT NULL, hiding_nodes BOOLEAN DEFAULT \'false\' NOT NULL, hiding_non_reachable_nodes BOOLEAN DEFAULT \'false\' NOT NULL, color VARCHAR(255) DEFAULT NULL, default_ttl INT DEFAULT 0 NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_409B1BCC5E237E06 ON node_types (name)'); - $this->addSql('CREATE INDEX IDX_409B1BCC7AB0E859 ON node_types (visible)'); - $this->addSql('CREATE INDEX IDX_409B1BCC7697C594 ON node_types (publishable)'); - $this->addSql('CREATE INDEX IDX_409B1BCCFB696FF0 ON node_types (hiding_nodes)'); - $this->addSql('CREATE INDEX IDX_409B1BCC5A3C14C7 ON node_types (hiding_non_reachable_nodes)'); - $this->addSql('CREATE INDEX IDX_409B1BCC96ED695F ON node_types (reachable)'); - $this->addSql('CREATE TABLE nodes (id INT NOT NULL, parent_node_id INT DEFAULT NULL, node_name VARCHAR(255) NOT NULL, dynamic_node_name BOOLEAN DEFAULT \'true\' NOT NULL, home BOOLEAN DEFAULT \'false\' NOT NULL, visible BOOLEAN DEFAULT \'true\' NOT NULL, status INT NOT NULL, ttl INT DEFAULT 0 NOT NULL, locked BOOLEAN DEFAULT \'false\' NOT NULL, priority NUMERIC(2, 1) NOT NULL, hide_children BOOLEAN DEFAULT \'false\' NOT NULL, sterile BOOLEAN DEFAULT \'false\' NOT NULL, children_order VARCHAR(255) NOT NULL, children_order_direction VARCHAR(4) NOT NULL, position DOUBLE PRECISION NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, nodeType_id INT DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_1D3D05FC9987F390 ON nodes (node_name)'); - $this->addSql('CREATE INDEX IDX_1D3D05FC47D04729 ON nodes (nodeType_id)'); - $this->addSql('CREATE INDEX IDX_1D3D05FC3445EB91 ON nodes (parent_node_id)'); - $this->addSql('CREATE INDEX IDX_1D3D05FC7AB0E859 ON nodes (visible)'); - $this->addSql('CREATE INDEX IDX_1D3D05FC7B00651C ON nodes (status)'); - $this->addSql('CREATE INDEX IDX_1D3D05FCEAD2C891 ON nodes (locked)'); - $this->addSql('CREATE INDEX IDX_1D3D05FCF32D8BE6 ON nodes (sterile)'); - $this->addSql('CREATE INDEX IDX_1D3D05FC462CE4F5 ON nodes (position)'); - $this->addSql('CREATE INDEX IDX_1D3D05FC8B8E8428 ON nodes (created_at)'); - $this->addSql('CREATE INDEX IDX_1D3D05FC43625D9F ON nodes (updated_at)'); - $this->addSql('CREATE INDEX IDX_1D3D05FC50E2D3D2 ON nodes (hide_children)'); - $this->addSql('CREATE INDEX IDX_1D3D05FC9987F3907B00651C ON nodes (node_name, status)'); - $this->addSql('CREATE INDEX IDX_1D3D05FC7AB0E8597B00651C ON nodes (visible, status)'); - $this->addSql('CREATE INDEX IDX_1D3D05FC7AB0E8597B00651C3445EB91 ON nodes (visible, status, parent_node_id)'); - $this->addSql('CREATE INDEX IDX_1D3D05FC7AB0E8593445EB91 ON nodes (visible, parent_node_id)'); - $this->addSql('CREATE INDEX IDX_1D3D05FC71D60CD0 ON nodes (home)'); - $this->addSql('CREATE TABLE nodes_tags (node_id INT NOT NULL, tag_id INT NOT NULL, PRIMARY KEY(node_id, tag_id))'); - $this->addSql('CREATE INDEX IDX_5B5CB38C460D9FD7 ON nodes_tags (node_id)'); - $this->addSql('CREATE INDEX IDX_5B5CB38CBAD26311 ON nodes_tags (tag_id)'); - $this->addSql('CREATE TABLE stack_types (node_id INT NOT NULL, nodetype_id INT NOT NULL, PRIMARY KEY(node_id, nodetype_id))'); - $this->addSql('CREATE INDEX IDX_DE24E53460D9FD7 ON stack_types (node_id)'); - $this->addSql('CREATE INDEX IDX_DE24E53886D7EB5 ON stack_types (nodetype_id)'); - $this->addSql('CREATE TABLE nodes_custom_forms (id INT NOT NULL, node_id INT DEFAULT NULL, custom_form_id INT DEFAULT NULL, node_type_field_id INT DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_4D401A0C460D9FD7 ON nodes_custom_forms (node_id)'); - $this->addSql('CREATE INDEX IDX_4D401A0C58AFF2B0 ON nodes_custom_forms (custom_form_id)'); - $this->addSql('CREATE INDEX IDX_4D401A0C47705282 ON nodes_custom_forms (node_type_field_id)'); - $this->addSql('CREATE INDEX IDX_4D401A0C462CE4F5 ON nodes_custom_forms (position)'); - $this->addSql('CREATE TABLE nodes_sources (id INT NOT NULL, node_id INT DEFAULT NULL, translation_id INT DEFAULT NULL, title VARCHAR(255) DEFAULT NULL, published_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, meta_title VARCHAR(255) NOT NULL, meta_keywords TEXT NOT NULL, meta_description TEXT NOT NULL, discr VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_7C7DED6D460D9FD7 ON nodes_sources (node_id)'); - $this->addSql('CREATE INDEX IDX_7C7DED6D9CAA2B25 ON nodes_sources (translation_id)'); - $this->addSql('CREATE INDEX IDX_7C7DED6D4AD26064 ON nodes_sources (discr)'); - $this->addSql('CREATE INDEX IDX_7C7DED6D4AD260649CAA2B25 ON nodes_sources (discr, translation_id)'); - $this->addSql('CREATE INDEX IDX_7C7DED6DE0D4FDE14AD260649CAA2B25 ON nodes_sources (published_at, discr, translation_id)'); - $this->addSql('CREATE INDEX IDX_7C7DED6D2B36786B ON nodes_sources (title)'); - $this->addSql('CREATE INDEX IDX_7C7DED6DE0D4FDE1 ON nodes_sources (published_at)'); - $this->addSql('CREATE INDEX IDX_7C7DED6DE0D4FDE19CAA2B25 ON nodes_sources (published_at, translation_id)'); - $this->addSql('CREATE INDEX IDX_7C7DED6D460D9FD79CAA2B25E0D4FDE1 ON nodes_sources (node_id, translation_id, published_at)'); - $this->addSql('CREATE INDEX IDX_7C7DED6D2B36786BE0D4FDE1 ON nodes_sources (title, published_at)'); - $this->addSql('CREATE INDEX IDX_7C7DED6D2B36786BE0D4FDE19CAA2B25 ON nodes_sources (title, published_at, translation_id)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_7C7DED6D460D9FD79CAA2B25 ON nodes_sources (node_id, translation_id)'); - $this->addSql('CREATE TABLE nodes_sources_documents (id INT NOT NULL, ns_id INT DEFAULT NULL, document_id INT DEFAULT NULL, node_type_field_id INT DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_1CD104F7AA2D61 ON nodes_sources_documents (ns_id)'); - $this->addSql('CREATE INDEX IDX_1CD104F7C33F7837 ON nodes_sources_documents (document_id)'); - $this->addSql('CREATE INDEX IDX_1CD104F747705282 ON nodes_sources_documents (node_type_field_id)'); - $this->addSql('CREATE INDEX IDX_1CD104F7462CE4F5 ON nodes_sources_documents (position)'); - $this->addSql('CREATE INDEX IDX_1CD104F7AA2D6147705282 ON nodes_sources_documents (ns_id, node_type_field_id)'); - $this->addSql('CREATE INDEX IDX_1CD104F7AA2D6147705282462CE4F5 ON nodes_sources_documents (ns_id, node_type_field_id, position)'); - $this->addSql('CREATE TABLE nodes_to_nodes (id INT NOT NULL, node_a_id INT DEFAULT NULL, node_b_id INT DEFAULT NULL, node_type_field_id INT DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_761F9A91FC7ADECE ON nodes_to_nodes (node_a_id)'); - $this->addSql('CREATE INDEX IDX_761F9A91EECF7120 ON nodes_to_nodes (node_b_id)'); - $this->addSql('CREATE INDEX IDX_761F9A9147705282 ON nodes_to_nodes (node_type_field_id)'); - $this->addSql('CREATE INDEX IDX_761F9A91462CE4F5 ON nodes_to_nodes (position)'); - $this->addSql('CREATE INDEX IDX_761F9A91FC7ADECE47705282 ON nodes_to_nodes (node_a_id, node_type_field_id)'); - $this->addSql('CREATE INDEX IDX_761F9A91FC7ADECE47705282462CE4F5 ON nodes_to_nodes (node_a_id, node_type_field_id, position)'); - $this->addSql('CREATE INDEX IDX_761F9A91EECF712047705282 ON nodes_to_nodes (node_b_id, node_type_field_id)'); - $this->addSql('CREATE INDEX IDX_761F9A91EECF712047705282462CE4F5 ON nodes_to_nodes (node_b_id, node_type_field_id, position)'); - $this->addSql('CREATE TABLE redirections (id INT NOT NULL, ns_id INT DEFAULT NULL, query VARCHAR(255) NOT NULL, redirectUri VARCHAR(255) DEFAULT NULL, type INT NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_38F5ECE424BDB5EB ON redirections (query)'); - $this->addSql('CREATE INDEX IDX_38F5ECE4AA2D61 ON redirections (ns_id)'); - $this->addSql('CREATE INDEX IDX_38F5ECE48B8E8428 ON redirections (created_at)'); - $this->addSql('CREATE INDEX IDX_38F5ECE443625D9F ON redirections (updated_at)'); - $this->addSql('CREATE TABLE roles (id INT NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_B63E2EC75E237E06 ON roles (name)'); - $this->addSql('CREATE TABLE settings (id INT NOT NULL, setting_group_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, value TEXT DEFAULT NULL, visible BOOLEAN DEFAULT \'true\' NOT NULL, encrypted BOOLEAN DEFAULT \'false\' NOT NULL, type INT NOT NULL, defaultValues TEXT DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_E545A0C55E237E06 ON settings (name)'); - $this->addSql('CREATE INDEX IDX_E545A0C550DDE1BD ON settings (setting_group_id)'); - $this->addSql('CREATE INDEX IDX_E545A0C58CDE5729 ON settings (type)'); - $this->addSql('CREATE INDEX IDX_E545A0C55E237E06 ON settings (name)'); - $this->addSql('CREATE INDEX IDX_E545A0C57AB0E859 ON settings (visible)'); - $this->addSql('CREATE TABLE settings_groups (id INT NOT NULL, name VARCHAR(255) NOT NULL, in_menu BOOLEAN DEFAULT \'false\' NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_FFD519025E237E06 ON settings_groups (name)'); - $this->addSql('CREATE TABLE tags (id INT NOT NULL, parent_tag_id INT DEFAULT NULL, color VARCHAR(7) DEFAULT \'#000000\' NOT NULL, tag_name VARCHAR(255) NOT NULL, visible BOOLEAN DEFAULT \'true\' NOT NULL, children_order VARCHAR(255) DEFAULT \'position\' NOT NULL, children_order_direction VARCHAR(4) DEFAULT \'ASC\' NOT NULL, locked BOOLEAN DEFAULT \'false\' NOT NULL, position DOUBLE PRECISION NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_6FBC9426B02CC1B0 ON tags (tag_name)'); - $this->addSql('CREATE INDEX IDX_6FBC9426F5C1A0D7 ON tags (parent_tag_id)'); - $this->addSql('CREATE INDEX IDX_6FBC94267AB0E859 ON tags (visible)'); - $this->addSql('CREATE INDEX IDX_6FBC9426EAD2C891 ON tags (locked)'); - $this->addSql('CREATE INDEX IDX_6FBC9426462CE4F5 ON tags (position)'); - $this->addSql('CREATE INDEX IDX_6FBC94268B8E8428 ON tags (created_at)'); - $this->addSql('CREATE INDEX IDX_6FBC942643625D9F ON tags (updated_at)'); - $this->addSql('CREATE INDEX IDX_6FBC9426F5C1A0D77AB0E859 ON tags (parent_tag_id, visible)'); - $this->addSql('CREATE TABLE tags_translations (id INT NOT NULL, tag_id INT DEFAULT NULL, translation_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_95D326DCBAD26311 ON tags_translations (tag_id)'); - $this->addSql('CREATE INDEX IDX_95D326DC9CAA2B25 ON tags_translations (translation_id)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_95D326DCBAD263119CAA2B25 ON tags_translations (tag_id, translation_id)'); - $this->addSql('CREATE TABLE tags_translations_documents (id INT NOT NULL, tag_translation_id INT DEFAULT NULL, document_id INT DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_6E886F1F22010F1 ON tags_translations_documents (tag_translation_id)'); - $this->addSql('CREATE INDEX IDX_6E886F1FC33F7837 ON tags_translations_documents (document_id)'); - $this->addSql('CREATE INDEX IDX_6E886F1F462CE4F5 ON tags_translations_documents (position)'); - $this->addSql('CREATE INDEX IDX_6E886F1F22010F1462CE4F5 ON tags_translations_documents (tag_translation_id, position)'); - $this->addSql('CREATE TABLE translations (id INT NOT NULL, locale VARCHAR(10) NOT NULL, override_locale VARCHAR(10) DEFAULT NULL, name VARCHAR(255) NOT NULL, default_translation BOOLEAN DEFAULT \'false\' NOT NULL, available BOOLEAN DEFAULT \'true\' NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_C6B7DA874180C698 ON translations (locale)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_C6B7DA873F824FD6 ON translations (override_locale)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_C6B7DA875E237E06 ON translations (name)'); - $this->addSql('CREATE INDEX IDX_C6B7DA87A58FA485 ON translations (available)'); - $this->addSql('CREATE INDEX IDX_C6B7DA87609A56D9 ON translations (default_translation)'); - $this->addSql('CREATE INDEX IDX_C6B7DA878B8E8428 ON translations (created_at)'); - $this->addSql('CREATE INDEX IDX_C6B7DA8743625D9F ON translations (updated_at)'); - $this->addSql('CREATE INDEX IDX_C6B7DA87A58FA485609A56D9 ON translations (available, default_translation)'); - $this->addSql('CREATE INDEX IDX_C6B7DA87A58FA4854180C698 ON translations (available, locale)'); - $this->addSql('CREATE INDEX IDX_C6B7DA87A58FA4853F824FD6 ON translations (available, override_locale)'); - $this->addSql('CREATE TABLE url_aliases (id INT NOT NULL, ns_id INT DEFAULT NULL, alias VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_E261ED65E16C6B94 ON url_aliases (alias)'); - $this->addSql('CREATE INDEX IDX_E261ED65AA2D61 ON url_aliases (ns_id)'); - $this->addSql('CREATE TABLE user_log_entries (id INT NOT NULL, user_id INT DEFAULT NULL, action VARCHAR(8) NOT NULL, logged_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, object_id VARCHAR(64) DEFAULT NULL, object_class VARCHAR(191) NOT NULL, version INT NOT NULL, data TEXT DEFAULT NULL, username VARCHAR(191) DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_BC2E42C7A76ED395 ON user_log_entries (user_id)'); - $this->addSql('CREATE INDEX log_class_lookup_idx ON user_log_entries (object_class)'); - $this->addSql('CREATE INDEX log_date_lookup_idx ON user_log_entries (logged_at)'); - $this->addSql('CREATE INDEX log_user_lookup_idx ON user_log_entries (username)'); - $this->addSql('CREATE INDEX log_version_lookup_idx ON user_log_entries (object_id, object_class, version)'); - $this->addSql('COMMENT ON COLUMN user_log_entries.data IS \'(DC2Type:array)\''); - $this->addSql('CREATE TABLE usergroups (id INT NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_98972EB45E237E06 ON usergroups (name)'); - $this->addSql('CREATE TABLE groups_roles (group_id INT NOT NULL, role_id INT NOT NULL, PRIMARY KEY(group_id, role_id))'); - $this->addSql('CREATE INDEX IDX_E79D4963FE54D947 ON groups_roles (group_id)'); - $this->addSql('CREATE INDEX IDX_E79D4963D60322AC ON groups_roles (role_id)'); - $this->addSql('CREATE TABLE users (id INT NOT NULL, chroot_id INT DEFAULT NULL, facebook_name VARCHAR(255) DEFAULT NULL, picture_url TEXT DEFAULT NULL, enabled BOOLEAN DEFAULT \'true\' NOT NULL, confirmation_token VARCHAR(255) DEFAULT NULL, password_requested_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, username VARCHAR(255) NOT NULL, salt VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL, last_login TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, expired BOOLEAN DEFAULT \'false\' NOT NULL, locked BOOLEAN DEFAULT \'false\' NOT NULL, credentials_expires_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, credentials_expired BOOLEAN DEFAULT \'false\' NOT NULL, expires_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, locale VARCHAR(7) DEFAULT NULL, email VARCHAR(255) NOT NULL, firstName VARCHAR(255) DEFAULT NULL, lastName VARCHAR(255) DEFAULT NULL, phone VARCHAR(255) DEFAULT NULL, company VARCHAR(255) DEFAULT NULL, job VARCHAR(255) DEFAULT NULL, birthday TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E9C05FB297 ON users (confirmation_token)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E9F85E0677 ON users (username)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E9E7927C74 ON users (email)'); - $this->addSql('CREATE INDEX IDX_1483A5E96483A539 ON users (chroot_id)'); - $this->addSql('CREATE INDEX IDX_1483A5E950F9BB84 ON users (enabled)'); - $this->addSql('CREATE INDEX IDX_1483A5E9194FED4B ON users (expired)'); - $this->addSql('CREATE INDEX IDX_1483A5E9F9D83E2 ON users (expires_at)'); - $this->addSql('CREATE INDEX IDX_1483A5E94180C698 ON users (locale)'); - $this->addSql('CREATE TABLE users_roles (user_id INT NOT NULL, role_id INT NOT NULL, PRIMARY KEY(user_id, role_id))'); - $this->addSql('CREATE INDEX IDX_51498A8EA76ED395 ON users_roles (user_id)'); - $this->addSql('CREATE INDEX IDX_51498A8ED60322AC ON users_roles (role_id)'); - $this->addSql('CREATE TABLE users_groups (user_id INT NOT NULL, group_id INT NOT NULL, PRIMARY KEY(user_id, group_id))'); - $this->addSql('CREATE INDEX IDX_FF8AB7E0A76ED395 ON users_groups (user_id)'); - $this->addSql('CREATE INDEX IDX_FF8AB7E0FE54D947 ON users_groups (group_id)'); - $this->addSql('ALTER TABLE attribute_group_translations ADD CONSTRAINT FK_5C704A6862D643B7 FOREIGN KEY (attribute_group_id) REFERENCES attribute_groups (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE attribute_group_translations ADD CONSTRAINT FK_5C704A689CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE attribute_translations ADD CONSTRAINT FK_4059D4A0B6E62EFA FOREIGN KEY (attribute_id) REFERENCES attributes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE attribute_translations ADD CONSTRAINT FK_4059D4A09CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE attribute_value_translations ADD CONSTRAINT FK_1293849B9CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE attribute_value_translations ADD CONSTRAINT FK_1293849BFE4FBB82 FOREIGN KEY (attribute_value) REFERENCES attribute_values (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE attribute_values ADD CONSTRAINT FK_184662BCB6E62EFA FOREIGN KEY (attribute_id) REFERENCES attributes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE attribute_values ADD CONSTRAINT FK_184662BC460D9FD7 FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE attributes ADD CONSTRAINT FK_319B9E70FE54D947 FOREIGN KEY (group_id) REFERENCES attribute_groups (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE attributes_documents ADD CONSTRAINT FK_67CCC9E0B6E62EFA FOREIGN KEY (attribute_id) REFERENCES attributes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE attributes_documents ADD CONSTRAINT FK_67CCC9E0C33F7837 FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE custom_form_answers ADD CONSTRAINT FK_1A3BB12658AFF2B0 FOREIGN KEY (custom_form_id) REFERENCES custom_forms (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE custom_form_field_attributes ADD CONSTRAINT FK_B7133605F1D6C2D1 FOREIGN KEY (custom_form_answer_id) REFERENCES custom_form_answers (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE custom_form_field_attributes ADD CONSTRAINT FK_B71336057F13CC0F FOREIGN KEY (custom_form_field_id) REFERENCES custom_form_fields (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE custom_form_answers_documents ADD CONSTRAINT FK_E979F877C84CA2FC FOREIGN KEY (customformfieldattribute_id) REFERENCES custom_form_field_attributes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE custom_form_answers_documents ADD CONSTRAINT FK_E979F877C33F7837 FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE custom_form_fields ADD CONSTRAINT FK_4A3782EC58AFF2B0 FOREIGN KEY (custom_form_id) REFERENCES custom_forms (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE documents ADD CONSTRAINT FK_A2B0728826CBD5A5 FOREIGN KEY (raw_document) REFERENCES documents (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE documents ADD CONSTRAINT FK_A2B072882F727085 FOREIGN KEY (original) REFERENCES documents (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE documents_translations ADD CONSTRAINT FK_5CD2F5509CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE documents_translations ADD CONSTRAINT FK_5CD2F550C33F7837 FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE folders ADD CONSTRAINT FK_FE37D30F727ACA70 FOREIGN KEY (parent_id) REFERENCES folders (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE documents_folders ADD CONSTRAINT FK_617BB29C162CB942 FOREIGN KEY (folder_id) REFERENCES folders (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE documents_folders ADD CONSTRAINT FK_617BB29CC33F7837 FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE folders_translations ADD CONSTRAINT FK_9F6A68B2162CB942 FOREIGN KEY (folder_id) REFERENCES folders (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE folders_translations ADD CONSTRAINT FK_9F6A68B29CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE log ADD CONSTRAINT FK_8F3F68C5A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE log ADD CONSTRAINT FK_8F3F68C58E831402 FOREIGN KEY (node_source_id) REFERENCES nodes_sources (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE node_type_fields ADD CONSTRAINT FK_1D3923596344C9E1 FOREIGN KEY (node_type_id) REFERENCES node_types (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes ADD CONSTRAINT FK_1D3D05FC47D04729 FOREIGN KEY (nodeType_id) REFERENCES node_types (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes ADD CONSTRAINT FK_1D3D05FC3445EB91 FOREIGN KEY (parent_node_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes_tags ADD CONSTRAINT FK_5B5CB38C460D9FD7 FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes_tags ADD CONSTRAINT FK_5B5CB38CBAD26311 FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE stack_types ADD CONSTRAINT FK_DE24E53460D9FD7 FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE stack_types ADD CONSTRAINT FK_DE24E53886D7EB5 FOREIGN KEY (nodetype_id) REFERENCES node_types (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes_custom_forms ADD CONSTRAINT FK_4D401A0C460D9FD7 FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes_custom_forms ADD CONSTRAINT FK_4D401A0C58AFF2B0 FOREIGN KEY (custom_form_id) REFERENCES custom_forms (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes_custom_forms ADD CONSTRAINT FK_4D401A0C47705282 FOREIGN KEY (node_type_field_id) REFERENCES node_type_fields (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes_sources ADD CONSTRAINT FK_7C7DED6D460D9FD7 FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes_sources ADD CONSTRAINT FK_7C7DED6D9CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes_sources_documents ADD CONSTRAINT FK_1CD104F7AA2D61 FOREIGN KEY (ns_id) REFERENCES nodes_sources (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes_sources_documents ADD CONSTRAINT FK_1CD104F7C33F7837 FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes_sources_documents ADD CONSTRAINT FK_1CD104F747705282 FOREIGN KEY (node_type_field_id) REFERENCES node_type_fields (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes_to_nodes ADD CONSTRAINT FK_761F9A91FC7ADECE FOREIGN KEY (node_a_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes_to_nodes ADD CONSTRAINT FK_761F9A91EECF7120 FOREIGN KEY (node_b_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes_to_nodes ADD CONSTRAINT FK_761F9A9147705282 FOREIGN KEY (node_type_field_id) REFERENCES node_type_fields (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE redirections ADD CONSTRAINT FK_38F5ECE4AA2D61 FOREIGN KEY (ns_id) REFERENCES nodes_sources (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE settings ADD CONSTRAINT FK_E545A0C550DDE1BD FOREIGN KEY (setting_group_id) REFERENCES settings_groups (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE tags ADD CONSTRAINT FK_6FBC9426F5C1A0D7 FOREIGN KEY (parent_tag_id) REFERENCES tags (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE tags_translations ADD CONSTRAINT FK_95D326DCBAD26311 FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE tags_translations ADD CONSTRAINT FK_95D326DC9CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE tags_translations_documents ADD CONSTRAINT FK_6E886F1F22010F1 FOREIGN KEY (tag_translation_id) REFERENCES tags_translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE tags_translations_documents ADD CONSTRAINT FK_6E886F1FC33F7837 FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE url_aliases ADD CONSTRAINT FK_E261ED65AA2D61 FOREIGN KEY (ns_id) REFERENCES nodes_sources (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE user_log_entries ADD CONSTRAINT FK_BC2E42C7A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE groups_roles ADD CONSTRAINT FK_E79D4963FE54D947 FOREIGN KEY (group_id) REFERENCES usergroups (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE groups_roles ADD CONSTRAINT FK_E79D4963D60322AC FOREIGN KEY (role_id) REFERENCES roles (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE users ADD CONSTRAINT FK_1483A5E96483A539 FOREIGN KEY (chroot_id) REFERENCES nodes (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE users_roles ADD CONSTRAINT FK_51498A8EA76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE users_roles ADD CONSTRAINT FK_51498A8ED60322AC FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE users_groups ADD CONSTRAINT FK_FF8AB7E0A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE users_groups ADD CONSTRAINT FK_FF8AB7E0FE54D947 FOREIGN KEY (group_id) REFERENCES usergroups (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); - } - public function down(Schema $schema) : void { $this->throwIrreversibleMigrationException(); diff --git a/migrations/Version20201225181256.php b/migrations/Version20201225181256.php index 3e099267..5c22be4b 100644 --- a/migrations/Version20201225181256.php +++ b/migrations/Version20201225181256.php @@ -9,19 +9,361 @@ /** * Database initialization migration for PostgreSQL. * - * @deprecated Use Roadiz\Core\Migrations\Version20201203004857 instead. * @package RZ\Roadiz\Migrations */ final class Version20201225181256 extends AbstractMigration { public function getDescription() : string { - return '[deprecated] Database initialization migration for PostgreSQL.'; + return 'Database initialization migration for PostgreSQL.'; } public function up(Schema $schema) : void { - $this->write('Nothing to do with RZ\Roadiz\Migrations\Version20201225181256.'); + $this->skipIf( + $this->connection->getDatabasePlatform()->getName() !== 'postgresql', + 'Migration can only be executed safely on \'postgresql\'.' + ); + $this->skipIf($schema->hasTable('nodes'), 'Database has been initialized before Doctrine Migration tool.'); + + $this->addSql('CREATE SEQUENCE attribute_group_translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE attribute_groups_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE attribute_translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE attribute_value_translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE attribute_values_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE attributes_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE attributes_documents_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE custom_form_answers_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE custom_form_field_attributes_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE custom_form_fields_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE custom_forms_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE documents_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE documents_translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE folders_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE folders_translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE log_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE login_attempts_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE node_type_fields_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE node_types_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE nodes_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE nodes_custom_forms_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE nodes_sources_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE nodes_sources_documents_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE nodes_to_nodes_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE redirections_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE roles_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE settings_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE settings_groups_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE tags_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE tags_translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE tags_translations_documents_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE url_aliases_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE user_log_entries_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE usergroups_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE users_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE attribute_group_translations (id INT NOT NULL, attribute_group_id INT DEFAULT NULL, translation_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_5C704A6862D643B7 ON attribute_group_translations (attribute_group_id)'); + $this->addSql('CREATE INDEX IDX_5C704A689CAA2B25 ON attribute_group_translations (translation_id)'); + $this->addSql('CREATE INDEX IDX_5C704A685E237E06 ON attribute_group_translations (name)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_5C704A6862D643B79CAA2B25 ON attribute_group_translations (attribute_group_id, translation_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_5C704A685E237E069CAA2B25 ON attribute_group_translations (name, translation_id)'); + $this->addSql('CREATE TABLE attribute_groups (id INT NOT NULL, canonical_name VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_D28C172A674D812 ON attribute_groups (canonical_name)'); + $this->addSql('CREATE INDEX IDX_D28C172A674D812 ON attribute_groups (canonical_name)'); + $this->addSql('CREATE TABLE attribute_translations (id INT NOT NULL, attribute_id INT DEFAULT NULL, translation_id INT DEFAULT NULL, label VARCHAR(255) NOT NULL, options TEXT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_4059D4A0B6E62EFA ON attribute_translations (attribute_id)'); + $this->addSql('CREATE INDEX IDX_4059D4A09CAA2B25 ON attribute_translations (translation_id)'); + $this->addSql('CREATE INDEX IDX_4059D4A0EA750E8 ON attribute_translations (label)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_4059D4A0B6E62EFA9CAA2B25 ON attribute_translations (attribute_id, translation_id)'); + $this->addSql('COMMENT ON COLUMN attribute_translations.options IS \'(DC2Type:simple_array)\''); + $this->addSql('CREATE TABLE attribute_value_translations (id INT NOT NULL, translation_id INT DEFAULT NULL, attribute_value INT DEFAULT NULL, value VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_1293849B9CAA2B25 ON attribute_value_translations (translation_id)'); + $this->addSql('CREATE INDEX IDX_1293849BFE4FBB82 ON attribute_value_translations (attribute_value)'); + $this->addSql('CREATE INDEX IDX_1293849B1D775834 ON attribute_value_translations (value)'); + $this->addSql('CREATE INDEX IDX_1293849B9CAA2B25FE4FBB82 ON attribute_value_translations (translation_id, attribute_value)'); + $this->addSql('CREATE TABLE attribute_values (id INT NOT NULL, attribute_id INT DEFAULT NULL, node_id INT DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_184662BCB6E62EFA ON attribute_values (attribute_id)'); + $this->addSql('CREATE INDEX IDX_184662BC460D9FD7 ON attribute_values (node_id)'); + $this->addSql('CREATE INDEX IDX_184662BCB6E62EFA460D9FD7 ON attribute_values (attribute_id, node_id)'); + $this->addSql('CREATE TABLE attributes (id INT NOT NULL, group_id INT DEFAULT NULL, code VARCHAR(255) NOT NULL, searchable BOOLEAN DEFAULT \'false\' NOT NULL, type INT NOT NULL, color VARCHAR(7) DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_319B9E7077153098 ON attributes (code)'); + $this->addSql('CREATE INDEX IDX_319B9E7077153098 ON attributes (code)'); + $this->addSql('CREATE INDEX IDX_319B9E708CDE5729 ON attributes (type)'); + $this->addSql('CREATE INDEX IDX_319B9E7094CD8C0D ON attributes (searchable)'); + $this->addSql('CREATE INDEX IDX_319B9E70FE54D947 ON attributes (group_id)'); + $this->addSql('CREATE TABLE attributes_documents (id INT NOT NULL, attribute_id INT DEFAULT NULL, document_id INT DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_67CCC9E0B6E62EFA ON attributes_documents (attribute_id)'); + $this->addSql('CREATE INDEX IDX_67CCC9E0C33F7837 ON attributes_documents (document_id)'); + $this->addSql('CREATE INDEX IDX_67CCC9E0462CE4F5 ON attributes_documents (position)'); + $this->addSql('CREATE INDEX IDX_67CCC9E0B6E62EFA462CE4F5 ON attributes_documents (attribute_id, position)'); + $this->addSql('CREATE TABLE custom_form_answers (id INT NOT NULL, custom_form_id INT DEFAULT NULL, ip VARCHAR(255) NOT NULL, submitted_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_1A3BB12658AFF2B0 ON custom_form_answers (custom_form_id)'); + $this->addSql('CREATE INDEX IDX_1A3BB126A5E3B32D ON custom_form_answers (ip)'); + $this->addSql('CREATE INDEX IDX_1A3BB1263182C73C ON custom_form_answers (submitted_at)'); + $this->addSql('CREATE TABLE custom_form_field_attributes (id INT NOT NULL, custom_form_answer_id INT DEFAULT NULL, custom_form_field_id INT DEFAULT NULL, value TEXT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_B7133605F1D6C2D1 ON custom_form_field_attributes (custom_form_answer_id)'); + $this->addSql('CREATE INDEX IDX_B71336057F13CC0F ON custom_form_field_attributes (custom_form_field_id)'); + $this->addSql('CREATE TABLE custom_form_answers_documents (customformfieldattribute_id INT NOT NULL, document_id INT NOT NULL, PRIMARY KEY(customformfieldattribute_id, document_id))'); + $this->addSql('CREATE INDEX IDX_E979F877C84CA2FC ON custom_form_answers_documents (customformfieldattribute_id)'); + $this->addSql('CREATE INDEX IDX_E979F877C33F7837 ON custom_form_answers_documents (document_id)'); + $this->addSql('CREATE TABLE custom_form_fields (id INT NOT NULL, custom_form_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, label VARCHAR(255) NOT NULL, placeholder VARCHAR(255) DEFAULT NULL, description TEXT DEFAULT NULL, default_values TEXT DEFAULT NULL, type INT NOT NULL, expanded BOOLEAN DEFAULT \'false\' NOT NULL, field_required BOOLEAN DEFAULT \'false\' NOT NULL, group_name VARCHAR(255) DEFAULT NULL, group_name_canonical VARCHAR(255) DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_4A3782EC58AFF2B0 ON custom_form_fields (custom_form_id)'); + $this->addSql('CREATE INDEX IDX_4A3782EC462CE4F5 ON custom_form_fields (position)'); + $this->addSql('CREATE INDEX IDX_4A3782EC77792576 ON custom_form_fields (group_name)'); + $this->addSql('CREATE INDEX IDX_4A3782EC8CDE5729 ON custom_form_fields (type)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_4A3782EC5E237E0658AFF2B0 ON custom_form_fields (name, custom_form_id)'); + $this->addSql('CREATE TABLE custom_forms (id INT NOT NULL, color VARCHAR(255) DEFAULT NULL, name VARCHAR(255) NOT NULL, display_name VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, email TEXT DEFAULT NULL, open BOOLEAN DEFAULT \'true\' NOT NULL, close_date TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_3E32E39E5E237E06 ON custom_forms (name)'); + $this->addSql('CREATE INDEX IDX_3E32E39E8B8E8428 ON custom_forms (created_at)'); + $this->addSql('CREATE INDEX IDX_3E32E39E43625D9F ON custom_forms (updated_at)'); + $this->addSql('CREATE TABLE documents (id INT NOT NULL, raw_document INT DEFAULT NULL, original INT DEFAULT NULL, raw BOOLEAN DEFAULT \'false\' NOT NULL, embedId VARCHAR(255) DEFAULT NULL, embedPlatform VARCHAR(255) DEFAULT NULL, filename VARCHAR(255) DEFAULT NULL, mime_type VARCHAR(255) DEFAULT NULL, folder VARCHAR(255) NOT NULL, private BOOLEAN DEFAULT \'false\' NOT NULL, imageWidth INT DEFAULT 0 NOT NULL, imageHeight INT DEFAULT 0 NOT NULL, average_color VARCHAR(7) DEFAULT NULL, filesize INT DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_A2B0728826CBD5A5 ON documents (raw_document)'); + $this->addSql('CREATE INDEX IDX_A2B072882F727085 ON documents (original)'); + $this->addSql('CREATE INDEX IDX_A2B072881AB3DB55 ON documents (raw)'); + $this->addSql('CREATE INDEX IDX_A2B07288D206C1D1 ON documents (private)'); + $this->addSql('CREATE INDEX IDX_A2B072881AB3DB55D206C1D1 ON documents (raw, private)'); + $this->addSql('CREATE INDEX IDX_A2B072882100AA2E ON documents (mime_type)'); + $this->addSql('CREATE TABLE documents_translations (id INT NOT NULL, translation_id INT DEFAULT NULL, document_id INT DEFAULT NULL, name VARCHAR(255) DEFAULT NULL, description TEXT DEFAULT NULL, copyright TEXT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_5CD2F5509CAA2B25 ON documents_translations (translation_id)'); + $this->addSql('CREATE INDEX IDX_5CD2F550C33F7837 ON documents_translations (document_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_5CD2F550C33F78379CAA2B25 ON documents_translations (document_id, translation_id)'); + $this->addSql('CREATE TABLE folders (id INT NOT NULL, parent_id INT DEFAULT NULL, folder_name VARCHAR(255) NOT NULL, visible BOOLEAN DEFAULT \'true\' NOT NULL, position DOUBLE PRECISION NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_FE37D30F47BC5813 ON folders (folder_name)'); + $this->addSql('CREATE INDEX IDX_FE37D30F727ACA70 ON folders (parent_id)'); + $this->addSql('CREATE INDEX IDX_FE37D30F7AB0E859 ON folders (visible)'); + $this->addSql('CREATE INDEX IDX_FE37D30F462CE4F5 ON folders (position)'); + $this->addSql('CREATE INDEX IDX_FE37D30F8B8E8428 ON folders (created_at)'); + $this->addSql('CREATE INDEX IDX_FE37D30F43625D9F ON folders (updated_at)'); + $this->addSql('CREATE TABLE documents_folders (folder_id INT NOT NULL, document_id INT NOT NULL, PRIMARY KEY(folder_id, document_id))'); + $this->addSql('CREATE INDEX IDX_617BB29C162CB942 ON documents_folders (folder_id)'); + $this->addSql('CREATE INDEX IDX_617BB29CC33F7837 ON documents_folders (document_id)'); + $this->addSql('CREATE TABLE folders_translations (id INT NOT NULL, folder_id INT DEFAULT NULL, translation_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_9F6A68B2162CB942 ON folders_translations (folder_id)'); + $this->addSql('CREATE INDEX IDX_9F6A68B29CAA2B25 ON folders_translations (translation_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_9F6A68B2162CB9429CAA2B25 ON folders_translations (folder_id, translation_id)'); + $this->addSql('CREATE TABLE log (id INT NOT NULL, user_id INT DEFAULT NULL, node_source_id INT DEFAULT NULL, username VARCHAR(255) DEFAULT NULL, message TEXT NOT NULL, level INT NOT NULL, datetime TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, client_ip VARCHAR(255) DEFAULT NULL, channel VARCHAR(255) DEFAULT NULL, additional_data JSON DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_8F3F68C5A76ED395 ON log (user_id)'); + $this->addSql('CREATE INDEX IDX_8F3F68C58E831402 ON log (node_source_id)'); + $this->addSql('CREATE INDEX IDX_8F3F68C593F3C6CA ON log (datetime)'); + $this->addSql('CREATE INDEX IDX_8F3F68C59AEACC13 ON log (level)'); + $this->addSql('CREATE INDEX IDX_8F3F68C5F85E0677 ON log (username)'); + $this->addSql('CREATE INDEX IDX_8F3F68C5A2F98E47 ON log (channel)'); + $this->addSql('CREATE TABLE login_attempts (id INT NOT NULL, ip_address VARCHAR(50) DEFAULT NULL, date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, blocks_login_until TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, username VARCHAR(255) NOT NULL, attempt_count INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_9163C7FBF85E0677 ON login_attempts (username)'); + $this->addSql('CREATE INDEX IDX_9163C7FBEFF8A4EEF85E0677 ON login_attempts (blocks_login_until, username)'); + $this->addSql('CREATE INDEX IDX_9163C7FBEFF8A4EEF85E067722FFD58C ON login_attempts (blocks_login_until, username, ip_address)'); + $this->addSql('COMMENT ON COLUMN login_attempts.date IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('CREATE TABLE node_type_fields (id INT NOT NULL, node_type_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, label VARCHAR(255) NOT NULL, placeholder VARCHAR(255) DEFAULT NULL, description TEXT DEFAULT NULL, default_values TEXT DEFAULT NULL, type INT NOT NULL, expanded BOOLEAN DEFAULT \'false\' NOT NULL, universal BOOLEAN DEFAULT \'false\' NOT NULL, exclude_from_search BOOLEAN DEFAULT \'false\' NOT NULL, min_length INT DEFAULT NULL, max_length INT DEFAULT NULL, indexed BOOLEAN DEFAULT \'false\' NOT NULL, visible BOOLEAN DEFAULT \'true\' NOT NULL, group_name VARCHAR(255) DEFAULT NULL, group_name_canonical VARCHAR(255) DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_1D3923596344C9E1 ON node_type_fields (node_type_id)'); + $this->addSql('CREATE INDEX IDX_1D3923597AB0E859 ON node_type_fields (visible)'); + $this->addSql('CREATE INDEX IDX_1D392359D9416D95 ON node_type_fields (indexed)'); + $this->addSql('CREATE INDEX IDX_1D392359462CE4F5 ON node_type_fields (position)'); + $this->addSql('CREATE INDEX IDX_1D39235977792576 ON node_type_fields (group_name)'); + $this->addSql('CREATE INDEX IDX_1D3923594BAF07A4 ON node_type_fields (group_name_canonical)'); + $this->addSql('CREATE INDEX IDX_1D3923598CDE5729 ON node_type_fields (type)'); + $this->addSql('CREATE INDEX IDX_1D392359A4B8F6E1 ON node_type_fields (universal)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1D3923595E237E066344C9E1 ON node_type_fields (name, node_type_id)'); + $this->addSql('CREATE TABLE node_types (id INT NOT NULL, name VARCHAR(255) NOT NULL, display_name VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, visible BOOLEAN DEFAULT \'true\' NOT NULL, publishable BOOLEAN DEFAULT \'false\' NOT NULL, reachable BOOLEAN DEFAULT \'true\' NOT NULL, hiding_nodes BOOLEAN DEFAULT \'false\' NOT NULL, hiding_non_reachable_nodes BOOLEAN DEFAULT \'false\' NOT NULL, color VARCHAR(255) DEFAULT NULL, default_ttl INT DEFAULT 0 NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_409B1BCC5E237E06 ON node_types (name)'); + $this->addSql('CREATE INDEX IDX_409B1BCC7AB0E859 ON node_types (visible)'); + $this->addSql('CREATE INDEX IDX_409B1BCC7697C594 ON node_types (publishable)'); + $this->addSql('CREATE INDEX IDX_409B1BCCFB696FF0 ON node_types (hiding_nodes)'); + $this->addSql('CREATE INDEX IDX_409B1BCC5A3C14C7 ON node_types (hiding_non_reachable_nodes)'); + $this->addSql('CREATE INDEX IDX_409B1BCC96ED695F ON node_types (reachable)'); + $this->addSql('CREATE TABLE nodes (id INT NOT NULL, parent_node_id INT DEFAULT NULL, node_name VARCHAR(255) NOT NULL, dynamic_node_name BOOLEAN DEFAULT \'true\' NOT NULL, home BOOLEAN DEFAULT \'false\' NOT NULL, visible BOOLEAN DEFAULT \'true\' NOT NULL, status INT NOT NULL, ttl INT DEFAULT 0 NOT NULL, locked BOOLEAN DEFAULT \'false\' NOT NULL, priority NUMERIC(2, 1) NOT NULL, hide_children BOOLEAN DEFAULT \'false\' NOT NULL, sterile BOOLEAN DEFAULT \'false\' NOT NULL, children_order VARCHAR(255) NOT NULL, children_order_direction VARCHAR(4) NOT NULL, position DOUBLE PRECISION NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, nodeType_id INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1D3D05FC9987F390 ON nodes (node_name)'); + $this->addSql('CREATE INDEX IDX_1D3D05FC47D04729 ON nodes (nodeType_id)'); + $this->addSql('CREATE INDEX IDX_1D3D05FC3445EB91 ON nodes (parent_node_id)'); + $this->addSql('CREATE INDEX IDX_1D3D05FC7AB0E859 ON nodes (visible)'); + $this->addSql('CREATE INDEX IDX_1D3D05FC7B00651C ON nodes (status)'); + $this->addSql('CREATE INDEX IDX_1D3D05FCEAD2C891 ON nodes (locked)'); + $this->addSql('CREATE INDEX IDX_1D3D05FCF32D8BE6 ON nodes (sterile)'); + $this->addSql('CREATE INDEX IDX_1D3D05FC462CE4F5 ON nodes (position)'); + $this->addSql('CREATE INDEX IDX_1D3D05FC8B8E8428 ON nodes (created_at)'); + $this->addSql('CREATE INDEX IDX_1D3D05FC43625D9F ON nodes (updated_at)'); + $this->addSql('CREATE INDEX IDX_1D3D05FC50E2D3D2 ON nodes (hide_children)'); + $this->addSql('CREATE INDEX IDX_1D3D05FC9987F3907B00651C ON nodes (node_name, status)'); + $this->addSql('CREATE INDEX IDX_1D3D05FC7AB0E8597B00651C ON nodes (visible, status)'); + $this->addSql('CREATE INDEX IDX_1D3D05FC7AB0E8597B00651C3445EB91 ON nodes (visible, status, parent_node_id)'); + $this->addSql('CREATE INDEX IDX_1D3D05FC7AB0E8593445EB91 ON nodes (visible, parent_node_id)'); + $this->addSql('CREATE INDEX IDX_1D3D05FC71D60CD0 ON nodes (home)'); + $this->addSql('CREATE TABLE nodes_tags (node_id INT NOT NULL, tag_id INT NOT NULL, PRIMARY KEY(node_id, tag_id))'); + $this->addSql('CREATE INDEX IDX_5B5CB38C460D9FD7 ON nodes_tags (node_id)'); + $this->addSql('CREATE INDEX IDX_5B5CB38CBAD26311 ON nodes_tags (tag_id)'); + $this->addSql('CREATE TABLE stack_types (node_id INT NOT NULL, nodetype_id INT NOT NULL, PRIMARY KEY(node_id, nodetype_id))'); + $this->addSql('CREATE INDEX IDX_DE24E53460D9FD7 ON stack_types (node_id)'); + $this->addSql('CREATE INDEX IDX_DE24E53886D7EB5 ON stack_types (nodetype_id)'); + $this->addSql('CREATE TABLE nodes_custom_forms (id INT NOT NULL, node_id INT DEFAULT NULL, custom_form_id INT DEFAULT NULL, node_type_field_id INT DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_4D401A0C460D9FD7 ON nodes_custom_forms (node_id)'); + $this->addSql('CREATE INDEX IDX_4D401A0C58AFF2B0 ON nodes_custom_forms (custom_form_id)'); + $this->addSql('CREATE INDEX IDX_4D401A0C47705282 ON nodes_custom_forms (node_type_field_id)'); + $this->addSql('CREATE INDEX IDX_4D401A0C462CE4F5 ON nodes_custom_forms (position)'); + $this->addSql('CREATE TABLE nodes_sources (id INT NOT NULL, node_id INT DEFAULT NULL, translation_id INT DEFAULT NULL, title VARCHAR(255) DEFAULT NULL, published_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, meta_title VARCHAR(255) NOT NULL, meta_keywords TEXT NOT NULL, meta_description TEXT NOT NULL, discr VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_7C7DED6D460D9FD7 ON nodes_sources (node_id)'); + $this->addSql('CREATE INDEX IDX_7C7DED6D9CAA2B25 ON nodes_sources (translation_id)'); + $this->addSql('CREATE INDEX IDX_7C7DED6D4AD26064 ON nodes_sources (discr)'); + $this->addSql('CREATE INDEX IDX_7C7DED6D4AD260649CAA2B25 ON nodes_sources (discr, translation_id)'); + $this->addSql('CREATE INDEX IDX_7C7DED6DE0D4FDE14AD260649CAA2B25 ON nodes_sources (published_at, discr, translation_id)'); + $this->addSql('CREATE INDEX IDX_7C7DED6D2B36786B ON nodes_sources (title)'); + $this->addSql('CREATE INDEX IDX_7C7DED6DE0D4FDE1 ON nodes_sources (published_at)'); + $this->addSql('CREATE INDEX IDX_7C7DED6DE0D4FDE19CAA2B25 ON nodes_sources (published_at, translation_id)'); + $this->addSql('CREATE INDEX IDX_7C7DED6D460D9FD79CAA2B25E0D4FDE1 ON nodes_sources (node_id, translation_id, published_at)'); + $this->addSql('CREATE INDEX IDX_7C7DED6D2B36786BE0D4FDE1 ON nodes_sources (title, published_at)'); + $this->addSql('CREATE INDEX IDX_7C7DED6D2B36786BE0D4FDE19CAA2B25 ON nodes_sources (title, published_at, translation_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_7C7DED6D460D9FD79CAA2B25 ON nodes_sources (node_id, translation_id)'); + $this->addSql('CREATE TABLE nodes_sources_documents (id INT NOT NULL, ns_id INT DEFAULT NULL, document_id INT DEFAULT NULL, node_type_field_id INT DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_1CD104F7AA2D61 ON nodes_sources_documents (ns_id)'); + $this->addSql('CREATE INDEX IDX_1CD104F7C33F7837 ON nodes_sources_documents (document_id)'); + $this->addSql('CREATE INDEX IDX_1CD104F747705282 ON nodes_sources_documents (node_type_field_id)'); + $this->addSql('CREATE INDEX IDX_1CD104F7462CE4F5 ON nodes_sources_documents (position)'); + $this->addSql('CREATE INDEX IDX_1CD104F7AA2D6147705282 ON nodes_sources_documents (ns_id, node_type_field_id)'); + $this->addSql('CREATE INDEX IDX_1CD104F7AA2D6147705282462CE4F5 ON nodes_sources_documents (ns_id, node_type_field_id, position)'); + $this->addSql('CREATE TABLE nodes_to_nodes (id INT NOT NULL, node_a_id INT DEFAULT NULL, node_b_id INT DEFAULT NULL, node_type_field_id INT DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_761F9A91FC7ADECE ON nodes_to_nodes (node_a_id)'); + $this->addSql('CREATE INDEX IDX_761F9A91EECF7120 ON nodes_to_nodes (node_b_id)'); + $this->addSql('CREATE INDEX IDX_761F9A9147705282 ON nodes_to_nodes (node_type_field_id)'); + $this->addSql('CREATE INDEX IDX_761F9A91462CE4F5 ON nodes_to_nodes (position)'); + $this->addSql('CREATE INDEX IDX_761F9A91FC7ADECE47705282 ON nodes_to_nodes (node_a_id, node_type_field_id)'); + $this->addSql('CREATE INDEX IDX_761F9A91FC7ADECE47705282462CE4F5 ON nodes_to_nodes (node_a_id, node_type_field_id, position)'); + $this->addSql('CREATE INDEX IDX_761F9A91EECF712047705282 ON nodes_to_nodes (node_b_id, node_type_field_id)'); + $this->addSql('CREATE INDEX IDX_761F9A91EECF712047705282462CE4F5 ON nodes_to_nodes (node_b_id, node_type_field_id, position)'); + $this->addSql('CREATE TABLE redirections (id INT NOT NULL, ns_id INT DEFAULT NULL, query VARCHAR(255) NOT NULL, redirectUri VARCHAR(255) DEFAULT NULL, type INT NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_38F5ECE424BDB5EB ON redirections (query)'); + $this->addSql('CREATE INDEX IDX_38F5ECE4AA2D61 ON redirections (ns_id)'); + $this->addSql('CREATE INDEX IDX_38F5ECE48B8E8428 ON redirections (created_at)'); + $this->addSql('CREATE INDEX IDX_38F5ECE443625D9F ON redirections (updated_at)'); + $this->addSql('CREATE TABLE roles (id INT NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_B63E2EC75E237E06 ON roles (name)'); + $this->addSql('CREATE TABLE settings (id INT NOT NULL, setting_group_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, value TEXT DEFAULT NULL, visible BOOLEAN DEFAULT \'true\' NOT NULL, encrypted BOOLEAN DEFAULT \'false\' NOT NULL, type INT NOT NULL, defaultValues TEXT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_E545A0C55E237E06 ON settings (name)'); + $this->addSql('CREATE INDEX IDX_E545A0C550DDE1BD ON settings (setting_group_id)'); + $this->addSql('CREATE INDEX IDX_E545A0C58CDE5729 ON settings (type)'); + $this->addSql('CREATE INDEX IDX_E545A0C55E237E06 ON settings (name)'); + $this->addSql('CREATE INDEX IDX_E545A0C57AB0E859 ON settings (visible)'); + $this->addSql('CREATE TABLE settings_groups (id INT NOT NULL, name VARCHAR(255) NOT NULL, in_menu BOOLEAN DEFAULT \'false\' NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_FFD519025E237E06 ON settings_groups (name)'); + $this->addSql('CREATE TABLE tags (id INT NOT NULL, parent_tag_id INT DEFAULT NULL, color VARCHAR(7) DEFAULT \'#000000\' NOT NULL, tag_name VARCHAR(255) NOT NULL, visible BOOLEAN DEFAULT \'true\' NOT NULL, children_order VARCHAR(255) DEFAULT \'position\' NOT NULL, children_order_direction VARCHAR(4) DEFAULT \'ASC\' NOT NULL, locked BOOLEAN DEFAULT \'false\' NOT NULL, position DOUBLE PRECISION NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6FBC9426B02CC1B0 ON tags (tag_name)'); + $this->addSql('CREATE INDEX IDX_6FBC9426F5C1A0D7 ON tags (parent_tag_id)'); + $this->addSql('CREATE INDEX IDX_6FBC94267AB0E859 ON tags (visible)'); + $this->addSql('CREATE INDEX IDX_6FBC9426EAD2C891 ON tags (locked)'); + $this->addSql('CREATE INDEX IDX_6FBC9426462CE4F5 ON tags (position)'); + $this->addSql('CREATE INDEX IDX_6FBC94268B8E8428 ON tags (created_at)'); + $this->addSql('CREATE INDEX IDX_6FBC942643625D9F ON tags (updated_at)'); + $this->addSql('CREATE INDEX IDX_6FBC9426F5C1A0D77AB0E859 ON tags (parent_tag_id, visible)'); + $this->addSql('CREATE TABLE tags_translations (id INT NOT NULL, tag_id INT DEFAULT NULL, translation_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_95D326DCBAD26311 ON tags_translations (tag_id)'); + $this->addSql('CREATE INDEX IDX_95D326DC9CAA2B25 ON tags_translations (translation_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_95D326DCBAD263119CAA2B25 ON tags_translations (tag_id, translation_id)'); + $this->addSql('CREATE TABLE tags_translations_documents (id INT NOT NULL, tag_translation_id INT DEFAULT NULL, document_id INT DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_6E886F1F22010F1 ON tags_translations_documents (tag_translation_id)'); + $this->addSql('CREATE INDEX IDX_6E886F1FC33F7837 ON tags_translations_documents (document_id)'); + $this->addSql('CREATE INDEX IDX_6E886F1F462CE4F5 ON tags_translations_documents (position)'); + $this->addSql('CREATE INDEX IDX_6E886F1F22010F1462CE4F5 ON tags_translations_documents (tag_translation_id, position)'); + $this->addSql('CREATE TABLE translations (id INT NOT NULL, locale VARCHAR(10) NOT NULL, override_locale VARCHAR(10) DEFAULT NULL, name VARCHAR(255) NOT NULL, default_translation BOOLEAN DEFAULT \'false\' NOT NULL, available BOOLEAN DEFAULT \'true\' NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_C6B7DA874180C698 ON translations (locale)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_C6B7DA873F824FD6 ON translations (override_locale)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_C6B7DA875E237E06 ON translations (name)'); + $this->addSql('CREATE INDEX IDX_C6B7DA87A58FA485 ON translations (available)'); + $this->addSql('CREATE INDEX IDX_C6B7DA87609A56D9 ON translations (default_translation)'); + $this->addSql('CREATE INDEX IDX_C6B7DA878B8E8428 ON translations (created_at)'); + $this->addSql('CREATE INDEX IDX_C6B7DA8743625D9F ON translations (updated_at)'); + $this->addSql('CREATE INDEX IDX_C6B7DA87A58FA485609A56D9 ON translations (available, default_translation)'); + $this->addSql('CREATE INDEX IDX_C6B7DA87A58FA4854180C698 ON translations (available, locale)'); + $this->addSql('CREATE INDEX IDX_C6B7DA87A58FA4853F824FD6 ON translations (available, override_locale)'); + $this->addSql('CREATE TABLE url_aliases (id INT NOT NULL, ns_id INT DEFAULT NULL, alias VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_E261ED65E16C6B94 ON url_aliases (alias)'); + $this->addSql('CREATE INDEX IDX_E261ED65AA2D61 ON url_aliases (ns_id)'); + $this->addSql('CREATE TABLE user_log_entries (id INT NOT NULL, user_id INT DEFAULT NULL, action VARCHAR(8) NOT NULL, logged_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, object_id VARCHAR(64) DEFAULT NULL, object_class VARCHAR(191) NOT NULL, version INT NOT NULL, data TEXT DEFAULT NULL, username VARCHAR(191) DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_BC2E42C7A76ED395 ON user_log_entries (user_id)'); + $this->addSql('CREATE INDEX log_class_lookup_idx ON user_log_entries (object_class)'); + $this->addSql('CREATE INDEX log_date_lookup_idx ON user_log_entries (logged_at)'); + $this->addSql('CREATE INDEX log_user_lookup_idx ON user_log_entries (username)'); + $this->addSql('CREATE INDEX log_version_lookup_idx ON user_log_entries (object_id, object_class, version)'); + $this->addSql('COMMENT ON COLUMN user_log_entries.data IS \'(DC2Type:array)\''); + $this->addSql('CREATE TABLE usergroups (id INT NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_98972EB45E237E06 ON usergroups (name)'); + $this->addSql('CREATE TABLE groups_roles (group_id INT NOT NULL, role_id INT NOT NULL, PRIMARY KEY(group_id, role_id))'); + $this->addSql('CREATE INDEX IDX_E79D4963FE54D947 ON groups_roles (group_id)'); + $this->addSql('CREATE INDEX IDX_E79D4963D60322AC ON groups_roles (role_id)'); + $this->addSql('CREATE TABLE users (id INT NOT NULL, chroot_id INT DEFAULT NULL, facebook_name VARCHAR(255) DEFAULT NULL, picture_url TEXT DEFAULT NULL, enabled BOOLEAN DEFAULT \'true\' NOT NULL, confirmation_token VARCHAR(255) DEFAULT NULL, password_requested_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, username VARCHAR(255) NOT NULL, salt VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL, last_login TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, expired BOOLEAN DEFAULT \'false\' NOT NULL, locked BOOLEAN DEFAULT \'false\' NOT NULL, credentials_expires_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, credentials_expired BOOLEAN DEFAULT \'false\' NOT NULL, expires_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, locale VARCHAR(7) DEFAULT NULL, email VARCHAR(255) NOT NULL, firstName VARCHAR(255) DEFAULT NULL, lastName VARCHAR(255) DEFAULT NULL, phone VARCHAR(255) DEFAULT NULL, company VARCHAR(255) DEFAULT NULL, job VARCHAR(255) DEFAULT NULL, birthday TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E9C05FB297 ON users (confirmation_token)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E9F85E0677 ON users (username)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E9E7927C74 ON users (email)'); + $this->addSql('CREATE INDEX IDX_1483A5E96483A539 ON users (chroot_id)'); + $this->addSql('CREATE INDEX IDX_1483A5E950F9BB84 ON users (enabled)'); + $this->addSql('CREATE INDEX IDX_1483A5E9194FED4B ON users (expired)'); + $this->addSql('CREATE INDEX IDX_1483A5E9F9D83E2 ON users (expires_at)'); + $this->addSql('CREATE INDEX IDX_1483A5E94180C698 ON users (locale)'); + $this->addSql('CREATE TABLE users_roles (user_id INT NOT NULL, role_id INT NOT NULL, PRIMARY KEY(user_id, role_id))'); + $this->addSql('CREATE INDEX IDX_51498A8EA76ED395 ON users_roles (user_id)'); + $this->addSql('CREATE INDEX IDX_51498A8ED60322AC ON users_roles (role_id)'); + $this->addSql('CREATE TABLE users_groups (user_id INT NOT NULL, group_id INT NOT NULL, PRIMARY KEY(user_id, group_id))'); + $this->addSql('CREATE INDEX IDX_FF8AB7E0A76ED395 ON users_groups (user_id)'); + $this->addSql('CREATE INDEX IDX_FF8AB7E0FE54D947 ON users_groups (group_id)'); + $this->addSql('ALTER TABLE attribute_group_translations ADD CONSTRAINT FK_5C704A6862D643B7 FOREIGN KEY (attribute_group_id) REFERENCES attribute_groups (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE attribute_group_translations ADD CONSTRAINT FK_5C704A689CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE attribute_translations ADD CONSTRAINT FK_4059D4A0B6E62EFA FOREIGN KEY (attribute_id) REFERENCES attributes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE attribute_translations ADD CONSTRAINT FK_4059D4A09CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE attribute_value_translations ADD CONSTRAINT FK_1293849B9CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE attribute_value_translations ADD CONSTRAINT FK_1293849BFE4FBB82 FOREIGN KEY (attribute_value) REFERENCES attribute_values (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE attribute_values ADD CONSTRAINT FK_184662BCB6E62EFA FOREIGN KEY (attribute_id) REFERENCES attributes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE attribute_values ADD CONSTRAINT FK_184662BC460D9FD7 FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE attributes ADD CONSTRAINT FK_319B9E70FE54D947 FOREIGN KEY (group_id) REFERENCES attribute_groups (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE attributes_documents ADD CONSTRAINT FK_67CCC9E0B6E62EFA FOREIGN KEY (attribute_id) REFERENCES attributes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE attributes_documents ADD CONSTRAINT FK_67CCC9E0C33F7837 FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE custom_form_answers ADD CONSTRAINT FK_1A3BB12658AFF2B0 FOREIGN KEY (custom_form_id) REFERENCES custom_forms (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE custom_form_field_attributes ADD CONSTRAINT FK_B7133605F1D6C2D1 FOREIGN KEY (custom_form_answer_id) REFERENCES custom_form_answers (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE custom_form_field_attributes ADD CONSTRAINT FK_B71336057F13CC0F FOREIGN KEY (custom_form_field_id) REFERENCES custom_form_fields (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE custom_form_answers_documents ADD CONSTRAINT FK_E979F877C84CA2FC FOREIGN KEY (customformfieldattribute_id) REFERENCES custom_form_field_attributes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE custom_form_answers_documents ADD CONSTRAINT FK_E979F877C33F7837 FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE custom_form_fields ADD CONSTRAINT FK_4A3782EC58AFF2B0 FOREIGN KEY (custom_form_id) REFERENCES custom_forms (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE documents ADD CONSTRAINT FK_A2B0728826CBD5A5 FOREIGN KEY (raw_document) REFERENCES documents (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE documents ADD CONSTRAINT FK_A2B072882F727085 FOREIGN KEY (original) REFERENCES documents (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE documents_translations ADD CONSTRAINT FK_5CD2F5509CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE documents_translations ADD CONSTRAINT FK_5CD2F550C33F7837 FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE folders ADD CONSTRAINT FK_FE37D30F727ACA70 FOREIGN KEY (parent_id) REFERENCES folders (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE documents_folders ADD CONSTRAINT FK_617BB29C162CB942 FOREIGN KEY (folder_id) REFERENCES folders (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE documents_folders ADD CONSTRAINT FK_617BB29CC33F7837 FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE folders_translations ADD CONSTRAINT FK_9F6A68B2162CB942 FOREIGN KEY (folder_id) REFERENCES folders (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE folders_translations ADD CONSTRAINT FK_9F6A68B29CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE log ADD CONSTRAINT FK_8F3F68C5A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE log ADD CONSTRAINT FK_8F3F68C58E831402 FOREIGN KEY (node_source_id) REFERENCES nodes_sources (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE node_type_fields ADD CONSTRAINT FK_1D3923596344C9E1 FOREIGN KEY (node_type_id) REFERENCES node_types (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes ADD CONSTRAINT FK_1D3D05FC47D04729 FOREIGN KEY (nodeType_id) REFERENCES node_types (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes ADD CONSTRAINT FK_1D3D05FC3445EB91 FOREIGN KEY (parent_node_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes_tags ADD CONSTRAINT FK_5B5CB38C460D9FD7 FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes_tags ADD CONSTRAINT FK_5B5CB38CBAD26311 FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE stack_types ADD CONSTRAINT FK_DE24E53460D9FD7 FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE stack_types ADD CONSTRAINT FK_DE24E53886D7EB5 FOREIGN KEY (nodetype_id) REFERENCES node_types (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes_custom_forms ADD CONSTRAINT FK_4D401A0C460D9FD7 FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes_custom_forms ADD CONSTRAINT FK_4D401A0C58AFF2B0 FOREIGN KEY (custom_form_id) REFERENCES custom_forms (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes_custom_forms ADD CONSTRAINT FK_4D401A0C47705282 FOREIGN KEY (node_type_field_id) REFERENCES node_type_fields (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes_sources ADD CONSTRAINT FK_7C7DED6D460D9FD7 FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes_sources ADD CONSTRAINT FK_7C7DED6D9CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes_sources_documents ADD CONSTRAINT FK_1CD104F7AA2D61 FOREIGN KEY (ns_id) REFERENCES nodes_sources (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes_sources_documents ADD CONSTRAINT FK_1CD104F7C33F7837 FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes_sources_documents ADD CONSTRAINT FK_1CD104F747705282 FOREIGN KEY (node_type_field_id) REFERENCES node_type_fields (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes_to_nodes ADD CONSTRAINT FK_761F9A91FC7ADECE FOREIGN KEY (node_a_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes_to_nodes ADD CONSTRAINT FK_761F9A91EECF7120 FOREIGN KEY (node_b_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes_to_nodes ADD CONSTRAINT FK_761F9A9147705282 FOREIGN KEY (node_type_field_id) REFERENCES node_type_fields (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE redirections ADD CONSTRAINT FK_38F5ECE4AA2D61 FOREIGN KEY (ns_id) REFERENCES nodes_sources (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE settings ADD CONSTRAINT FK_E545A0C550DDE1BD FOREIGN KEY (setting_group_id) REFERENCES settings_groups (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE tags ADD CONSTRAINT FK_6FBC9426F5C1A0D7 FOREIGN KEY (parent_tag_id) REFERENCES tags (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE tags_translations ADD CONSTRAINT FK_95D326DCBAD26311 FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE tags_translations ADD CONSTRAINT FK_95D326DC9CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE tags_translations_documents ADD CONSTRAINT FK_6E886F1F22010F1 FOREIGN KEY (tag_translation_id) REFERENCES tags_translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE tags_translations_documents ADD CONSTRAINT FK_6E886F1FC33F7837 FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE url_aliases ADD CONSTRAINT FK_E261ED65AA2D61 FOREIGN KEY (ns_id) REFERENCES nodes_sources (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE user_log_entries ADD CONSTRAINT FK_BC2E42C7A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE groups_roles ADD CONSTRAINT FK_E79D4963FE54D947 FOREIGN KEY (group_id) REFERENCES usergroups (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE groups_roles ADD CONSTRAINT FK_E79D4963D60322AC FOREIGN KEY (role_id) REFERENCES roles (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE users ADD CONSTRAINT FK_1483A5E96483A539 FOREIGN KEY (chroot_id) REFERENCES nodes (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE users_roles ADD CONSTRAINT FK_51498A8EA76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE users_roles ADD CONSTRAINT FK_51498A8ED60322AC FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE users_groups ADD CONSTRAINT FK_FF8AB7E0A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE users_groups ADD CONSTRAINT FK_FF8AB7E0FE54D947 FOREIGN KEY (group_id) REFERENCES usergroups (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); } public function down(Schema $schema) : void diff --git a/migrations/Version20230607134403.php b/migrations/Version20230607134403.php deleted file mode 100644 index 7b50e986..00000000 --- a/migrations/Version20230607134403.php +++ /dev/null @@ -1,80 +0,0 @@ -addSql('ALTER TABLE attribute_translations CHANGE label label VARCHAR(250) NOT NULL'); - $this->addSql('ALTER TABLE custom_form_answers CHANGE ip ip VARCHAR(46) NOT NULL'); - $this->addSql('ALTER TABLE custom_form_fields CHANGE name name VARCHAR(250) NOT NULL, CHANGE label label VARCHAR(250) NOT NULL, CHANGE placeholder placeholder VARCHAR(250) DEFAULT NULL, CHANGE group_name group_name VARCHAR(250) DEFAULT NULL, CHANGE group_name_canonical group_name_canonical VARCHAR(250) DEFAULT NULL'); - $this->addSql('ALTER TABLE custom_forms CHANGE color color VARCHAR(7) DEFAULT NULL, CHANGE name name VARCHAR(250) NOT NULL, CHANGE display_name display_name VARCHAR(250) NOT NULL'); - $this->addSql('ALTER TABLE documents CHANGE embedId embedId VARCHAR(250) DEFAULT NULL, CHANGE embedPlatform embedPlatform VARCHAR(100) DEFAULT NULL, CHANGE filename filename VARCHAR(250) DEFAULT NULL, CHANGE folder folder VARCHAR(100) NOT NULL'); - $this->addSql('ALTER TABLE documents_translations CHANGE name name VARCHAR(250) DEFAULT NULL'); - $this->addSql('ALTER TABLE folders CHANGE folder_name folder_name VARCHAR(250) NOT NULL'); - $this->addSql('ALTER TABLE folders_translations CHANGE name name VARCHAR(250) NOT NULL'); - $this->addSql('ALTER TABLE log CHANGE client_ip client_ip VARCHAR(46) DEFAULT NULL, CHANGE channel channel VARCHAR(64) DEFAULT NULL'); - $this->addSql('ALTER TABLE login_attempts CHANGE ip_address ip_address VARCHAR(46) DEFAULT NULL'); - $this->addSql('ALTER TABLE node_type_fields CHANGE name name VARCHAR(250) NOT NULL, CHANGE label label VARCHAR(250) NOT NULL, CHANGE placeholder placeholder VARCHAR(250) DEFAULT NULL, CHANGE group_name group_name VARCHAR(250) DEFAULT NULL, CHANGE group_name_canonical group_name_canonical VARCHAR(250) DEFAULT NULL'); - $this->addSql('ALTER TABLE node_types CHANGE name name VARCHAR(30) NOT NULL, CHANGE display_name display_name VARCHAR(250) NOT NULL, CHANGE color color VARCHAR(7) DEFAULT NULL'); - $this->addSql('ALTER TABLE nodes CHANGE children_order children_order VARCHAR(50) NOT NULL'); - $this->addSql('ALTER TABLE nodes_sources CHANGE title title VARCHAR(250) DEFAULT NULL, CHANGE meta_title meta_title VARCHAR(150) NOT NULL'); - $this->addSql('ALTER TABLE roles CHANGE name name VARCHAR(250) NOT NULL'); - $this->addSql('ALTER TABLE settings CHANGE name name VARCHAR(250) NOT NULL'); - $this->addSql('ALTER TABLE settings_groups CHANGE name name VARCHAR(250) NOT NULL'); - $this->addSql('ALTER TABLE tags CHANGE tag_name tag_name VARCHAR(250) NOT NULL, CHANGE children_order children_order VARCHAR(60) DEFAULT \'position\' NOT NULL'); - $this->addSql('ALTER TABLE tags_translations CHANGE name name VARCHAR(250) NOT NULL'); - $this->addSql('ALTER TABLE translations CHANGE name name VARCHAR(250) NOT NULL'); - $this->addSql('ALTER TABLE url_aliases CHANGE alias alias VARCHAR(250) NOT NULL'); - $this->addSql('ALTER TABLE usergroups CHANGE name name VARCHAR(250) NOT NULL'); - $this->addSql('ALTER TABLE users CHANGE facebook_name facebook_name VARCHAR(128) DEFAULT NULL, CHANGE confirmation_token confirmation_token VARCHAR(128) DEFAULT NULL, CHANGE username username VARCHAR(200) NOT NULL, CHANGE salt salt VARCHAR(64) NOT NULL, CHANGE password password VARCHAR(128) NOT NULL, CHANGE email email VARCHAR(200) NOT NULL, CHANGE firstName firstName VARCHAR(250) DEFAULT NULL, CHANGE lastName lastName VARCHAR(250) DEFAULT NULL, CHANGE phone phone VARCHAR(50) DEFAULT NULL, CHANGE company company VARCHAR(250) DEFAULT NULL, CHANGE job job VARCHAR(250) DEFAULT NULL, CHANGE publicName publicName VARCHAR(250) DEFAULT NULL'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('ALTER TABLE attribute_translations CHANGE label label VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('ALTER TABLE custom_form_answers CHANGE ip ip VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('ALTER TABLE custom_form_fields CHANGE group_name group_name VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE group_name_canonical group_name_canonical VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE label label VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE placeholder placeholder VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('ALTER TABLE custom_forms CHANGE color color VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE display_name display_name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('ALTER TABLE documents CHANGE embedId embedId VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE embedPlatform embedPlatform VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE filename filename VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE folder folder VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('ALTER TABLE documents_translations CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('ALTER TABLE folders CHANGE folder_name folder_name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('ALTER TABLE folders_translations CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('ALTER TABLE log CHANGE client_ip client_ip VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE channel channel VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('ALTER TABLE login_attempts CHANGE ip_address ip_address VARCHAR(50) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('ALTER TABLE node_type_fields CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE group_name group_name VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE group_name_canonical group_name_canonical VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE label label VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE placeholder placeholder VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('ALTER TABLE node_types CHANGE color color VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE display_name display_name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('ALTER TABLE nodes CHANGE children_order children_order VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('ALTER TABLE nodes_sources CHANGE title title VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE meta_title meta_title VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('ALTER TABLE roles CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('ALTER TABLE settings CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('ALTER TABLE settings_groups CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('ALTER TABLE tags CHANGE tag_name tag_name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE children_order children_order VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT \'position\' NOT NULL COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('ALTER TABLE tags_translations CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('ALTER TABLE translations CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('ALTER TABLE url_aliases CHANGE alias alias VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('ALTER TABLE usergroups CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('ALTER TABLE users CHANGE email email VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE facebook_name facebook_name VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE confirmation_token confirmation_token VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE username username VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE salt salt VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE password password VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE publicName publicName VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE firstName firstName VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE lastName lastName VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE phone phone VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE company company VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE job job VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`'); - } - - public function isTransactional(): bool - { - return false; - } -} diff --git a/migrations/Version20230615122615.php b/migrations/Version20230615122615.php deleted file mode 100644 index abed8b48..00000000 --- a/migrations/Version20230615122615.php +++ /dev/null @@ -1,38 +0,0 @@ -addSql('ALTER TABLE redirections ADD use_count INT DEFAULT 0 NOT NULL'); - $this->addSql('CREATE INDEX redirection_use_count ON redirections (use_count)'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('DROP INDEX redirection_use_count ON redirections'); - $this->addSql('ALTER TABLE redirections DROP use_count'); - } - - public function isTransactional(): bool - { - return false; - } -} diff --git a/migrations/Version20230628143106.php b/migrations/Version20230628143106.php deleted file mode 100644 index 98f4c214..00000000 --- a/migrations/Version20230628143106.php +++ /dev/null @@ -1,74 +0,0 @@ -addSql('ALTER TABLE log ADD entity_class VARCHAR(255) DEFAULT NULL, ADD entity_id VARCHAR(36) DEFAULT NULL'); - $this->addSql('CREATE INDEX IDX_8F3F68C541BF2C66 ON log (entity_class)'); - $this->addSql('CREATE INDEX IDX_8F3F68C541BF2C6681257D5D ON log (entity_class, entity_id)'); - $this->addSql('CREATE INDEX log_entity_class_datetime ON log (entity_class, datetime)'); - $this->addSql('CREATE INDEX log_entity_class_id_datetime ON log (entity_class, entity_id, datetime)'); - - // Move node_source_id to entity_class and entity_id - $nodeSourceClass = NodesSources::class; - $this->addSql(<<addSql('ALTER TABLE log DROP FOREIGN KEY FK_8F3F68C58E831402'); - $this->addSql('ALTER TABLE log DROP node_source_id'); - $this->addSql('DROP INDEX log_ns_datetime ON log'); - } - - public function down(Schema $schema): void - { - $this->addSql('ALTER TABLE log ADD node_source_id INT DEFAULT NULL'); - - // Move entity_class and entity_id to node_source_id - $nodeSourceClass = NodesSources::class; - $this->addSql(<<addSql('DROP INDEX IDX_8F3F68C541BF2C66 ON log'); - $this->addSql('DROP INDEX IDX_8F3F68C541BF2C6681257D5D ON log'); - $this->addSql('DROP INDEX log_entity_class_datetime ON log'); - $this->addSql('DROP INDEX log_entity_class_id_datetime ON log'); - $this->addSql('ALTER TABLE log DROP entity_class, DROP entity_id'); - $this->addSql('ALTER TABLE log ADD CONSTRAINT FK_8F3F68C58E831402 FOREIGN KEY (node_source_id) REFERENCES nodes_sources (id) ON UPDATE NO ACTION ON DELETE SET NULL'); - $this->addSql('CREATE INDEX log_ns_datetime ON log (node_source_id, datetime)'); - $this->addSql('CREATE INDEX IDX_8F3F68C58E831402 ON log (node_source_id)'); - } - - public function isTransactional(): bool - { - return false; - } -} diff --git a/migrations/Version20230628170203.php b/migrations/Version20230628170203.php deleted file mode 100644 index 84dc01b3..00000000 --- a/migrations/Version20230628170203.php +++ /dev/null @@ -1,40 +0,0 @@ -addSql('ALTER TABLE log DROP FOREIGN KEY FK_8F3F68C5A76ED395'); - $this->addSql('DROP INDEX IDX_8F3F68C5A76ED395 ON log'); - $this->addSql('ALTER TABLE log CHANGE user_id user_id VARCHAR(36) DEFAULT NULL'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('ALTER TABLE log CHANGE user_id user_id INT DEFAULT NULL'); - $this->addSql('ALTER TABLE log ADD CONSTRAINT FK_8F3F68C5A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON UPDATE NO ACTION ON DELETE SET NULL'); - $this->addSql('CREATE INDEX IDX_8F3F68C5A76ED395 ON log (user_id)'); - } - - public function isTransactional(): bool - { - return false; - } -} diff --git a/migrations/Version20230712171650.php b/migrations/Version20230712171650.php deleted file mode 100644 index cf03d449..00000000 --- a/migrations/Version20230712171650.php +++ /dev/null @@ -1,36 +0,0 @@ -addSql('DROP TABLE login_attempts'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('CREATE TABLE login_attempts (id INT AUTO_INCREMENT NOT NULL, ip_address VARCHAR(46) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, date DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', blocks_login_until DATETIME DEFAULT NULL, username VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, attempt_count INT DEFAULT NULL, INDEX IDX_9163C7FBEFF8A4EEF85E0677 (blocks_login_until, username), INDEX IDX_9163C7FBEFF8A4EEF85E067722FFD58C (blocks_login_until, username, ip_address), INDEX IDX_9163C7FBF85E0677 (username), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB COMMENT = \'\' '); - } - - public function isTransactional(): bool - { - return false; - } -} diff --git a/migrations/Version20230828092821.php b/migrations/Version20230828092821.php deleted file mode 100644 index 5eebad1f..00000000 --- a/migrations/Version20230828092821.php +++ /dev/null @@ -1,46 +0,0 @@ -addSql('CREATE INDEX custom_form_created_at ON custom_forms (created_at)'); - $this->addSql('CREATE INDEX custom_form_updated_at ON custom_forms (updated_at)'); - $this->addSql('CREATE INDEX redirection_created_at ON redirections (created_at)'); - $this->addSql('CREATE INDEX redirection_updated_at ON redirections (updated_at)'); - $this->addSql('CREATE INDEX idx_user_created_at ON users (created_at)'); - $this->addSql('CREATE INDEX idx_user_updated_at ON users (updated_at)'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('DROP INDEX custom_form_created_at ON custom_forms'); - $this->addSql('DROP INDEX custom_form_updated_at ON custom_forms'); - $this->addSql('DROP INDEX redirection_created_at ON redirections'); - $this->addSql('DROP INDEX redirection_updated_at ON redirections'); - $this->addSql('DROP INDEX idx_user_created_at ON users'); - $this->addSql('DROP INDEX idx_user_updated_at ON users'); - } - - public function isTransactional(): bool - { - return false; - } -} diff --git a/migrations/Version20230829082257.php b/migrations/Version20230829082257.php deleted file mode 100644 index fc6b3521..00000000 --- a/migrations/Version20230829082257.php +++ /dev/null @@ -1,38 +0,0 @@ -addSql('CREATE INDEX idx_attribute_value_node_position ON attribute_values (node_id, position)'); - $this->addSql('CREATE INDEX idx_attribute_value_position ON attribute_values (position)'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('DROP INDEX idx_attribute_value_node_position ON attribute_values'); - $this->addSql('DROP INDEX idx_attribute_value_position ON attribute_values'); - } - - public function isTransactional(): bool - { - return false; - } -} diff --git a/migrations/Version20230905140844.php b/migrations/Version20230905140844.php deleted file mode 100644 index 823029ea..00000000 --- a/migrations/Version20230905140844.php +++ /dev/null @@ -1,46 +0,0 @@ -addSql('ALTER TABLE attribute_values ADD realm_id INT DEFAULT NULL'); - $this->addSql('ALTER TABLE attribute_values ADD CONSTRAINT FK_184662BC9DFF5F89 FOREIGN KEY (realm_id) REFERENCES realms (id) ON DELETE SET NULL'); - $this->addSql('CREATE INDEX IDX_184662BC9DFF5F89 ON attribute_values (realm_id)'); - $this->addSql('ALTER TABLE attributes ADD realm_id INT DEFAULT NULL'); - $this->addSql('ALTER TABLE attributes ADD CONSTRAINT FK_319B9E709DFF5F89 FOREIGN KEY (realm_id) REFERENCES realms (id) ON DELETE SET NULL'); - $this->addSql('CREATE INDEX IDX_319B9E709DFF5F89 ON attributes (realm_id)'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('ALTER TABLE attribute_values DROP FOREIGN KEY FK_184662BC9DFF5F89'); - $this->addSql('DROP INDEX IDX_184662BC9DFF5F89 ON attribute_values'); - $this->addSql('ALTER TABLE attribute_values DROP realm_id'); - $this->addSql('ALTER TABLE attributes DROP FOREIGN KEY FK_319B9E709DFF5F89'); - $this->addSql('DROP INDEX IDX_319B9E709DFF5F89 ON attributes'); - $this->addSql('ALTER TABLE attributes DROP realm_id'); - } - - public function isTransactional(): bool - { - return false; - } -} diff --git a/migrations/Version20230915134833.php b/migrations/Version20230915134833.php deleted file mode 100644 index cc0bd784..00000000 --- a/migrations/Version20230915134833.php +++ /dev/null @@ -1,36 +0,0 @@ -addSql('ALTER TABLE users DROP salt'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('ALTER TABLE users ADD salt VARCHAR(64) NOT NULL'); - } - - public function isTransactional(): bool - { - return false; - } -} diff --git a/migrations/Version20231012154717.php b/migrations/Version20231012154717.php deleted file mode 100644 index 5b71054d..00000000 --- a/migrations/Version20231012154717.php +++ /dev/null @@ -1,36 +0,0 @@ -addSql('ALTER TABLE documents ADD image_crop_alignment VARCHAR(12) DEFAULT NULL'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('ALTER TABLE documents DROP image_crop_alignment'); - } - - public function isTransactional(): bool - { - return false; - } -} diff --git a/migrations/Version20231013132932.php b/migrations/Version20231013132932.php deleted file mode 100644 index b4b0829a..00000000 --- a/migrations/Version20231013132932.php +++ /dev/null @@ -1,38 +0,0 @@ -addSql('ALTER TABLE node_types ADD attributable TINYINT(1) DEFAULT 1 NOT NULL'); - $this->addSql('CREATE INDEX IDX_409B1BCC1F470BBD ON node_types (attributable)'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('DROP INDEX IDX_409B1BCC1F470BBD ON node_types'); - $this->addSql('ALTER TABLE node_types DROP attributable'); - } - - public function isTransactional(): bool - { - return false; - } -} diff --git a/migrations/Version20240214143213.php b/migrations/Version20240214143213.php deleted file mode 100644 index c1fae99a..00000000 --- a/migrations/Version20240214143213.php +++ /dev/null @@ -1,31 +0,0 @@ -addSql('ALTER TABLE nodes_sources DROP meta_keywords'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('ALTER TABLE nodes_sources ADD meta_keywords LONGTEXT NOT NULL'); - } -} diff --git a/migrations/Version20240214143849.php b/migrations/Version20240214143849.php deleted file mode 100644 index 677a88f2..00000000 --- a/migrations/Version20240214143849.php +++ /dev/null @@ -1,31 +0,0 @@ -addSql('ALTER TABLE nodes DROP priority'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('ALTER TABLE nodes ADD priority NUMERIC(2, 1) NOT NULL'); - } -} diff --git a/migrations/Version20240214145403.php b/migrations/Version20240214145403.php deleted file mode 100644 index a20f4e63..00000000 --- a/migrations/Version20240214145403.php +++ /dev/null @@ -1,31 +0,0 @@ -addSql('ALTER TABLE settings DROP encrypted'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('ALTER TABLE settings ADD encrypted TINYINT(1) DEFAULT 0 NOT NULL'); - } -} diff --git a/migrations/Version20240305124809.php b/migrations/Version20240305124809.php deleted file mode 100644 index 8f7e1c46..00000000 --- a/migrations/Version20240305124809.php +++ /dev/null @@ -1,31 +0,0 @@ -addSql('ALTER TABLE realms_nodes CHANGE realm_id realm_id INT NOT NULL'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('ALTER TABLE realms_nodes CHANGE realm_id realm_id INT DEFAULT NULL'); - } -} diff --git a/migrations/Version20240305125653.php b/migrations/Version20240305125653.php deleted file mode 100644 index 254249fe..00000000 --- a/migrations/Version20240305125653.php +++ /dev/null @@ -1,31 +0,0 @@ -addSql('ALTER TABLE nodes_sources_documents CHANGE ns_id ns_id INT NOT NULL, CHANGE document_id document_id INT NOT NULL, CHANGE node_type_field_id node_type_field_id INT NOT NULL'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('ALTER TABLE nodes_sources_documents CHANGE ns_id ns_id INT DEFAULT NULL, CHANGE document_id document_id INT DEFAULT NULL, CHANGE node_type_field_id node_type_field_id INT DEFAULT NULL'); - } -} diff --git a/migrations/Version20240305132609.php b/migrations/Version20240305132609.php deleted file mode 100644 index 13ccd8a8..00000000 --- a/migrations/Version20240305132609.php +++ /dev/null @@ -1,31 +0,0 @@ -addSql('ALTER TABLE nodes_to_nodes CHANGE node_a_id node_a_id INT NOT NULL, CHANGE node_b_id node_b_id INT NOT NULL, CHANGE node_type_field_id node_type_field_id INT NOT NULL'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('ALTER TABLE nodes_to_nodes CHANGE node_a_id node_a_id INT DEFAULT NULL, CHANGE node_b_id node_b_id INT DEFAULT NULL, CHANGE node_type_field_id node_type_field_id INT DEFAULT NULL'); - } -} diff --git a/migrations/Version20240305133122.php b/migrations/Version20240305133122.php deleted file mode 100644 index f21faee5..00000000 --- a/migrations/Version20240305133122.php +++ /dev/null @@ -1,31 +0,0 @@ -addSql('ALTER TABLE nodes_custom_forms CHANGE node_id node_id INT NOT NULL, CHANGE custom_form_id custom_form_id INT NOT NULL, CHANGE node_type_field_id node_type_field_id INT NOT NULL'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('ALTER TABLE nodes_custom_forms CHANGE node_id node_id INT DEFAULT NULL, CHANGE custom_form_id custom_form_id INT DEFAULT NULL, CHANGE node_type_field_id node_type_field_id INT DEFAULT NULL'); - } -} diff --git a/migrations/Version20240305133641.php b/migrations/Version20240305133641.php deleted file mode 100644 index ba74548a..00000000 --- a/migrations/Version20240305133641.php +++ /dev/null @@ -1,31 +0,0 @@ -addSql('ALTER TABLE nodes_sources CHANGE node_id node_id INT NOT NULL, CHANGE translation_id translation_id INT NOT NULL'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('ALTER TABLE nodes_sources CHANGE node_id node_id INT DEFAULT NULL, CHANGE translation_id translation_id INT DEFAULT NULL'); - } -} diff --git a/migrations/Version20240305134734.php b/migrations/Version20240305134734.php deleted file mode 100644 index 0bb9c334..00000000 --- a/migrations/Version20240305134734.php +++ /dev/null @@ -1,37 +0,0 @@ -addSql('ALTER TABLE documents_translations CHANGE translation_id translation_id INT NOT NULL, CHANGE document_id document_id INT NOT NULL'); - $this->addSql('ALTER TABLE folders_translations CHANGE folder_id folder_id INT NOT NULL, CHANGE translation_id translation_id INT NOT NULL'); - $this->addSql('ALTER TABLE tags_translations CHANGE tag_id tag_id INT NOT NULL, CHANGE translation_id translation_id INT NOT NULL'); - $this->addSql('ALTER TABLE tags_translations_documents CHANGE tag_translation_id tag_translation_id INT NOT NULL, CHANGE document_id document_id INT NOT NULL'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('ALTER TABLE documents_translations CHANGE translation_id translation_id INT DEFAULT NULL, CHANGE document_id document_id INT DEFAULT NULL'); - $this->addSql('ALTER TABLE folders_translations CHANGE folder_id folder_id INT DEFAULT NULL, CHANGE translation_id translation_id INT DEFAULT NULL'); - $this->addSql('ALTER TABLE tags_translations CHANGE tag_id tag_id INT DEFAULT NULL, CHANGE translation_id translation_id INT DEFAULT NULL'); - $this->addSql('ALTER TABLE tags_translations_documents CHANGE tag_translation_id tag_translation_id INT DEFAULT NULL, CHANGE document_id document_id INT DEFAULT NULL'); - } -} diff --git a/migrations/Version20240305142243.php b/migrations/Version20240305142243.php deleted file mode 100644 index 9a8f36a5..00000000 --- a/migrations/Version20240305142243.php +++ /dev/null @@ -1,62 +0,0 @@ -addSql('ALTER TABLE attribute_group_translations CHANGE attribute_group_id attribute_group_id INT NOT NULL, CHANGE translation_id translation_id INT NOT NULL'); - $this->addSql('ALTER TABLE attribute_translations CHANGE attribute_id attribute_id INT NOT NULL, CHANGE translation_id translation_id INT NOT NULL'); - $this->addSql('ALTER TABLE attribute_value_translations CHANGE translation_id translation_id INT NOT NULL, CHANGE attribute_value attribute_value INT NOT NULL'); - $this->addSql('ALTER TABLE attribute_values CHANGE attribute_id attribute_id INT NOT NULL, CHANGE node_id node_id INT NOT NULL'); - $this->addSql('ALTER TABLE attributes_documents CHANGE attribute_id attribute_id INT NOT NULL, CHANGE document_id document_id INT NOT NULL'); - $this->addSql('ALTER TABLE custom_form_answers CHANGE custom_form_id custom_form_id INT NOT NULL'); - $this->addSql('ALTER TABLE custom_form_field_attributes CHANGE custom_form_answer_id custom_form_answer_id INT NOT NULL, CHANGE custom_form_field_id custom_form_field_id INT NOT NULL'); - $this->addSql('ALTER TABLE custom_form_fields CHANGE custom_form_id custom_form_id INT NOT NULL'); - - // Remove all node_type_fields where node_type_id is null before changing it to not-nullable - $this->addSql('DELETE FROM node_type_fields WHERE node_type_id IS NULL'); - $this->addSql('ALTER TABLE node_type_fields CHANGE node_type_id node_type_id INT NOT NULL'); - - // Remove all nodes where nodeType_id is null before changing it to not-nullable - $this->addSql('DELETE FROM nodes WHERE nodeType_id IS NULL'); - $this->addSql('ALTER TABLE nodes CHANGE nodeType_id nodeType_id INT NOT NULL'); - - // Remove all url_aliases where ns_id is null before changing it to not-nullable - $this->addSql('DELETE FROM url_aliases WHERE ns_id IS NULL'); - $this->addSql('ALTER TABLE url_aliases DROP FOREIGN KEY FK_E261ED65AA2D61'); - $this->addSql('ALTER TABLE url_aliases CHANGE ns_id ns_id INT NOT NULL'); - $this->addSql('ALTER TABLE url_aliases ADD CONSTRAINT FK_E261ED65AA2D61 FOREIGN KEY (ns_id) REFERENCES nodes_sources (id) ON DELETE CASCADE'); - } - - public function down(Schema $schema): void - { - $this->addSql('ALTER TABLE attribute_group_translations CHANGE translation_id translation_id INT DEFAULT NULL, CHANGE attribute_group_id attribute_group_id INT DEFAULT NULL'); - $this->addSql('ALTER TABLE attribute_translations CHANGE translation_id translation_id INT DEFAULT NULL, CHANGE attribute_id attribute_id INT DEFAULT NULL'); - $this->addSql('ALTER TABLE attribute_value_translations CHANGE translation_id translation_id INT DEFAULT NULL, CHANGE attribute_value attribute_value INT DEFAULT NULL'); - $this->addSql('ALTER TABLE attribute_values CHANGE node_id node_id INT DEFAULT NULL, CHANGE attribute_id attribute_id INT DEFAULT NULL'); - $this->addSql('ALTER TABLE attributes_documents CHANGE attribute_id attribute_id INT DEFAULT NULL, CHANGE document_id document_id INT DEFAULT NULL'); - $this->addSql('ALTER TABLE custom_form_answers CHANGE custom_form_id custom_form_id INT DEFAULT NULL'); - $this->addSql('ALTER TABLE custom_form_field_attributes CHANGE custom_form_answer_id custom_form_answer_id INT DEFAULT NULL, CHANGE custom_form_field_id custom_form_field_id INT DEFAULT NULL'); - $this->addSql('ALTER TABLE custom_form_fields CHANGE custom_form_id custom_form_id INT DEFAULT NULL'); - $this->addSql('ALTER TABLE node_type_fields CHANGE node_type_id node_type_id INT DEFAULT NULL'); - $this->addSql('ALTER TABLE nodes CHANGE nodeType_id nodeType_id INT DEFAULT NULL'); - $this->addSql('ALTER TABLE url_aliases DROP FOREIGN KEY FK_E261ED65AA2D61'); - $this->addSql('ALTER TABLE url_aliases CHANGE ns_id ns_id INT DEFAULT NULL'); - $this->addSql('ALTER TABLE url_aliases ADD CONSTRAINT FK_E261ED65AA2D61 FOREIGN KEY (ns_id) REFERENCES nodes_sources (id) ON UPDATE NO ACTION ON DELETE NO ACTION'); - } -} diff --git a/migrations/Version20240305143443.php b/migrations/Version20240305143443.php deleted file mode 100644 index f2469a84..00000000 --- a/migrations/Version20240305143443.php +++ /dev/null @@ -1,31 +0,0 @@ -addSql('CREATE INDEX node_parent_position ON nodes (parent_node_id, position)'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('DROP INDEX node_parent_position ON nodes'); - } -} diff --git a/migrations/Version20240318184555.php b/migrations/Version20240318184555.php deleted file mode 100644 index e03fcf8c..00000000 --- a/migrations/Version20240318184555.php +++ /dev/null @@ -1,34 +0,0 @@ -addSql('ALTER TABLE nodes_custom_forms ADD field_name VARCHAR(250)'); - $this->addSql('ALTER TABLE nodes_sources_documents ADD field_name VARCHAR(250)'); - $this->addSql('ALTER TABLE nodes_to_nodes ADD field_name VARCHAR(250)'); - } - - public function down(Schema $schema): void - { - $this->throwIrreversibleMigrationException('Cannot convert node-type fields name back to their identifiers'); - } -} diff --git a/migrations/Version20240318184556.php b/migrations/Version20240318184556.php deleted file mode 100644 index 69d87323..00000000 --- a/migrations/Version20240318184556.php +++ /dev/null @@ -1,68 +0,0 @@ -connection->beginTransaction(); - $this->connection->executeStatement('UPDATE nodes_custom_forms SET field_name = (SELECT name FROM node_type_fields WHERE id = node_type_field_id)'); - $this->connection->executeStatement('UPDATE nodes_sources_documents SET field_name = (SELECT name FROM node_type_fields WHERE id = node_type_field_id)'); - $this->connection->executeStatement('UPDATE nodes_to_nodes SET field_name = (SELECT name FROM node_type_fields WHERE id = node_type_field_id)'); - $this->connection->commit(); - - $this->addSql('ALTER TABLE nodes_custom_forms DROP FOREIGN KEY FK_4D401A0C47705282'); - $this->addSql('DROP INDEX IDX_4D401A0C47705282 ON nodes_custom_forms'); - $this->addSql('DROP INDEX customform_node_field_position ON nodes_custom_forms'); - $this->addSql('CREATE INDEX customform_node_field_position ON nodes_custom_forms (node_id, field_name, position)'); - $this->addSql('ALTER TABLE nodes_sources_documents DROP FOREIGN KEY FK_1CD104F747705282'); - $this->addSql('DROP INDEX IDX_1CD104F747705282 ON nodes_sources_documents'); - $this->addSql('DROP INDEX nsdoc_field ON nodes_sources_documents'); - $this->addSql('DROP INDEX nsdoc_field_position ON nodes_sources_documents'); - $this->addSql('CREATE INDEX nsdoc_field ON nodes_sources_documents (ns_id, field_name)'); - $this->addSql('CREATE INDEX nsdoc_field_position ON nodes_sources_documents (ns_id, field_name, position)'); - $this->addSql('ALTER TABLE nodes_to_nodes DROP FOREIGN KEY FK_761F9A9147705282'); - $this->addSql('DROP INDEX IDX_761F9A9147705282 ON nodes_to_nodes'); - $this->addSql('DROP INDEX node_a_field ON nodes_to_nodes'); - $this->addSql('DROP INDEX node_a_field_position ON nodes_to_nodes'); - $this->addSql('DROP INDEX node_b_field ON nodes_to_nodes'); - $this->addSql('DROP INDEX node_b_field_position ON nodes_to_nodes'); - $this->addSql('CREATE INDEX node_a_field ON nodes_to_nodes (node_a_id, field_name)'); - $this->addSql('CREATE INDEX node_a_field_position ON nodes_to_nodes (node_a_id, field_name, position)'); - $this->addSql('CREATE INDEX node_b_field ON nodes_to_nodes (node_b_id, field_name)'); - $this->addSql('CREATE INDEX node_b_field_position ON nodes_to_nodes (node_b_id, field_name, position)'); - - /* - * DESTRUCTIVE OPERATIONS - */ - $this->addSql('ALTER TABLE nodes_custom_forms CHANGE field_name field_name VARCHAR(250) NOT NULL'); - $this->addSql('ALTER TABLE nodes_sources_documents CHANGE field_name field_name VARCHAR(250) NOT NULL'); - $this->addSql('ALTER TABLE nodes_to_nodes CHANGE field_name field_name VARCHAR(250) NOT NULL'); - - $this->addSql('ALTER TABLE nodes_custom_forms DROP node_type_field_id'); - $this->addSql('ALTER TABLE nodes_sources_documents DROP node_type_field_id'); - $this->addSql('ALTER TABLE nodes_to_nodes DROP node_type_field_id'); - } - - public function down(Schema $schema): void - { - $this->throwIrreversibleMigrationException('Cannot convert node-type fields name back to their identifiers'); - } -} diff --git a/migrations/Version20240318204224.php b/migrations/Version20240318204224.php deleted file mode 100644 index 0897dc0b..00000000 --- a/migrations/Version20240318204224.php +++ /dev/null @@ -1,44 +0,0 @@ -connection->executeQuery('SELECT max(length(name)) FROM `node_type_fields`'); - $maxLength = $result->fetchOne(); - - $this->skipIf(!is_numeric($maxLength), 'Cannot find node_type_fields name maximum length.'); - $this->skipIf($maxLength >= 50, 'You have at least on node_type_field name that exceed 50 characters long.'); - - // this up() migration is auto-generated, please modify it to your needs - $this->addSql('ALTER TABLE node_type_fields CHANGE name name VARCHAR(50) NOT NULL'); - $this->addSql('CREATE INDEX ntf_name ON node_type_fields (name)'); - $this->addSql('ALTER TABLE nodes_custom_forms CHANGE field_name field_name VARCHAR(50) NOT NULL'); - $this->addSql('ALTER TABLE nodes_sources_documents CHANGE field_name field_name VARCHAR(50) NOT NULL'); - $this->addSql('ALTER TABLE nodes_to_nodes CHANGE field_name field_name VARCHAR(50) NOT NULL'); - } - - public function down(Schema $schema): void - { - $this->addSql('DROP INDEX ntf_name ON node_type_fields'); - $this->addSql('ALTER TABLE node_type_fields CHANGE name name VARCHAR(250) NOT NULL'); - $this->addSql('ALTER TABLE nodes_custom_forms CHANGE field_name field_name VARCHAR(250) NOT NULL'); - $this->addSql('ALTER TABLE nodes_sources_documents CHANGE field_name field_name VARCHAR(250) NOT NULL'); - $this->addSql('ALTER TABLE nodes_to_nodes CHANGE field_name field_name VARCHAR(250) NOT NULL'); - } -} diff --git a/migrations/Version20240603210209.php b/migrations/Version20240603210209.php deleted file mode 100644 index 7aa4ccd3..00000000 --- a/migrations/Version20240603210209.php +++ /dev/null @@ -1,33 +0,0 @@ -addSql('ALTER TABLE attributes ADD weight INT DEFAULT 0 NOT NULL'); - $this->addSql('CREATE INDEX IDX_319B9E707CD5541 ON attributes (weight)'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('DROP INDEX IDX_319B9E707CD5541 ON attributes'); - $this->addSql('ALTER TABLE attributes DROP weight'); - } -} diff --git a/migrations/Version20240604143759.php b/migrations/Version20240604143759.php deleted file mode 100644 index da201acc..00000000 --- a/migrations/Version20240604143759.php +++ /dev/null @@ -1,33 +0,0 @@ -addSql('CREATE INDEX IDX_319B9E70665648E9 ON attributes (color)'); - $this->addSql('ALTER TABLE node_types ADD attributable_by_weight TINYINT(1) DEFAULT 0 NOT NULL'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('DROP INDEX IDX_319B9E70665648E9 ON attributes'); - $this->addSql('ALTER TABLE node_types DROP attributable_by_weight'); - } -} diff --git a/phpstan.neon b/phpstan.neon index 882156d4..8785b98d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,17 +1,12 @@ parameters: - level: 7 + level: 6 paths: - src excludePaths: - */node_modules/* - */bower_components/* - */static/* - doctrine: - repositoryClass: RZ\Roadiz\CoreBundle\Repository\EntityRepository - objectManagerLoader: ./tests/object-manager.php ignoreErrors: - - identifier: missingType.iterableValue - - identifier: missingType.generics - '#Call to an undefined method RZ\\Roadiz\\CoreBundle\\Repository#' - '#Call to an undefined method RZ\\Roadiz\\UserBundle\\Repository#' - '#Call to an undefined method Doctrine\\Persistence\\ObjectRepository#' @@ -31,11 +26,9 @@ parameters: - '#Doctrine\\ORM\\Mapping\\GeneratedValue constructor expects#' - '#type mapping mismatch: property can contain Doctrine\\Common\\Collections\\Collection]+> but database expects Doctrine\\Common\\Collections\\Collection&iterable<[^\>]+>#' - '#should return Doctrine\\Common\\Collections\\Collection]+Interface> but returns Doctrine\\Common\\Collections\\Collection]+>#' - - '#but returns Doctrine\\Common\\Collections\\ReadableCollection]+>#' - - '#does not accept Doctrine\\Common\\Collections\\ReadableCollection]+>#' reportUnmatchedIgnoredErrors: false - treatPhpDocTypesAsCertain: false + checkGenericClassInNonGenericObjectType: false + checkMissingIterableValueType: false includes: - vendor/phpstan/phpstan-doctrine/extension.neon - - vendor/phpstan/phpstan-doctrine/rules.neon diff --git a/phpunit.xml.dist b/phpunit.xml.dist deleted file mode 100644 index c0f0120e..00000000 --- a/phpunit.xml.dist +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - src/Test - - - - - - src - - - - - - - - - - - - diff --git a/src/Api/Breadcrumbs/NodesSourcesBreadcrumbsFactory.php b/src/Api/Breadcrumbs/NodesSourcesBreadcrumbsFactory.php index 6f02c639..5a5e3fe9 100644 --- a/src/Api/Breadcrumbs/NodesSourcesBreadcrumbsFactory.php +++ b/src/Api/Breadcrumbs/NodesSourcesBreadcrumbsFactory.php @@ -20,6 +20,8 @@ public function create(?PersistableInterface $entity): ?BreadcrumbsInterface } if ( + null === $entity->getNode() || + null === $entity->getNode()->getNodeType() || !$entity->isReachable() ) { return null; diff --git a/src/Api/Controller/GetWebResponseByPathController.php b/src/Api/Controller/GetWebResponseByPathController.php index 2e924a4f..6441d6a4 100644 --- a/src/Api/Controller/GetWebResponseByPathController.php +++ b/src/Api/Controller/GetWebResponseByPathController.php @@ -4,96 +4,72 @@ namespace RZ\Roadiz\CoreBundle\Api\Controller; -use ApiPlatform\Api\IriConverterInterface; -use ApiPlatform\Exception\InvalidArgumentException; -use ApiPlatform\Exception\ResourceClassNotFoundException; -use ApiPlatform\Metadata\Exception\OperationNotFoundException; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use Psr\Log\LoggerInterface; +use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Core\Exception\InvalidArgumentException; use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; use RZ\Roadiz\CoreBundle\Api\DataTransformer\WebResponseDataTransformerInterface; use RZ\Roadiz\CoreBundle\Api\Model\WebResponseInterface; -use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\Redirection; -use RZ\Roadiz\CoreBundle\NodeType\ApiResourceOperationNameGenerator; use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; use RZ\Roadiz\CoreBundle\Routing\PathResolverInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\String\UnicodeString; final class GetWebResponseByPathController extends AbstractController { + private RequestStack $requestStack; + private PathResolverInterface $pathResolver; + private WebResponseDataTransformerInterface $webResponseDataTransformer; + private IriConverterInterface $iriConverter; + private PreviewResolverInterface $previewResolver; + public function __construct( - private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, - private readonly PathResolverInterface $pathResolver, - private readonly WebResponseDataTransformerInterface $webResponseDataTransformer, - private readonly IriConverterInterface $iriConverter, - private readonly PreviewResolverInterface $previewResolver, - private readonly ApiResourceOperationNameGenerator $apiResourceOperationNameGenerator, - private readonly LoggerInterface $logger + RequestStack $requestStack, + PathResolverInterface $pathResolver, + WebResponseDataTransformerInterface $webResponseDataTransformer, + IriConverterInterface $iriConverter, + PreviewResolverInterface $previewResolver ) { + $this->requestStack = $requestStack; + $this->pathResolver = $pathResolver; + $this->webResponseDataTransformer = $webResponseDataTransformer; + $this->iriConverter = $iriConverter; + $this->previewResolver = $previewResolver; } - public function __invoke(?Request $request): ?WebResponseInterface + public function __invoke(): ?WebResponseInterface { try { if ( - null === $request || - empty($request->query->get('path')) + null === $this->requestStack->getMainRequest() || + empty($this->requestStack->getMainRequest()->query->get('path')) ) { throw new InvalidArgumentException('path query parameter is mandatory'); } $resource = $this->normalizeResourcePath( - $request, - (string) $request->query->get('path') + (string) $this->requestStack->getMainRequest()->query->get('path') ); - $request->attributes->set('id', $resource->getId()); - $request->attributes->set('path', (string) $request->query->get('path')); - $request->attributes->set('_route_params', [ - ...$request->attributes->get('_route_params', []), - 'path' => (string) $request->query->get('path'), - ]); - - try { - /* - * Force API Platform to look for real resource configuration and serialization - * context. You must define "%entity%_get_by_path" operation for your WebResponse resource configuration. - * It should be generated automatically by Roadiz when you create new reachable NodeTypes. - */ - $resourceClass = get_class($resource); - $operationName = $this->apiResourceOperationNameGenerator->generateGetByPath($resourceClass); - $webResponseClass = $request->attributes->get('_api_resource_class'); - $operation = $this->resourceMetadataCollectionFactory - ->create($webResponseClass) - ->getOperation($operationName); - $request->attributes->set('_api_operation', $operation); - $request->attributes->set('_web_response_item_class', $resourceClass); - $request->attributes->set('_api_operation_name', $operationName); - } catch (OperationNotFoundException $exception) { - // Do not fail if operation is not found - // But warn in logs about missing operation configuration for this resource - $this->logger->warning($exception->getMessage()); - } - - $request->attributes->set('_stateless', true); - - if ($resource instanceof NodesSources) { - $request->attributes->set('_translation', $resource->getTranslation()); - $request->attributes->set('_locale', $resource->getTranslation()->getPreferredLocale()); - } - - $data = $this->webResponseDataTransformer->transform($resource, WebResponseInterface::class); - $request->attributes->set('data', $data); - - return $data; - } catch (ResourceNotFoundException | ResourceClassNotFoundException $exception) { - throw $this->createNotFoundException($exception->getMessage(), $exception); + $this->requestStack->getMainRequest()->attributes->set('data', $resource); + $this->requestStack->getMainRequest()->attributes->set('id', $resource->getId()); + /* + * Force API Platform to look for real resource configuration and serialization + * context. You must define "itemOperations.getByPath" for your API resource configuration. + */ + $this->requestStack->getMainRequest()->attributes->set('_api_resource_class', get_class($resource)); + return $this->webResponseDataTransformer->transform($resource, WebResponseInterface::class); + } catch (ResourceNotFoundException $exception) { + throw new NotFoundHttpException($exception->getMessage(), $exception); } } - protected function normalizeResourcePath(?Request $request, string $path): PersistableInterface + /** + * @param string $path + * @return PersistableInterface + */ + protected function normalizeResourcePath(string $path): PersistableInterface { /* * Serve any PersistableInterface Resource by implementing @@ -128,11 +104,11 @@ protected function normalizeResourcePath(?Request $request, string $path): Persi * Recursive call to normalize path coming from Redirection if redirected path * is internal (starting with /) */ - return $this->normalizeResourcePath($request, $resource->getRedirectUri()); + return $this->normalizeResourcePath($resource->getRedirectUri()); } } - $this->addResourceToCacheTags($request, $resource); + $this->addResourceToCacheTags($resource); /* * Or plain entity @@ -140,11 +116,12 @@ protected function normalizeResourcePath(?Request $request, string $path): Persi return $resource; } - protected function addResourceToCacheTags(?Request $request, PersistableInterface $resource): void + protected function addResourceToCacheTags(PersistableInterface $resource): void { + $request = $this->requestStack->getMainRequest(); if (null !== $request) { - $iri = $this->iriConverter->getIriFromResource($resource); - $request->attributes->set('_resources', $request->attributes->get('_resources', []) + [ $iri => $iri ]); + $iri = $this->iriConverter->getIriFromItem($resource); + $request->attributes->set('_resources', $request->attributes->get('_resources', []) + [$iri]); } } } diff --git a/src/Api/Controller/TranslationAwareControllerTrait.php b/src/Api/Controller/TranslationAwareControllerTrait.php index 47533414..f291948d 100644 --- a/src/Api/Controller/TranslationAwareControllerTrait.php +++ b/src/Api/Controller/TranslationAwareControllerTrait.php @@ -4,7 +4,6 @@ namespace RZ\Roadiz\CoreBundle\Api\Controller; -use Doctrine\ORM\NonUniqueResultException; use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; @@ -17,20 +16,12 @@ trait TranslationAwareControllerTrait abstract protected function getManagerRegistry(): ManagerRegistry; abstract protected function getPreviewResolver(): PreviewResolverInterface; - /** - * @throws NonUniqueResultException - */ protected function getTranslation(Request $request): TranslationInterface { $locale = $request->query->get('_locale'); - $requestTranslation = $request->attributes->get('_translation'); - if ($requestTranslation instanceof TranslationInterface) { - return $requestTranslation; - } - /** @var TranslationRepository $repository */ $repository = $this->getManagerRegistry()->getRepository(TranslationInterface::class); - if (!\is_string($locale) || $locale === '') { + if (null === $locale) { return $repository->findDefault(); } diff --git a/src/Api/DataTransformer/AttributeOutputDataTransformer.php b/src/Api/DataTransformer/AttributeOutputDataTransformer.php new file mode 100644 index 00000000..15bdea15 --- /dev/null +++ b/src/Api/DataTransformer/AttributeOutputDataTransformer.php @@ -0,0 +1,45 @@ +group = $data->getGroup(); + $output->code = $data->getCode(); + $output->type = $data->getType(); + $output->color = $data->getColor(); + $output->documents = $data->getDocuments()->toArray(); + + if (isset($context['translation']) && $context['translation'] instanceof TranslationInterface) { + $output->name = $data->getLabelOrCode($context['translation']); + } + return $output; + } + + /** + * @inheritDoc + */ + public function supportsTransformation($data, string $to, array $context = []): bool + { + return AttributeOutput::class === $to && $data instanceof AttributeInterface; + } +} diff --git a/src/Api/DataTransformer/AttributeValueOutputDataTransformer.php b/src/Api/DataTransformer/AttributeValueOutputDataTransformer.php new file mode 100644 index 00000000..9421721b --- /dev/null +++ b/src/Api/DataTransformer/AttributeValueOutputDataTransformer.php @@ -0,0 +1,48 @@ +attribute = $data->getAttribute(); + $output->attributable = $data->getAttributable(); + $output->type = $data->getType(); + + if (isset($context['translation']) && $context['translation'] instanceof TranslationInterface) { + $translatedData = $data->getAttributeValueTranslation($context['translation']); + $output->label = $data->getAttribute()->getLabelOrCode($context['translation']); + if ($translatedData instanceof AttributeValueTranslationInterface) { + $output->value = $translatedData->getValue(); + } + } + return $output; + } + + /** + * @inheritDoc + */ + public function supportsTransformation($data, string $to, array $context = []): bool + { + return AttributeValueOutput::class === $to && $data instanceof AttributeValueInterface; + } +} diff --git a/src/Api/DataTransformer/BaseNodesSourcesOutputDataTransformer.php b/src/Api/DataTransformer/BaseNodesSourcesOutputDataTransformer.php new file mode 100644 index 00000000..2465864f --- /dev/null +++ b/src/Api/DataTransformer/BaseNodesSourcesOutputDataTransformer.php @@ -0,0 +1,35 @@ +transformNodesSources($output, $data, $context); + } + + /** + * @inheritDoc + */ + public function supportsTransformation($data, string $to, array $context = []): bool + { + return NodesSourcesOutput::class === $to && $data instanceof NodesSources; + } +} diff --git a/src/Api/DataTransformer/BlocksAwareWebResponseOutputDataTransformerTrait.php b/src/Api/DataTransformer/BlocksAwareWebResponseOutputDataTransformerTrait.php index 2e89b12e..b5be9688 100644 --- a/src/Api/DataTransformer/BlocksAwareWebResponseOutputDataTransformerTrait.php +++ b/src/Api/DataTransformer/BlocksAwareWebResponseOutputDataTransformerTrait.php @@ -8,7 +8,6 @@ use RZ\Roadiz\CoreBundle\Api\Model\BlocksAwareWebResponseInterface; use RZ\Roadiz\CoreBundle\Api\Model\RealmsAwareWebResponseInterface; use RZ\Roadiz\CoreBundle\Api\Model\WebResponseInterface; -use RZ\Roadiz\CoreBundle\Api\TreeWalker\TreeWalkerGenerator; use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\TreeWalker\AbstractWalker; use RZ\TreeWalker\WalkerContextInterface; @@ -17,7 +16,6 @@ trait BlocksAwareWebResponseOutputDataTransformerTrait { abstract protected function getWalkerContext(): WalkerContextInterface; abstract protected function getCacheItemPool(): CacheItemPoolInterface; - abstract protected function getTreeWalkerGenerator(): TreeWalkerGenerator; abstract protected function getChildrenNodeSourceWalkerMaxLevel(): int; /** @@ -25,17 +23,22 @@ abstract protected function getChildrenNodeSourceWalkerMaxLevel(): int; */ abstract protected function getChildrenNodeSourceWalkerClassname(): string; + /** + * @param BlocksAwareWebResponseInterface $output + * @param NodesSources $data + * @return WebResponseInterface + */ protected function injectBlocks(BlocksAwareWebResponseInterface $output, NodesSources $data): WebResponseInterface { if (!$output instanceof RealmsAwareWebResponseInterface || !$output->isHidingBlocks()) { - $walker = $this->getTreeWalkerGenerator()->buildForRoot( + /** @var class-string $childrenNodeSourceWalkerClassname */ + $childrenNodeSourceWalkerClassname = $this->getChildrenNodeSourceWalkerClassname(); + $output->setBlocks($childrenNodeSourceWalkerClassname::build( $data, - $this->getChildrenNodeSourceWalkerClassname(), $this->getWalkerContext(), $this->getChildrenNodeSourceWalkerMaxLevel(), $this->getCacheItemPool() - ); - $output->setBlocks($walker->getChildren()); + )->getChildren()); } return $output; diff --git a/src/Api/DataTransformer/CustomFormOutputDataTransformer.php b/src/Api/DataTransformer/CustomFormOutputDataTransformer.php new file mode 100644 index 00000000..c4411bca --- /dev/null +++ b/src/Api/DataTransformer/CustomFormOutputDataTransformer.php @@ -0,0 +1,59 @@ +urlGenerator = $urlGenerator; + } + + /** + * @inheritDoc + */ + public function transform($data, string $to, array $context = []): object + { + if (!$data instanceof CustomForm) { + throw new \InvalidArgumentException('Data to transform must be instance of ' . CustomForm::class); + } + $output = new CustomFormOutput(); + $output->name = $data->getDisplayName(); + $output->color = $data->getColor(); + $output->description = $data->getDescription(); + $output->slug = (new AsciiSlugger())->slug($data->getName())->snake()->toString(); + $output->open = $data->isFormStillOpen(); + $output->definitionUrl = $this->urlGenerator->generate('api_custom_forms_item_definition', [ + 'id' => $data->getId() + ]); + $output->postUrl = $this->urlGenerator->generate('api_custom_forms_item_post', [ + 'id' => $data->getId() + ]); + + return $output; + } + + /** + * @inheritDoc + */ + public function supportsTransformation($data, string $to, array $context = []): bool + { + return CustomFormOutput::class === $to && $data instanceof CustomForm; + } +} diff --git a/src/Api/DataTransformer/DocumentOutputDataTransformer.php b/src/Api/DataTransformer/DocumentOutputDataTransformer.php new file mode 100644 index 00000000..7455219f --- /dev/null +++ b/src/Api/DataTransformer/DocumentOutputDataTransformer.php @@ -0,0 +1,96 @@ +documentFinder = $documentFinder; + } + + /** + * @inheritDoc + */ + public function transform($object, string $to, array $context = []): object + { + if (!$object instanceof Document) { + throw new \InvalidArgumentException('Data to transform must be instance of ' . DocumentInterface::class); + } + $output = new DocumentOutput(); + $output->relativePath = $object->getRelativePath(); + $output->processable = $object->isProcessable(); + $output->type = $object->getShortType(); + $output->imageWidth = $object->getImageWidth(); + $output->imageHeight = $object->getImageHeight(); + $output->mimeType = $object->getMimeType(); + $output->alt = $object->getFilename(); + $output->embedId = $object->getEmbedId(); + $output->embedPlatform = $object->getEmbedPlatform(); + $output->imageAverageColor = $object->getImageAverageColor(); + $output->mediaDuration = $object->getMediaDuration(); + + /** @var array $serializationGroups */ + $serializationGroups = isset($context['groups']) && is_array($context['groups']) ? $context['groups'] : []; + + if (($object->isEmbed() || !$object->isImage()) && false !== $object->getThumbnails()->first()) { + $output->thumbnail = $object->getThumbnails()->first(); + } + + if (isset($context['translation']) && $context['translation'] instanceof TranslationInterface) { + $translatedData = $object->getDocumentTranslationsByTranslation($context['translation'])->first() ?: null; + if ($translatedData instanceof DocumentTranslation) { + $output->name = $translatedData->getName(); + $output->description = $translatedData->getDescription(); + $output->copyright = $translatedData->getCopyright(); + $output->alt = !empty($translatedData->getName()) ? $translatedData->getName() : $object->getFilename(); + $output->externalUrl = $translatedData->getExternalUrl(); + } + } + + if (in_array('document_folders', $serializationGroups)) { + $output->folders = $object->getFolders()->toArray(); + } + + if (in_array('document_display_sources', $serializationGroups)) { + if ($object->isLocal() && $object->isVideo()) { + foreach ($this->documentFinder->findVideosWithFilename($object->getRelativePath()) as $document) { + if ($document->getRelativePath() !== $object->getRelativePath()) { + $output->altSources[] = $document; + } + } + } elseif ($object->isLocal() && $object->isAudio()) { + foreach ($this->documentFinder->findAudiosWithFilename($object->getRelativePath()) as $document) { + if ($document->getRelativePath() !== $object->getRelativePath()) { + $output->altSources[] = $document; + } + } + } + } + + return $output; + } + + /** + * @inheritDoc + */ + public function supportsTransformation($data, string $to, array $context = []): bool + { + return DocumentOutput::class === $to && $data instanceof DocumentInterface; + } +} diff --git a/src/Api/DataTransformer/FolderOutputDataTransformer.php b/src/Api/DataTransformer/FolderOutputDataTransformer.php new file mode 100644 index 00000000..63d8b79b --- /dev/null +++ b/src/Api/DataTransformer/FolderOutputDataTransformer.php @@ -0,0 +1,49 @@ +name = $data->getName(); + $output->slug = $data->getFolderName(); + $output->visible = $data->getVisible(); + $output->position = $data->getPosition(); + + if (isset($context['translation']) && $context['translation'] instanceof TranslationInterface) { + $translatedData = $data->getTranslatedFoldersByTranslation($context['translation'])->first() ?: null; + if ($translatedData instanceof FolderTranslation) { + $output->name = $translatedData->getName(); + } + } + return $output; + } + + /** + * @inheritDoc + */ + public function supportsTransformation($data, string $to, array $context = []): bool + { + return FolderOutput::class === $to && $data instanceof FolderInterface; + } +} diff --git a/src/Api/DataTransformer/NodeOutputDataTransformer.php b/src/Api/DataTransformer/NodeOutputDataTransformer.php new file mode 100644 index 00000000..3dec987d --- /dev/null +++ b/src/Api/DataTransformer/NodeOutputDataTransformer.php @@ -0,0 +1,39 @@ +nodeName = $data->getNodeName(); + $output->visible = $data->isVisible(); + $output->position = $data->getPosition(); + $output->tags = $data->getTags()->toArray(); + return $output; + } + + /** + * @inheritDoc + */ + public function supportsTransformation($data, string $to, array $context = []): bool + { + return NodeOutput::class === $to && $data instanceof Node; + } +} diff --git a/src/Api/DataTransformer/NodesSourcesOutputDataTransformer.php b/src/Api/DataTransformer/NodesSourcesOutputDataTransformer.php new file mode 100644 index 00000000..03c85bbd --- /dev/null +++ b/src/Api/DataTransformer/NodesSourcesOutputDataTransformer.php @@ -0,0 +1,52 @@ +urlGenerator = $urlGenerator; + } + + protected function transformNodesSources( + NodesSourcesDto $output, + NodesSources $data, + array $context = [] + ): NodesSourcesDto { + $output->title = $data->getTitle(); + $output->node = $data->getNode(); + $output->metaTitle = $data->getMetaTitle(); + $output->metaDescription = $data->getMetaDescription(); + $output->translation = $data->getTranslation(); + $output->slug = $data->getIdentifier(); + if ($data->isPublishable()) { + $output->publishedAt = $data->getPublishedAt(); + } + return $output; + } + + /** + * @inheritDoc + */ + public function supportsTransformation($data, string $to, array $context = []): bool + { + return $data instanceof NodesSources; + } +} diff --git a/src/Api/DataTransformer/TagOutputDataTransformer.php b/src/Api/DataTransformer/TagOutputDataTransformer.php new file mode 100644 index 00000000..947a0acc --- /dev/null +++ b/src/Api/DataTransformer/TagOutputDataTransformer.php @@ -0,0 +1,53 @@ +slug = $data->getTagName(); + $output->color = $data->getColor(); + $output->visible = $data->isVisible(); + $output->position = $data->getPosition(); + if ($data->getParent() instanceof Tag) { + $output->parent = $data->getParent(); + } + + if (isset($context['translation']) && $context['translation'] instanceof TranslationInterface) { + $translatedData = $data->getTranslatedTagsByTranslation($context['translation'])->first() ?: null; + if ($translatedData instanceof TagTranslation) { + $output->name = $translatedData->getName(); + $output->description = $translatedData->getDescription(); + $output->documents = $translatedData->getDocuments(); + } + } + return $output; + } + + /** + * @inheritDoc + */ + public function supportsTransformation($data, string $to, array $context = []): bool + { + return TagOutput::class === $to && $data instanceof Tag; + } +} diff --git a/src/Api/DataTransformer/TranslationOutputDataTransformer.php b/src/Api/DataTransformer/TranslationOutputDataTransformer.php new file mode 100644 index 00000000..41f6d702 --- /dev/null +++ b/src/Api/DataTransformer/TranslationOutputDataTransformer.php @@ -0,0 +1,36 @@ +locale = $data->getPreferredLocale(); + $output->defaultTranslation = $data->isDefaultTranslation(); + $output->available = $data->isAvailable(); + $output->name = $data->getName(); + return $output; + } + + /** + * @inheritDoc + */ + public function supportsTransformation($data, string $to, array $context = []): bool + { + return TranslationOutput::class === $to && $data instanceof TranslationInterface; + } +} diff --git a/src/Api/DataTransformer/WebResponseDataTransformerInterface.php b/src/Api/DataTransformer/WebResponseDataTransformerInterface.php index 424d02a3..a6d97c0f 100644 --- a/src/Api/DataTransformer/WebResponseDataTransformerInterface.php +++ b/src/Api/DataTransformer/WebResponseDataTransformerInterface.php @@ -4,19 +4,25 @@ namespace RZ\Roadiz\CoreBundle\Api\DataTransformer; -use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; use RZ\Roadiz\CoreBundle\Api\Model\WebResponseInterface; interface WebResponseDataTransformerInterface { /** - * @template T of PersistableInterface - * @param T $object + * Transforms the given object to something else, usually another object. + * This must return the original object if no transformations have been done. + * + * @param object $object * @param string $to * @param array $context - * @return WebResponseInterface|null + * @return WebResponseInterface|null */ - public function transform(PersistableInterface $object, string $to, array $context = []): ?WebResponseInterface; + public function transform($object, string $to, array $context = []): ?WebResponseInterface; - public function createWebResponse(): WebResponseInterface; + /** + * Checks whether the transformation is supported for a given data and context. + * + * @param object|array $data object on normalize / array on denormalize + */ + public function supportsTransformation($data, string $to, array $context = []): bool; } diff --git a/src/Api/DataTransformer/WebResponseOutputDataTransformer.php b/src/Api/DataTransformer/WebResponseOutputDataTransformer.php index 7cd9a1e1..950eeef1 100644 --- a/src/Api/DataTransformer/WebResponseOutputDataTransformer.php +++ b/src/Api/DataTransformer/WebResponseOutputDataTransformer.php @@ -8,15 +8,12 @@ use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; use RZ\Roadiz\CoreBundle\Api\Breadcrumbs\BreadcrumbsFactoryInterface; -use RZ\Roadiz\CoreBundle\Api\Model\BlocksAwareWebResponseInterface; use RZ\Roadiz\CoreBundle\Api\Model\NodesSourcesHeadFactoryInterface; -use RZ\Roadiz\CoreBundle\Api\Model\RealmsAwareWebResponseInterface; +use RZ\Roadiz\CoreBundle\Api\Model\WebResponse; use RZ\Roadiz\CoreBundle\Api\Model\WebResponseInterface; use RZ\Roadiz\CoreBundle\Api\TreeWalker\AutoChildrenNodeSourceWalker; -use RZ\Roadiz\CoreBundle\Api\TreeWalker\TreeWalkerGenerator; use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Realm\RealmResolverInterface; -use RZ\TreeWalker\AbstractWalker; use RZ\TreeWalker\WalkerContextInterface; use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -26,26 +23,27 @@ class WebResponseOutputDataTransformer implements WebResponseDataTransformerInte use BlocksAwareWebResponseOutputDataTransformerTrait; use RealmsAwareWebResponseOutputDataTransformerTrait; - /** - * @param NodesSourcesHeadFactoryInterface $nodesSourcesHeadFactory - * @param BreadcrumbsFactoryInterface $breadcrumbsFactory - * @param WalkerContextInterface $walkerContext - * @param CacheItemPoolInterface $cacheItemPool - * @param UrlGeneratorInterface $urlGenerator - * @param RealmResolverInterface $realmResolver - * @param TreeWalkerGenerator $treeWalkerGenerator - * @param class-string $webResponseClass - */ + private NodesSourcesHeadFactoryInterface $nodesSourcesHeadFactory; + private BreadcrumbsFactoryInterface $breadcrumbsFactory; + private WalkerContextInterface $walkerContext; + private CacheItemPoolInterface $cacheItemPool; + private UrlGeneratorInterface $urlGenerator; + private RealmResolverInterface $realmResolver; + public function __construct( - protected readonly NodesSourcesHeadFactoryInterface $nodesSourcesHeadFactory, - protected readonly BreadcrumbsFactoryInterface $breadcrumbsFactory, - protected readonly WalkerContextInterface $walkerContext, - protected readonly CacheItemPoolInterface $cacheItemPool, - protected readonly UrlGeneratorInterface $urlGenerator, - protected readonly RealmResolverInterface $realmResolver, - protected readonly TreeWalkerGenerator $treeWalkerGenerator, - private readonly string $webResponseClass + NodesSourcesHeadFactoryInterface $nodesSourcesHeadFactory, + BreadcrumbsFactoryInterface $breadcrumbsFactory, + WalkerContextInterface $walkerContext, + CacheItemPoolInterface $cacheItemPool, + UrlGeneratorInterface $urlGenerator, + RealmResolverInterface $realmResolver ) { + $this->nodesSourcesHeadFactory = $nodesSourcesHeadFactory; + $this->breadcrumbsFactory = $breadcrumbsFactory; + $this->walkerContext = $walkerContext; + $this->cacheItemPool = $cacheItemPool; + $this->urlGenerator = $urlGenerator; + $this->realmResolver = $realmResolver; } protected function getWalkerContext(): WalkerContextInterface @@ -63,14 +61,6 @@ protected function getChildrenNodeSourceWalkerMaxLevel(): int return 5; } - public function getTreeWalkerGenerator(): TreeWalkerGenerator - { - return $this->treeWalkerGenerator; - } - - /** - * @return class-string - */ protected function getChildrenNodeSourceWalkerClassname(): string { return AutoChildrenNodeSourceWalker::class; @@ -81,33 +71,40 @@ protected function getRealmResolver(): RealmResolverInterface return $this->realmResolver; } - public function createWebResponse(): WebResponseInterface - { - return new ($this->webResponseClass)(); - } - - public function transform(PersistableInterface $object, string $to, array $context = []): ?WebResponseInterface + /** + * @inheritDoc + */ + public function transform($object, string $to, array $context = []): ?WebResponseInterface { - $output = $this->createWebResponse(); - $output->setItem($object); + if (!$object instanceof PersistableInterface) { + throw new \InvalidArgumentException( + 'Data to transform must be instance of ' . + PersistableInterface::class + ); + } + $output = new WebResponse(); + $output->item = $object; if ($object instanceof NodesSources) { - if ($output instanceof RealmsAwareWebResponseInterface) { - $this->injectRealms($output, $object); - } - if ($output instanceof BlocksAwareWebResponseInterface) { - $this->injectBlocks($output, $object); - } + $this->injectRealms($output, $object); + $this->injectBlocks($output, $object); - $output->setPath($this->urlGenerator->generate(RouteObjectInterface::OBJECT_BASED_ROUTE_NAME, [ + $output->path = $this->urlGenerator->generate(RouteObjectInterface::OBJECT_BASED_ROUTE_NAME, [ RouteObjectInterface::ROUTE_OBJECT => $object - ], UrlGeneratorInterface::ABSOLUTE_PATH)); - $output->setHead($this->nodesSourcesHeadFactory->createForNodeSource($object)); - $output->setBreadcrumbs($this->breadcrumbsFactory->create($object)); - $output->setMaxAge($object->getNode()->getTtl() * 60); + ], UrlGeneratorInterface::ABSOLUTE_PATH); + $output->head = $this->nodesSourcesHeadFactory->createForNodeSource($object); + $output->breadcrumbs = $this->breadcrumbsFactory->create($object); } if ($object instanceof TranslationInterface) { - $output->setHead($this->nodesSourcesHeadFactory->createForTranslation($object)); + $output->head = $this->nodesSourcesHeadFactory->createForTranslation($object); } return $output; } + + /** + * @inheritDoc + */ + public function supportsTransformation($data, string $to, array $context = []): bool + { + return WebResponseInterface::class === $to && $data instanceof PersistableInterface; + } } diff --git a/src/Api/Dto/AttributeOutput.php b/src/Api/Dto/AttributeOutput.php new file mode 100644 index 00000000..4ed39205 --- /dev/null +++ b/src/Api/Dto/AttributeOutput.php @@ -0,0 +1,51 @@ + + */ + #[Groups(['attribute'])] + public array $documents = []; +} diff --git a/src/Api/Dto/AttributeValueOutput.php b/src/Api/Dto/AttributeValueOutput.php new file mode 100644 index 00000000..9c1457eb --- /dev/null +++ b/src/Api/Dto/AttributeValueOutput.php @@ -0,0 +1,41 @@ + + */ + #[Groups(['document_folders'])] + public array $folders = []; + /** + * @var array + */ + #[Groups(['document_display_sources'])] + #[MaxDepth(1)] + public array $altSources = []; + /** + * @var string|null + */ + #[Groups(['document', 'document_display'])] + public ?string $alt = null; +} diff --git a/src/Api/Dto/FolderOutput.php b/src/Api/Dto/FolderOutput.php new file mode 100644 index 00000000..cd152ed1 --- /dev/null +++ b/src/Api/Dto/FolderOutput.php @@ -0,0 +1,34 @@ + + */ + #[Groups(['tag', 'tag_base'])] + public array $documents = []; + /** + * @var Tag|null + */ + #[Groups(['tag', 'tag_base'])] + #[MaxDepth(1)] + public ?Tag $parent = null; + /** + * @Groups({"tag", "tag_base"}) + */ + public ?float $position = null; +} diff --git a/src/Api/Dto/TranslationOutput.php b/src/Api/Dto/TranslationOutput.php new file mode 100644 index 00000000..0abd4630 --- /dev/null +++ b/src/Api/Dto/TranslationOutput.php @@ -0,0 +1,34 @@ +resourceMetadataFactory = $resourceMetadataFactory; $this->requestStack = $requestStack; $this->defaultPublicationFieldName = $defaultPublicationFieldName; } @@ -58,10 +63,9 @@ public function applyToCollection( QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, - ?Operation $operation = null, - array $context = [] + string $operationName = null ): void { - if (!$this->supportsResult($resourceClass, $operation)) { + if (!$this->supportsResult($resourceClass, $operationName)) { return; } if (null === $request = $this->requestStack->getCurrentRequest()) { @@ -69,7 +73,7 @@ public function applyToCollection( } $aliases = $queryBuilder->getRootAliases(); $alias = reset($aliases); - $publicationFieldName = $this->getPublicationFieldName($operation); + $publicationFieldName = $this->getPublicationFieldName($request, $this->resourceMetadataFactory->create($resourceClass), $operationName); $publicationField = $alias . '.' . $publicationFieldName; $queryBuilder->select($publicationField) @@ -77,21 +81,17 @@ public function applyToCollection( ->orderBy($publicationField, 'DESC'); } - public function supportsResult(string $resourceClass, ?Operation $operation = null, array $context = []): bool + public function supportsResult(string $resourceClass, string $operationName = null): bool { if (null === $request = $this->requestStack->getCurrentRequest()) { return false; } - return $this->isArchiveEnabled($operation); + return $this->isArchiveEnabled($request, $this->resourceMetadataFactory->create($resourceClass), $operationName); } - public function getResult( - QueryBuilder $queryBuilder, - ?string $resourceClass = null, - ?Operation $operation = null, - array $context = [] - ): iterable { + public function getResult(QueryBuilder $queryBuilder): iterable + { $entities = []; $dates = []; $paginator = new Paginator($queryBuilder, false); @@ -104,7 +104,7 @@ public function getResult( foreach ($paginator as $result) { $dateTimeField = reset($result); - if ($dateTimeField instanceof \DateTimeInterface) { + if ($dateTimeField instanceof \DateTime) { $year = $dateTimeField->format('Y'); $month = $dateTimeField->format('Y-m'); @@ -128,14 +128,28 @@ public function getResult( } private function isArchiveEnabled( - ?Operation $operation = null + Request $request, + ResourceMetadata $resourceMetadata, + string $operationName = null ): bool { - return $operation->getExtraProperties()['archive_enabled'] ?? false; + return $resourceMetadata->getCollectionOperationAttribute( + $operationName, + 'archive_enabled', + false, + true + ); } private function getPublicationFieldName( - ?Operation $operation = null + Request $request, + ResourceMetadata $resourceMetadata, + string $operationName = null ): string { - return $operation->getExtraProperties()['archive_publication_field_name'] ?? $this->defaultPublicationFieldName; + return $resourceMetadata->getCollectionOperationAttribute( + $operationName, + 'archive_publication_field_name', + $this->defaultPublicationFieldName, + true + ); } } diff --git a/src/Api/Extension/AttributeValueQueryExtension.php b/src/Api/Extension/AttributeValueQueryExtension.php deleted file mode 100644 index 3a095af7..00000000 --- a/src/Api/Extension/AttributeValueQueryExtension.php +++ /dev/null @@ -1,84 +0,0 @@ -previewResolver = $previewResolver; - } - - public function applyToItem( - QueryBuilder $queryBuilder, - QueryNameGeneratorInterface $queryNameGenerator, - string $resourceClass, - array $identifiers, - ?Operation $operation = null, - array $context = [] - ): void { - $this->apply($queryBuilder, $resourceClass); - } - - public function applyToCollection( - QueryBuilder $queryBuilder, - QueryNameGeneratorInterface $queryNameGenerator, - string $resourceClass, - ?Operation $operation = null, - array $context = [] - ): void { - $this->apply($queryBuilder, $resourceClass); - } - - private function apply( - QueryBuilder $queryBuilder, - string $resourceClass - ): void { - if ( - $resourceClass !== AttributeValue::class - ) { - return; - } - - $rootAlias = $queryBuilder->getRootAliases()[0]; - - /** - * AttributeValue is always linked to a Node. - * We need to join Node to filter by its status. - */ - $existingNodeJoin = QueryBuilderHelper::getExistingJoin($queryBuilder, 'o', 'node'); - if (null === $existingNodeJoin || !$existingNodeJoin->getAlias()) { - $queryBuilder->leftJoin($rootAlias . '.node', 'node'); - $joinAlias = 'node'; - } else { - $joinAlias = $existingNodeJoin->getAlias(); - } - - if ($this->previewResolver->isPreview()) { - $queryBuilder - ->andWhere($queryBuilder->expr()->lte($joinAlias . '.status', ':status')) - ->setParameter(':status', Node::PUBLISHED); - return; - } - - $queryBuilder - ->andWhere($queryBuilder->expr()->eq($joinAlias . '.status', ':status')) - ->setParameter(':status', Node::PUBLISHED); - return; - } -} diff --git a/src/Api/Extension/AttributeValueRealmExtension.php b/src/Api/Extension/AttributeValueRealmExtension.php deleted file mode 100644 index fb8f06c9..00000000 --- a/src/Api/Extension/AttributeValueRealmExtension.php +++ /dev/null @@ -1,69 +0,0 @@ -addWhere($queryBuilder, $resourceClass); - } - - public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, Operation $operation = null, array $context = []): void - { - $this->addWhere($queryBuilder, $resourceClass); - } - - private function addWhere(QueryBuilder $queryBuilder, string $resourceClass): void - { - if ($resourceClass !== AttributeValue::class || $this->security->isGranted('ROLE_ACCESS_NODE_ATTRIBUTES')) { - return; - } - - /* - * Filter out all attribute values requiring a realm for anonymous users. - */ - $rootAlias = $queryBuilder->getRootAliases()[0]; - if ($this->security->isGranted('IS_ANONYMOUS')) { - $queryBuilder->andWhere($queryBuilder->expr()->isNull(sprintf('%s.realm', $rootAlias))); - return; - } - - /* - * Filter all attribute values requiring a granted realm or no realm for current user. - */ - $queryBuilder->andWhere($queryBuilder->expr()->orX( - $queryBuilder->expr()->isNull(sprintf('%s.realm', $rootAlias)), - $queryBuilder->expr()->in( - sprintf('%s.realm', $rootAlias), - ':realmIds' - ) - ))->setParameter('realmIds', $this->getGrantedRealmIds()); - } - - private function getGrantedRealmIds(): array - { - return array_map( - fn (RealmInterface $realm) => $realm->getId(), - array_filter($this->realmResolver->getGrantedRealms()) - ); - } -} diff --git a/src/Api/Extension/DocumentQueryExtension.php b/src/Api/Extension/DocumentQueryExtension.php index 7d3894da..8b4b0611 100644 --- a/src/Api/Extension/DocumentQueryExtension.php +++ b/src/Api/Extension/DocumentQueryExtension.php @@ -4,10 +4,9 @@ namespace RZ\Roadiz\CoreBundle\Api\Extension; -use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface; -use ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface; -use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; -use ApiPlatform\Metadata\Operation; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; use Doctrine\ORM\QueryBuilder; use RZ\Roadiz\CoreBundle\Entity\Document; @@ -16,7 +15,8 @@ final class DocumentQueryExtension implements QueryItemExtensionInterface, Query private function apply( QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, - string $resourceClass + string $resourceClass, + string $operationName = null ): void { if ($resourceClass !== Document::class) { return; @@ -32,19 +32,18 @@ public function applyToItem( QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, - Operation $operation = null, + string $operationName = null, array $context = [] ): void { - $this->apply($queryBuilder, $queryNameGenerator, $resourceClass); + $this->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName); } public function applyToCollection( QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, - Operation $operation = null, - array $context = [] + string $operationName = null ): void { - $this->apply($queryBuilder, $queryNameGenerator, $resourceClass); + $this->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName); } } diff --git a/src/Api/Extension/NodeQueryExtension.php b/src/Api/Extension/NodeQueryExtension.php index 0b15784e..7b259f71 100644 --- a/src/Api/Extension/NodeQueryExtension.php +++ b/src/Api/Extension/NodeQueryExtension.php @@ -4,11 +4,10 @@ namespace RZ\Roadiz\CoreBundle\Api\Extension; -use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface; -use ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface; -use ApiPlatform\Doctrine\Orm\Util\QueryBuilderHelper; -use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; -use ApiPlatform\Metadata\Operation; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryBuilderHelper; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; use RZ\Roadiz\CoreBundle\Entity\Node; @@ -29,16 +28,17 @@ public function applyToItem( QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, - Operation $operation = null, + string $operationName = null, array $context = [] ): void { - $this->apply($queryBuilder, $queryNameGenerator, $resourceClass); + $this->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName); } private function apply( QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, - string $resourceClass + string $resourceClass, + string $operationName = null ): void { if ($resourceClass !== Node::class) { return; @@ -70,9 +70,8 @@ public function applyToCollection( QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, - Operation $operation = null, - array $context = [] + string $operationName = null ): void { - $this->apply($queryBuilder, $queryNameGenerator, $resourceClass); + $this->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName); } } diff --git a/src/Api/Extension/NodesSourcesQueryExtension.php b/src/Api/Extension/NodesSourcesQueryExtension.php index 2e5b3960..2239df4f 100644 --- a/src/Api/Extension/NodesSourcesQueryExtension.php +++ b/src/Api/Extension/NodesSourcesQueryExtension.php @@ -4,11 +4,10 @@ namespace RZ\Roadiz\CoreBundle\Api\Extension; -use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface; -use ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface; -use ApiPlatform\Doctrine\Orm\Util\QueryBuilderHelper; -use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; -use ApiPlatform\Metadata\Operation; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryBuilderHelper; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; use RZ\Roadiz\CoreBundle\Entity\Node; @@ -33,26 +32,26 @@ public function applyToItem( QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, - ?Operation $operation = null, + string $operationName = null, array $context = [] ): void { - $this->apply($queryBuilder, $queryNameGenerator, $resourceClass); + $this->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName); } public function applyToCollection( QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, - ?Operation $operation = null, - array $context = [] + string $operationName = null ): void { - $this->apply($queryBuilder, $queryNameGenerator, $resourceClass); + $this->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName); } private function apply( QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, - string $resourceClass + string $resourceClass, + string $operationName = null ): void { if ( $resourceClass !== NodesSources::class && diff --git a/src/Api/Extension/NodesTagsQueryExtension.php b/src/Api/Extension/NodesTagsQueryExtension.php deleted file mode 100644 index a5ea3cb3..00000000 --- a/src/Api/Extension/NodesTagsQueryExtension.php +++ /dev/null @@ -1,89 +0,0 @@ -previewResolver = $previewResolver; - } - - public function applyToItem( - QueryBuilder $queryBuilder, - QueryNameGeneratorInterface $queryNameGenerator, - string $resourceClass, - array $identifiers, - ?Operation $operation = null, - array $context = [] - ): void { - $this->apply($queryBuilder, $resourceClass); - } - - public function applyToCollection( - QueryBuilder $queryBuilder, - QueryNameGeneratorInterface $queryNameGenerator, - string $resourceClass, - ?Operation $operation = null, - array $context = [] - ): void { - $this->apply($queryBuilder, $resourceClass); - } - - private function apply( - QueryBuilder $queryBuilder, - string $resourceClass - ): void { - if ( - $resourceClass !== Tag::class - ) { - return; - } - - $parts = $queryBuilder->getDQLPart('join'); - $rootAlias = $queryBuilder->getRootAliases()[0]; - if (!\is_array($parts) || !isset($parts[$rootAlias])) { - return; - } - - $existingJoin = QueryBuilderHelper::getExistingJoin($queryBuilder, 'o', 'nodesTags'); - if (null === $existingJoin || !$existingJoin->getAlias()) { - return; - } - $existingNodeJoin = QueryBuilderHelper::getExistingJoin( - $queryBuilder, - $existingJoin->getAlias(), - 'node' - ); - if (null === $existingNodeJoin || !$existingNodeJoin->getAlias()) { - return; - } - - if ($this->previewResolver->isPreview()) { - $queryBuilder - ->andWhere($queryBuilder->expr()->lte($existingNodeJoin->getAlias() . '.status', ':status')) - ->setParameter(':status', Node::PUBLISHED); - return; - } - - $queryBuilder - ->andWhere($queryBuilder->expr()->eq($existingNodeJoin->getAlias() . '.status', ':status')) - ->setParameter(':status', Node::PUBLISHED); - return; - } -} diff --git a/src/Api/Filter/ArchiveFilter.php b/src/Api/Filter/ArchiveFilter.php index 7b58e2f5..a7cf3fc6 100644 --- a/src/Api/Filter/ArchiveFilter.php +++ b/src/Api/Filter/ArchiveFilter.php @@ -4,16 +4,14 @@ namespace RZ\Roadiz\CoreBundle\Api\Filter; -use ApiPlatform\Doctrine\Common\PropertyHelperTrait; -use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter; -use ApiPlatform\Doctrine\Orm\Filter\DateFilter; -use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Core\Bridge\Doctrine\Common\PropertyHelperTrait; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\DateFilter; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; use ApiPlatform\Exception\FilterValidationException; -use ApiPlatform\Metadata\Operation; -use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; -final class ArchiveFilter extends AbstractFilter +final class ArchiveFilter extends AbstractContextAwareFilter { use PropertyHelperTrait; @@ -39,19 +37,18 @@ protected function isDateField(string $property, string $resourceClass): bool */ protected function filterProperty( string $property, - mixed $value, + $values, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, - ?Operation $operation = null, - array $context = [] + string $operationName = null ): void { // Expect $values to be an array having the period as keys and the date value as values if ( !$this->isPropertyEnabled($property, $resourceClass) || !$this->isPropertyMapped($property, $resourceClass) || !$this->isDateField($property, $resourceClass) || - !isset($value[self::PARAMETER_ARCHIVE]) + !isset($values[self::PARAMETER_ARCHIVE]) ) { return; } @@ -60,24 +57,17 @@ protected function filterProperty( $field = $property; if ($this->isPropertyNested($property, $resourceClass)) { - [$alias, $field] = $this->addJoinsForNestedProperty( - $property, - $alias, - $queryBuilder, - $queryNameGenerator, - $resourceClass, - Join::INNER_JOIN - ); + [$alias, $field] = $this->addJoinsForNestedProperty($property, $alias, $queryBuilder, $queryNameGenerator, $resourceClass); } - if (!is_string($value[self::PARAMETER_ARCHIVE])) { + if (!is_string($values[self::PARAMETER_ARCHIVE])) { throw new FilterValidationException([sprintf( '“%s” filter must be only used with a string value.', self::PARAMETER_ARCHIVE )]); } - $range = $this->normalizeFilteringDates($value[self::PARAMETER_ARCHIVE]); + $range = $this->normalizeFilteringDates($values[self::PARAMETER_ARCHIVE]); if (null === $range || count($range) !== 2) { return; diff --git a/src/Api/Filter/CopyrightValidFilter.php b/src/Api/Filter/CopyrightValidFilter.php index ea8f56f2..513ba4fd 100644 --- a/src/Api/Filter/CopyrightValidFilter.php +++ b/src/Api/Filter/CopyrightValidFilter.php @@ -4,13 +4,12 @@ namespace RZ\Roadiz\CoreBundle\Api\Filter; -use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter; -use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; -use ApiPlatform\Metadata\Operation; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; use Doctrine\ORM\QueryBuilder; use RZ\Roadiz\CoreBundle\Entity\Document; -final class CopyrightValidFilter extends AbstractFilter +final class CopyrightValidFilter extends AbstractContextAwareFilter { public const PARAMETER = 'copyrightValid'; public const TRUE_VALUES = [1, '1', 'true', true, 'on', 'yes']; @@ -18,12 +17,11 @@ final class CopyrightValidFilter extends AbstractFilter protected function filterProperty( string $property, - mixed $value, + $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, - ?Operation $operation = null, - array $context = [] + string $operationName = null ): void { if ($property !== self::PARAMETER) { return; diff --git a/src/Api/Filter/GeneratedEntityFilter.php b/src/Api/Filter/GeneratedEntityFilter.php index 255be66d..d34cb712 100644 --- a/src/Api/Filter/GeneratedEntityFilter.php +++ b/src/Api/Filter/GeneratedEntityFilter.php @@ -4,30 +4,30 @@ namespace RZ\Roadiz\CoreBundle\Api\Filter; -use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter; use Doctrine\Persistence\ManagerRegistry; use Psr\Log\LoggerInterface; -use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\HttpFoundation\RequestStack; -abstract class GeneratedEntityFilter extends AbstractFilter +abstract class GeneratedEntityFilter extends AbstractContextAwareFilter { private string $generatedEntityNamespacePattern; /** * @param ManagerRegistry $managerRegistry + * @param RequestStack|null $requestStack + * @param string $generatedEntityNamespacePattern * @param LoggerInterface|null $logger * @param array|null $properties - * @param NameConverterInterface|null $nameConverter - * @param string $generatedEntityNamespacePattern */ public function __construct( ManagerRegistry $managerRegistry, - LoggerInterface $logger = null, - array $properties = null, - NameConverterInterface $nameConverter = null, + ?RequestStack $requestStack = null, string $generatedEntityNamespacePattern = '#^App\\\GeneratedEntity\\\NS(?:[a-zA-Z]+)$#', + LoggerInterface $logger = null, + array $properties = null ) { - parent::__construct($managerRegistry, $logger, $properties, $nameConverter); + parent::__construct($managerRegistry, $requestStack, $logger, $properties); $this->generatedEntityNamespacePattern = $generatedEntityNamespacePattern; } diff --git a/src/Api/Filter/IntersectionFilter.php b/src/Api/Filter/IntersectionFilter.php index 699e1edc..10fb2d06 100644 --- a/src/Api/Filter/IntersectionFilter.php +++ b/src/Api/Filter/IntersectionFilter.php @@ -4,17 +4,16 @@ namespace RZ\Roadiz\CoreBundle\Api\Filter; -use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; -use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; use ApiPlatform\Exception\FilterValidationException; -use ApiPlatform\Metadata\Operation; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; /** * Intersection filter must be used AFTER SearchFilter if you want to combine both. */ -final class IntersectionFilter extends AbstractFilter +final class IntersectionFilter extends AbstractContextAwareFilter { public const PARAMETER = 'intersect'; @@ -23,12 +22,11 @@ final class IntersectionFilter extends AbstractFilter */ protected function filterProperty( string $property, - mixed $value, + $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, - ?Operation $operation = null, - array $context = [] + string $operationName = null ): void { if ($property !== IntersectionFilter::PARAMETER || !is_array($value)) { return; diff --git a/src/Api/Filter/LocaleFilter.php b/src/Api/Filter/LocaleFilter.php index a4579ec5..95da674d 100644 --- a/src/Api/Filter/LocaleFilter.php +++ b/src/Api/Filter/LocaleFilter.php @@ -4,16 +4,15 @@ namespace RZ\Roadiz\CoreBundle\Api\Filter; -use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; use ApiPlatform\Exception\FilterValidationException; -use ApiPlatform\Metadata\Operation; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; use Psr\Log\LoggerInterface; use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\Translation; use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; -use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\HttpFoundation\RequestStack; final class LocaleFilter extends GeneratedEntityFilter { @@ -24,23 +23,34 @@ final class LocaleFilter extends GeneratedEntityFilter public function __construct( PreviewResolverInterface $previewResolver, ManagerRegistry $managerRegistry, + ?RequestStack $requestStack = null, + string $generatedEntityNamespacePattern = '#^App\\\GeneratedEntity\\\NS(?:[a-zA-Z]+)$#', LoggerInterface $logger = null, - array $properties = null, - NameConverterInterface $nameConverter = null, - string $generatedEntityNamespacePattern = '#^App\\\GeneratedEntity\\\NS(?:[a-zA-Z]+)$#' + array $properties = null ) { - parent::__construct($managerRegistry, $logger, $properties, $nameConverter, $generatedEntityNamespacePattern); + parent::__construct($managerRegistry, $requestStack, $generatedEntityNamespacePattern, $logger, $properties); $this->previewResolver = $previewResolver; } + + /** + * Passes a property through the filter. + * + * @param string $property + * @param mixed $value + * @param QueryBuilder $queryBuilder + * @param QueryNameGeneratorInterface $queryNameGenerator + * @param string $resourceClass + * @param string|null $operationName + * @throws \Exception + */ protected function filterProperty( string $property, - mixed $value, + $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, - ?Operation $operation = null, - array $context = [] + string $operationName = null ): void { if ($property !== self::PROPERTY) { return; diff --git a/src/Api/Filter/NotFilter.php b/src/Api/Filter/NotFilter.php index b1677bd0..22e37e2a 100644 --- a/src/Api/Filter/NotFilter.php +++ b/src/Api/Filter/NotFilter.php @@ -4,25 +4,33 @@ namespace RZ\Roadiz\CoreBundle\Api\Filter; -use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; -use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter; -use ApiPlatform\Metadata\Operation; -use Doctrine\ORM\Query\Expr\Join; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; use Doctrine\ORM\QueryBuilder; use Symfony\Component\String\Slugger\AsciiSlugger; -final class NotFilter extends AbstractFilter +final class NotFilter extends AbstractContextAwareFilter { public const PARAMETER = 'not'; + /** + * Passes a property through the filter. + * + * @param string $property + * @param mixed $value + * @param QueryBuilder $queryBuilder + * @param QueryNameGeneratorInterface $queryNameGenerator + * @param string $resourceClass + * @param string|null $operationName + * @throws \Exception + */ protected function filterProperty( string $property, - mixed $value, + $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, - ?Operation $operation = null, - array $context = [] + string $operationName = null ): void { if ($property !== self::PARAMETER || !\is_array($value)) { return; @@ -33,14 +41,7 @@ protected function filterProperty( $field = $property; if ($this->isPropertyNested($property, $resourceClass)) { - list($alias, $field) = $this->addJoinsForNestedProperty( - $property, - $alias, - $queryBuilder, - $queryNameGenerator, - $resourceClass, - Join::INNER_JOIN - ); + list($alias, $field) = $this->addJoinsForNestedProperty($property, $alias, $queryBuilder, $queryNameGenerator); } $placeholder = ':' . (new AsciiSlugger())->slug($alias . '_' . $field, '_')->toString(); diff --git a/src/Api/ListManager/SolrPaginator.php b/src/Api/ListManager/SolrPaginator.php index 8eedf602..99d467be 100644 --- a/src/Api/ListManager/SolrPaginator.php +++ b/src/Api/ListManager/SolrPaginator.php @@ -62,10 +62,6 @@ public function getItemsPerPage(): float public function getIterator(): \Traversable { $this->handleOnce(); - $entities = $this->listManager->getEntities(); - if (\is_array($entities)) { - return new \ArrayIterator($entities); - } - return $entities->getIterator(); + return new ArrayCollection($this->listManager->getEntities()); } } diff --git a/src/Api/ListManager/SolrSearchListManager.php b/src/Api/ListManager/SolrSearchListManager.php index d4c6e75f..2185a575 100644 --- a/src/Api/ListManager/SolrSearchListManager.php +++ b/src/Api/ListManager/SolrSearchListManager.php @@ -15,7 +15,6 @@ final class SolrSearchListManager extends AbstractEntityListManager protected ?SearchResultsInterface $searchResults; private array $criteria; private bool $searchInTags; - private ?string $query = null; public function __construct( ?Request $request, @@ -35,40 +34,49 @@ public function handle(bool $disabled = false) throw new \InvalidArgumentException('Cannot handle a NULL request.'); } - $this->handleRequestQuery($disabled); + $query = trim($this->request->query->get('search') ?? ''); - if (null === $this->query) { - throw new \InvalidArgumentException('Cannot handle a NULL query.'); + if ( + $this->request->query->has('page') && + $this->request->query->get('page') > 1 + ) { + $this->setPage((int) $this->request->query->get('page')); + } else { + $this->setPage(1); + } + + if ( + $this->request->query->has('itemsPerPage') && + $this->request->query->get('itemsPerPage') > 0 + ) { + $this->setItemPerPage((int) $this->request->query->get('itemsPerPage')); } + /* * Query must be longer than 3 chars or Solr might crash * on highlighting fields. */ - if (\mb_strlen($this->query) > 3) { + if (\mb_strlen($query) > 3) { $this->searchResults = $this->searchHandler->searchWithHighlight( - $this->query, # Use ?q query parameter to search with + $query, # Use ?q query parameter to search with $this->criteria, # a simple criteria array to filter search results $this->getItemPerPage(), # result count $this->searchInTags, # Search in tags too, + 1, $this->getPage() ); } else { $this->searchResults = $this->searchHandler->search( - $this->query, # Use ?q query parameter to search with + $query, # Use ?q query parameter to search with $this->criteria, # a simple criteria array to filter search results $this->getItemPerPage(), # result count $this->searchInTags, # Search in tags too, + 2, $this->getPage() ); } } - protected function handleSearchParam(string $search): void - { - parent::handleSearchParam($search); - $this->query = trim($search); - } - /** * @inheritDoc */ @@ -83,7 +91,7 @@ public function getItemCount(): int /** * @inheritDoc */ - public function getEntities(): array + public function getEntities() { if (null !== $this->searchResults) { return $this->searchResults->getResultItems(); diff --git a/src/Api/Model/BlocksAwareWebResponseInterface.php b/src/Api/Model/BlocksAwareWebResponseInterface.php index bdf356fe..e94917f7 100644 --- a/src/Api/Model/BlocksAwareWebResponseInterface.php +++ b/src/Api/Model/BlocksAwareWebResponseInterface.php @@ -10,13 +10,13 @@ interface BlocksAwareWebResponseInterface extends WebResponseInterface { /** - * @return WalkerInterface[]|null + * @return Collection|null */ - public function getBlocks(): ?array; + public function getBlocks(): ?Collection; /** * @param Collection|null $blocks - * @return $this + * @return BlocksAwareWebResponseInterface */ - public function setBlocks(?Collection $blocks): self; + public function setBlocks(?Collection $blocks): BlocksAwareWebResponseInterface; } diff --git a/src/Api/Model/NodesSourcesHead.php b/src/Api/Model/NodesSourcesHead.php index c0f52e2b..ef94ded5 100644 --- a/src/Api/Model/NodesSourcesHead.php +++ b/src/Api/Model/NodesSourcesHead.php @@ -113,6 +113,7 @@ public function getMatomoSiteId(): ?string #[Serializer\Groups(["web_response", "nodes_sources_single", "walker"])] public function getSiteName(): ?string { + // site_name return $this->settingsBag->get('site_name', null) ?? null; } @@ -172,6 +173,9 @@ public function isNoIndex(): bool return false; } + /** + * @return string|null + */ #[Serializer\Groups(["web_response", "nodes_sources_single", "walker"])] public function getPolicyUrl(): ?string { @@ -195,60 +199,63 @@ public function getPolicyUrl(): ?string return null; } + /** + * @return string|null + */ #[Serializer\Groups(["web_response", "nodes_sources_single", "walker"])] public function getMainColor(): ?string { return $this->settingsBag->get('main_color', null) ?? null; } + /** + * @return string|null + */ #[Serializer\Groups(["web_response", "nodes_sources_single", "walker"])] public function getFacebookUrl(): ?string { return $this->settingsBag->get('facebook_url', null) ?? null; } + /** + * @return string|null + */ #[Serializer\Groups(["web_response", "nodes_sources_single", "walker"])] public function getInstagramUrl(): ?string { return $this->settingsBag->get('instagram_url', null) ?? null; } + /** + * @return string|null + */ #[Serializer\Groups(["web_response", "nodes_sources_single", "walker"])] public function getTwitterUrl(): ?string { return $this->settingsBag->get('twitter_url', null) ?? null; } + /** + * @return string|null + */ #[Serializer\Groups(["web_response", "nodes_sources_single", "walker"])] public function getYoutubeUrl(): ?string { return $this->settingsBag->get('youtube_url', null) ?? null; } + /** + * @return string|null + */ #[Serializer\Groups(["nodes_sources_single", "walker"])] public function getLinkedinUrl(): ?string { return $this->settingsBag->get('linkedin_url', null) ?? null; } - #[Serializer\Groups(["nodes_sources_single", "walker"])] - public function getSpotifyUrl(): ?string - { - return $this->settingsBag->get('spotify_url', null) ?? null; - } - - #[Serializer\Groups(["nodes_sources_single", "walker"])] - public function getSoundcloudUrl(): ?string - { - return $this->settingsBag->get('soundcloud_url', null) ?? null; - } - - #[Serializer\Groups(["nodes_sources_single", "walker"])] - public function getTikTokUrl(): ?string - { - return $this->settingsBag->get('tiktok_url', null) ?? null; - } - + /** + * @return string|null + */ #[Serializer\Groups(["web_response", "nodes_sources_single", "walker"])] public function getHomePageUrl(): ?string { @@ -261,6 +268,9 @@ public function getHomePageUrl(): ?string return null; } + /** + * @return DocumentInterface|null + */ #[Serializer\Groups(["web_response", "nodes_sources_single"])] public function getShareImage(): ?DocumentInterface { @@ -281,6 +291,9 @@ public function getShareImage(): ?DocumentInterface return $this->settingsBag->getDocument('share_image') ?? null; } + /** + * @return TranslationInterface + */ #[Serializer\Ignore()] public function getTranslation(): TranslationInterface { @@ -290,6 +303,9 @@ public function getTranslation(): TranslationInterface return $this->defaultTranslation; } + /** + * @return NodesSources|null + */ #[Serializer\Ignore()] public function getHomePage(): ?NodesSources { diff --git a/src/Api/Model/NodesSourcesHeadFactory.php b/src/Api/Model/NodesSourcesHeadFactory.php index 9cae9098..d84fc468 100644 --- a/src/Api/Model/NodesSourcesHeadFactory.php +++ b/src/Api/Model/NodesSourcesHeadFactory.php @@ -13,12 +13,27 @@ final class NodesSourcesHeadFactory implements NodesSourcesHeadFactoryInterface { + private Settings $settingsBag; + private UrlGeneratorInterface $urlGenerator; + private NodeSourceApi $nodeSourceApi; + private HandlerFactoryInterface $handlerFactory; + + /** + * @param Settings $settingsBag + * @param UrlGeneratorInterface $urlGenerator + * @param NodeSourceApi $nodeSourceApi + * @param HandlerFactoryInterface $handlerFactory + */ public function __construct( - private readonly Settings $settingsBag, - private readonly UrlGeneratorInterface $urlGenerator, - private readonly NodeSourceApi $nodeSourceApi, - private readonly HandlerFactoryInterface $handlerFactory + Settings $settingsBag, + UrlGeneratorInterface $urlGenerator, + NodeSourceApi $nodeSourceApi, + HandlerFactoryInterface $handlerFactory ) { + $this->settingsBag = $settingsBag; + $this->urlGenerator = $urlGenerator; + $this->nodeSourceApi = $nodeSourceApi; + $this->handlerFactory = $handlerFactory; } public function createForNodeSource(NodesSources $nodesSources): NodesSourcesHeadInterface diff --git a/src/Api/Model/RealmsAwareWebResponseInterface.php b/src/Api/Model/RealmsAwareWebResponseInterface.php index 3962bfe0..92e78e05 100644 --- a/src/Api/Model/RealmsAwareWebResponseInterface.php +++ b/src/Api/Model/RealmsAwareWebResponseInterface.php @@ -15,9 +15,9 @@ public function getRealms(): ?array; /** * @param RealmInterface[]|null $realms - * @return $this + * @return RealmsAwareWebResponseInterface */ - public function setRealms(?array $realms): self; + public function setRealms(?array $realms): RealmsAwareWebResponseInterface; /** * @return bool @@ -26,7 +26,7 @@ public function isHidingBlocks(): bool; /** * @param bool $hidingBlocks - * @return $this + * @return RealmsAwareWebResponseInterface */ - public function setHidingBlocks(bool $hidingBlocks): self; + public function setHidingBlocks(bool $hidingBlocks): RealmsAwareWebResponseInterface; } diff --git a/src/Api/Model/WebResponse.php b/src/Api/Model/WebResponse.php index d8558dce..2cf0b7e4 100644 --- a/src/Api/Model/WebResponse.php +++ b/src/Api/Model/WebResponse.php @@ -4,7 +4,100 @@ namespace RZ\Roadiz\CoreBundle\Api\Model; +use ApiPlatform\Core\Annotation\ApiProperty; +use Doctrine\Common\Collections\Collection; +use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; +use RZ\Roadiz\CoreBundle\Api\Breadcrumbs\BreadcrumbsInterface; +use RZ\Roadiz\CoreBundle\Model\RealmInterface; +use RZ\TreeWalker\WalkerInterface; +use Symfony\Component\Serializer\Annotation as Serializer; + final class WebResponse implements WebResponseInterface, BlocksAwareWebResponseInterface, RealmsAwareWebResponseInterface { - use WebResponseTrait; + #[ApiProperty(identifier: true)] + public ?string $path = null; + + #[Serializer\Groups(["web_response"])] + public ?PersistableInterface $item = null; + + #[Serializer\Groups(["web_response"])] + public ?BreadcrumbsInterface $breadcrumbs = null; + + #[Serializer\Groups(["web_response"])] + public ?NodesSourcesHeadInterface $head = null; + /** + * @var Collection|null + */ + #[Serializer\Groups(["web_response"])] + private ?Collection $blocks = null; + /** + * @var array|null + */ + #[Serializer\Groups(["web_response"])] + private ?array $realms = null; + + #[Serializer\Groups(["web_response"])] + private bool $hidingBlocks = false; + + /** + * @return PersistableInterface|null + */ + public function getItem(): ?PersistableInterface + { + return $this->item; + } + + /** + * @return Collection|null + */ + public function getBlocks(): ?Collection + { + return $this->blocks; + } + + /** + * @param Collection|null $blocks + * @return WebResponse + */ + public function setBlocks(?Collection $blocks): WebResponse + { + $this->blocks = $blocks; + return $this; + } + + /** + * @return RealmInterface[]|null + */ + public function getRealms(): ?array + { + return $this->realms; + } + + /** + * @param RealmInterface[]|null $realms + * @return WebResponse + */ + public function setRealms(?array $realms): WebResponse + { + $this->realms = $realms; + return $this; + } + + /** + * @return bool + */ + public function isHidingBlocks(): bool + { + return $this->hidingBlocks; + } + + /** + * @param bool $hidingBlocks + * @return WebResponse + */ + public function setHidingBlocks(bool $hidingBlocks): WebResponse + { + $this->hidingBlocks = $hidingBlocks; + return $this; + } } diff --git a/src/Api/Model/WebResponseInterface.php b/src/Api/Model/WebResponseInterface.php index 61ff1727..589b7983 100644 --- a/src/Api/Model/WebResponseInterface.php +++ b/src/Api/Model/WebResponseInterface.php @@ -5,26 +5,8 @@ namespace RZ\Roadiz\CoreBundle\Api\Model; use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; -use RZ\Roadiz\CoreBundle\Api\Breadcrumbs\BreadcrumbsInterface; -/** - * @template T of PersistableInterface - */ interface WebResponseInterface { - public function setHead(?NodesSourcesHeadInterface $head): self; - public function setBreadcrumbs(?BreadcrumbsInterface $breadcrumbs): self; - - /** - * @param T|null $item - * @return self - */ - public function setItem(?PersistableInterface $item): self; - public function setPath(?string $path): self; - /** - * @return T|null - */ public function getItem(): ?PersistableInterface; - public function getMaxAge(): ?int; - public function setMaxAge(?int $maxAge): self; } diff --git a/src/Api/Model/WebResponseTrait.php b/src/Api/Model/WebResponseTrait.php deleted file mode 100644 index 8acf455f..00000000 --- a/src/Api/Model/WebResponseTrait.php +++ /dev/null @@ -1,149 +0,0 @@ -|null - */ - #[Serializer\Groups(["web_response"])] - private ?Collection $blocks = null; - /** - * @var array|null - */ - #[Serializer\Groups(["web_response"])] - private ?array $realms = null; - - #[Serializer\Groups(["web_response"])] - private bool $hidingBlocks = false; - - /** - * @var int|null WebResponse item maximum age in seconds - */ - #[Serializer\Groups(["web_response"])] - private ?int $maxAge = null; - - public function getMaxAge(): ?int - { - return $this->maxAge; - } - - public function setMaxAge(?int $maxAge): self - { - $this->maxAge = $maxAge; - return $this; - } - - public function setPath(?string $path): self - { - $this->path = $path; - return $this; - } - - public function setItem(?PersistableInterface $item): self - { - $this->item = $item; - return $this; - } - - /** - * @return PersistableInterface|null - */ - public function getItem(): ?PersistableInterface - { - return $this->item; - } - - /** - * @return WalkerInterface[]|null - */ - public function getBlocks(): ?array - { - return $this->blocks?->getValues(); - } - - /** - * @param Collection|null $blocks - * @return $this - */ - public function setBlocks(?Collection $blocks): self - { - $this->blocks = $blocks; - return $this; - } - - /** - * @return RealmInterface[]|null - */ - public function getRealms(): ?array - { - return $this->realms; - } - - /** - * @param RealmInterface[]|null $realms - * @return $this - */ - public function setRealms(?array $realms): self - { - $this->realms = $realms; - return $this; - } - - /** - * @return bool - */ - public function isHidingBlocks(): bool - { - return $this->hidingBlocks; - } - - /** - * @param bool $hidingBlocks - * @return $this - */ - public function setHidingBlocks(bool $hidingBlocks): self - { - $this->hidingBlocks = $hidingBlocks; - return $this; - } - - public function setBreadcrumbs(?BreadcrumbsInterface $breadcrumbs): self - { - $this->breadcrumbs = $breadcrumbs; - return $this; - } - - public function setHead(?NodesSourcesHeadInterface $head): self - { - $this->head = $head; - return $this; - } -} diff --git a/src/Api/OpenApi/WebResponseDecorator.php b/src/Api/OpenApi/WebResponseDecorator.php index 21131a1e..da54e849 100644 --- a/src/Api/OpenApi/WebResponseDecorator.php +++ b/src/Api/OpenApi/WebResponseDecorator.php @@ -22,10 +22,6 @@ public function __invoke(array $context = []): OpenApi { $openApi = ($this->decorated)($context); $pathItem = $openApi->getPaths()->getPath('/api/web_response_by_path'); - if (null === $pathItem) { - return $openApi; - } - $operation = $pathItem->getGet(); $openApi->getPaths()->addPath('/api/web_response_by_path', $pathItem->withGet( diff --git a/src/Api/TreeWalker/Definition/DefinitionFactoryConfiguration.php b/src/Api/TreeWalker/Definition/DefinitionFactoryConfiguration.php deleted file mode 100644 index a63ca9c1..00000000 --- a/src/Api/TreeWalker/Definition/DefinitionFactoryConfiguration.php +++ /dev/null @@ -1,27 +0,0 @@ -classname = $classname; - $this->onlyVisible = $onlyVisible; - $this->definitionFactory = $definitionFactory; - } -} diff --git a/src/Api/TreeWalker/Definition/DefinitionFactoryInterface.php b/src/Api/TreeWalker/Definition/DefinitionFactoryInterface.php deleted file mode 100644 index 85e75536..00000000 --- a/src/Api/TreeWalker/Definition/DefinitionFactoryInterface.php +++ /dev/null @@ -1,12 +0,0 @@ -getArrayCopy(); } - // @phpstan-ignore-next-line - return iterator_to_array($iterator); } return $children; } diff --git a/src/Api/TreeWalker/Definition/ReachableNodeSourceDefinition.php b/src/Api/TreeWalker/Definition/ReachableNodeSourceDefinition.php index 22fbc6ac..4ad2f99b 100644 --- a/src/Api/TreeWalker/Definition/ReachableNodeSourceDefinition.php +++ b/src/Api/TreeWalker/Definition/ReachableNodeSourceDefinition.php @@ -47,8 +47,6 @@ public function __invoke(NodesSources $source): array if ($iterator instanceof ArrayIterator) { return $iterator->getArrayCopy(); } - // @phpstan-ignore-next-line - return iterator_to_array($iterator); } return $children; } diff --git a/src/Api/TreeWalker/TreeWalkerGenerator.php b/src/Api/TreeWalker/TreeWalkerGenerator.php index 7a8bff01..1e357445 100644 --- a/src/Api/TreeWalker/TreeWalkerGenerator.php +++ b/src/Api/TreeWalker/TreeWalkerGenerator.php @@ -6,12 +6,9 @@ use Psr\Cache\CacheItemPoolInterface; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; -use RZ\Roadiz\CoreBundle\Api\TreeWalker\Definition\DefinitionFactoryConfiguration; -use RZ\Roadiz\CoreBundle\Api\TreeWalker\Definition\DefinitionFactoryInterface; use RZ\Roadiz\CoreBundle\Bag\NodeTypes; use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\EntityApi\NodeSourceApi; -use RZ\TreeWalker\AbstractWalker; use RZ\TreeWalker\WalkerContextInterface; use RZ\TreeWalker\WalkerInterface; use Symfony\Component\String\UnicodeString; @@ -23,11 +20,6 @@ final class TreeWalkerGenerator private WalkerContextInterface $walkerContext; private CacheItemPoolInterface $cacheItemPool; - /** - * @var array - */ - private array $walkerDefinitionFactories = []; - public function __construct( NodeSourceApi $nodeSourceApi, NodeTypes $nodeTypesBag, @@ -42,7 +34,7 @@ public function __construct( /** * @param string $nodeType - * @param class-string $walkerClass + * @param class-string $walkerClass * @param TranslationInterface $translation * @param int $maxLevel * @return array @@ -62,14 +54,14 @@ public function getTreeWalkersForTypeAtRoot( ]); foreach ($roots as $root) { - $walkerName = (new UnicodeString($root->getNode()->getNodeName() . ' walker')) + $walkerName = (new UnicodeString($root->getNode()?->getNodeName() . ' walker')) ->trim() ->camel() ->toString(); - $walkers[$walkerName] = $this->buildForRoot( + $walkers[$walkerName] = call_user_func( + [$walkerClass, 'build'], $root, - $walkerClass, $this->walkerContext, $maxLevel, $this->cacheItemPool @@ -78,62 +70,4 @@ public function getTreeWalkersForTypeAtRoot( return $walkers; } - - /** - * @param object $root - * @param class-string $walkerClass - * @param WalkerContextInterface $walkerContext - * @param int $maxLevel - * @param CacheItemPoolInterface $cacheItemPool - * @return WalkerInterface - */ - public function buildForRoot( - object $root, - string $walkerClass, - WalkerContextInterface $walkerContext, - int $maxLevel, - CacheItemPoolInterface $cacheItemPool - ): WalkerInterface { - /** @var callable $callable */ - $callable = [$walkerClass, 'build']; - $walker = call_user_func( - $callable, - $root, - $walkerContext, - $maxLevel, - $cacheItemPool - ); - - foreach ($this->walkerDefinitionFactories as $definitionFactoryConfiguration) { - $walker->addDefinition( - $definitionFactoryConfiguration->classname, - $definitionFactoryConfiguration->definitionFactory->create( - $this->walkerContext, - $definitionFactoryConfiguration->onlyVisible - ) - ); - } - return $walker; - } - - /** - * Inject definition from factories registered in the container - * using `roadiz_core.tree_walker_definition_factory` tag. - * - * @param class-string $classname - * @param DefinitionFactoryInterface $definitionFactory - * @param bool $onlyVisible - * @return void - */ - public function addDefinitionFactoryConfiguration( - string $classname, - DefinitionFactoryInterface $definitionFactory, - bool $onlyVisible - ): void { - $this->walkerDefinitionFactories[$classname] = new DefinitionFactoryConfiguration( - $classname, - $definitionFactory, - $onlyVisible - ); - } } diff --git a/src/Bag/NodeTypes.php b/src/Bag/NodeTypes.php index fbaf9dd5..44147c4a 100644 --- a/src/Bag/NodeTypes.php +++ b/src/Bag/NodeTypes.php @@ -10,15 +10,23 @@ use RZ\Roadiz\CoreBundle\Entity\NodeType; use RZ\Roadiz\CoreBundle\Repository\NodeTypeRepository; -final class NodeTypes extends LazyParameterBag implements NodeTypeResolverInterface +class NodeTypes extends LazyParameterBag implements NodeTypeResolverInterface { private ?NodeTypeRepository $repository = null; + private ManagerRegistry $managerRegistry; - public function __construct(private readonly ManagerRegistry $managerRegistry) + /** + * @param ManagerRegistry $managerRegistry + */ + public function __construct(ManagerRegistry $managerRegistry) { parent::__construct(); + $this->managerRegistry = $managerRegistry; } + /** + * @return NodeTypeRepository + */ public function getRepository(): NodeTypeRepository { if (null === $this->repository) { diff --git a/src/Bag/Roles.php b/src/Bag/Roles.php index e98d7e14..4d13624c 100644 --- a/src/Bag/Roles.php +++ b/src/Bag/Roles.php @@ -9,15 +9,23 @@ use RZ\Roadiz\CoreBundle\Entity\Role; use RZ\Roadiz\CoreBundle\Repository\RoleRepository; -final class Roles extends LazyParameterBag +class Roles extends LazyParameterBag { + private ManagerRegistry $managerRegistry; private ?RoleRepository $repository = null; - public function __construct(private readonly ManagerRegistry $managerRegistry) + /** + * @param ManagerRegistry $managerRegistry; + */ + public function __construct(ManagerRegistry $managerRegistry) { parent::__construct(); + $this->managerRegistry = $managerRegistry; } + /** + * @return RoleRepository + */ public function getRepository(): RoleRepository { if (null === $this->repository) { @@ -49,15 +57,14 @@ protected function populateParameters(): void * * @return Role */ - public function get(string $key, $default = null): Role + public function get($key, $default = null): Role { $role = parent::get($key, $default); if (null === $role) { $role = new Role($key); - $roleManager = $this->managerRegistry->getManagerForClass(Role::class); - $roleManager->persist($role); - $roleManager->flush(); + $this->managerRegistry->getManagerForClass(Role::class)->persist($role); + $this->managerRegistry->getManagerForClass(Role::class)->flush(); } return $role; diff --git a/src/Bag/Settings.php b/src/Bag/Settings.php index 4760721b..128d9b89 100644 --- a/src/Bag/Settings.php +++ b/src/Bag/Settings.php @@ -9,19 +9,24 @@ use RZ\Roadiz\CoreBundle\Entity\Document; use RZ\Roadiz\CoreBundle\Entity\Setting; use RZ\Roadiz\CoreBundle\Repository\SettingRepository; -use Symfony\Component\Stopwatch\Stopwatch; class Settings extends LazyParameterBag { + private ManagerRegistry $managerRegistry; private ?SettingRepository $repository = null; - public function __construct( - private readonly ManagerRegistry $managerRegistry, - private readonly Stopwatch $stopwatch - ) { + /** + * @param ManagerRegistry $managerRegistry + */ + public function __construct(ManagerRegistry $managerRegistry) + { parent::__construct(); + $this->managerRegistry = $managerRegistry; } + /** + * @return SettingRepository + */ public function getRepository(): SettingRepository { if (null === $this->repository) { @@ -32,7 +37,6 @@ public function getRepository(): SettingRepository protected function populateParameters(): void { - $this->stopwatch->start('settings'); try { $settings = $this->getRepository()->findAll(); $this->parameters = []; @@ -44,15 +48,14 @@ protected function populateParameters(): void $this->parameters = []; } $this->ready = true; - $this->stopwatch->stop('settings'); } /** * @param string $key * @param mixed $default - * @return mixed + * @return bool|mixed */ - public function get(string $key, $default = false): mixed + public function get($key, $default = false) { return parent::get($key, $default); } @@ -63,7 +66,7 @@ public function get(string $key, $default = false): mixed * @param string $key * @return Document|null */ - public function getDocument(string $key): ?Document + public function getDocument($key): ?Document { try { $id = $this->getInt($key); diff --git a/src/Cache/Clearer/FileClearer.php b/src/Cache/Clearer/FileClearer.php index 955e348f..0cb90568 100644 --- a/src/Cache/Clearer/FileClearer.php +++ b/src/Cache/Clearer/FileClearer.php @@ -7,9 +7,11 @@ abstract class FileClearer implements ClearerInterface { protected ?string $output = null; + protected string $cacheDir; - public function __construct(protected readonly string $cacheDir) + public function __construct(string $cacheDir) { + $this->cacheDir = $cacheDir; } public function clear(): bool diff --git a/src/Cache/Clearer/NodesSourcesUrlsCacheClearer.php b/src/Cache/Clearer/NodesSourcesUrlsCacheClearer.php index a7018fe0..f59f919b 100644 --- a/src/Cache/Clearer/NodesSourcesUrlsCacheClearer.php +++ b/src/Cache/Clearer/NodesSourcesUrlsCacheClearer.php @@ -8,9 +8,12 @@ final class NodesSourcesUrlsCacheClearer extends FileClearer { - public function __construct(private readonly CacheItemPoolInterface $cacheProvider) + private CacheItemPoolInterface $cacheProvider; + + public function __construct(CacheItemPoolInterface $cacheProvider) { parent::__construct(''); + $this->cacheProvider = $cacheProvider; } public function clear(): bool diff --git a/src/Cache/CloudflareProxyCache.php b/src/Cache/CloudflareProxyCache.php index 93485e54..15e63174 100644 --- a/src/Cache/CloudflareProxyCache.php +++ b/src/Cache/CloudflareProxyCache.php @@ -6,15 +6,32 @@ final class CloudflareProxyCache { - public function __construct( - private readonly string $name, - private readonly string $zone, - private readonly string $version, - private readonly string $bearer, - private readonly string $email, - private readonly string $key, - private readonly int $timeout - ) { + protected string $name; + protected string $zone; + protected string $version; + protected string $bearer; + protected string $email; + protected string $key; + protected int $timeout; + + /** + * @param string $name + * @param string $zone + * @param string $version + * @param string $bearer + * @param string $email + * @param string $key + * @param int $timeout + */ + public function __construct(string $name, string $zone, string $version, string $bearer, string $email, string $key, int $timeout) + { + $this->name = $name; + $this->zone = $zone; + $this->version = $version; + $this->bearer = $bearer; + $this->email = $email; + $this->key = $key; + $this->timeout = $timeout; } /** diff --git a/src/Cache/ReverseProxyCache.php b/src/Cache/ReverseProxyCache.php index 8aa36611..80900a63 100644 --- a/src/Cache/ReverseProxyCache.php +++ b/src/Cache/ReverseProxyCache.php @@ -6,12 +6,23 @@ final class ReverseProxyCache { - public function __construct( - private readonly string $name, - private readonly string $host, - private readonly string $domainName, - private readonly int $timeout - ) { + protected string $name; + protected string $host; + protected string $domainName; + protected int $timeout; + + /** + * @param string $name + * @param string $host + * @param string $domainName + * @param int $timeout + */ + public function __construct(string $name, string $host, string $domainName, int $timeout) + { + $this->name = $name; + $this->host = $host; + $this->domainName = $domainName; + $this->timeout = $timeout; } /** @@ -37,9 +48,4 @@ public function getDomainName(): string { return $this->domainName; } - - public function getTimeout(): int - { - return $this->timeout; - } } diff --git a/src/Cache/ReverseProxyCacheLocator.php b/src/Cache/ReverseProxyCacheLocator.php index b3f4b963..b6a4a3f6 100644 --- a/src/Cache/ReverseProxyCacheLocator.php +++ b/src/Cache/ReverseProxyCacheLocator.php @@ -6,14 +6,20 @@ final class ReverseProxyCacheLocator { + /** + * @var ReverseProxyCache[] + */ + private array $frontends; + private ?CloudflareProxyCache $cloudflareProxyCache; + /** * @param ReverseProxyCache[] $frontends * @param CloudflareProxyCache|null $cloudflareProxyCache */ - public function __construct( - private readonly array $frontends, - private readonly ?CloudflareProxyCache $cloudflareProxyCache = null - ) { + public function __construct(array $frontends, ?CloudflareProxyCache $cloudflareProxyCache = null) + { + $this->frontends = $frontends; + $this->cloudflareProxyCache = $cloudflareProxyCache; } /** diff --git a/src/Console/AppInstallCommand.php b/src/Console/AppInstallCommand.php deleted file mode 100644 index e1023ba7..00000000 --- a/src/Console/AppInstallCommand.php +++ /dev/null @@ -1,168 +0,0 @@ -setName('app:install') - ->setDescription('Install application fixtures (node-types, settings, roles) from config.yml') - ->addOption( - 'dry-run', - 'd', - InputOption::VALUE_NONE, - 'Do nothing, only print information.' - ); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - if ($input->getOption('dry-run')) { - $this->dryRun = true; - } - $this->io = new SymfonyStyle($input, $output); - - /* - * Test if Classname is not a valid yaml file before using Theme - */ - $configPath = $this->projectDir . '/src/Resources/config.yml'; - $realConfigPath = realpath($configPath); - if (false !== $realConfigPath && file_exists($realConfigPath)) { - $this->io->note('Install assets directly from file: ' . $realConfigPath); - $themeConfigPath = $realConfigPath; - } else { - $this->io->error($configPath . ' configuration file is not readable.'); - return 1; - } - - $this->importAppData($themeConfigPath); - return 0; - } - - protected function importAppData(string $themeConfigPath): void - { - $data = $this->getAppConfig($themeConfigPath); - - if (isset($data["importFiles"])) { - if (isset($data["importFiles"]['groups'])) { - foreach ($data["importFiles"]['groups'] as $filename) { - $this->importFile($filename, $this->groupsImporter); - } - } - if (isset($data["importFiles"]['roles'])) { - foreach ($data["importFiles"]['roles'] as $filename) { - $this->importFile($filename, $this->rolesImporter); - } - } - if (isset($data["importFiles"]['settings'])) { - foreach ($data["importFiles"]['settings'] as $filename) { - $this->importFile($filename, $this->settingsImporter); - } - } - if (isset($data["importFiles"]['nodetypes'])) { - foreach ($data["importFiles"]['nodetypes'] as $filename) { - $this->importFile($filename, $this->nodeTypesImporter); - } - } - if (isset($data["importFiles"]['tags'])) { - foreach ($data["importFiles"]['tags'] as $filename) { - $this->importFile($filename, $this->tagsImporter); - } - } - if (isset($data["importFiles"]['attributes'])) { - foreach ($data["importFiles"]['attributes'] as $filename) { - $this->importFile($filename, $this->attributeImporter); - } - } - } else { - $this->io->warning('Config file "' . $themeConfigPath . '" has no data to import.'); - } - } - - /** - * @param string $filename - * @param EntityImporterInterface $importer - */ - protected function importFile(string $filename, EntityImporterInterface $importer): void - { - if (false !== $realFilename = realpath($filename)) { - $file = new File($realFilename); - } else { - throw new \RuntimeException($filename . ' is not a valid file'); - } - if (!$this->dryRun) { - try { - if (false === $fileContent = file_get_contents($file->getPathname())) { - throw new \RuntimeException($file->getPathname() . ' file is not readable'); - } - $importer->import($fileContent); - $this->managerRegistry->getManager()->flush(); - $this->io->writeln( - '* ' . $file->getPathname() . ' file has been imported.' - ); - return; - } catch (EntityAlreadyExistsException $e) { - $this->io->writeln( - '* ' . $file->getPathname() . '' . - ' has NOT been imported (' . $e->getMessage() . ').' - ); - } - } - $this->io->writeln( - '* ' . $file->getPathname() . ' file has been imported.' - ); - } - - /** - * @param string $appConfigPath - * @return array - */ - protected function getAppConfig(string $appConfigPath): array - { - if (false === $fileContent = file_get_contents($appConfigPath)) { - throw new \RuntimeException($appConfigPath . ' file is not readable'); - } - $data = Yaml::parse($fileContent); - if (!\is_array($data)) { - throw new \RuntimeException($appConfigPath . ' file is not a valid YAML file'); - } - return $data; - } -} diff --git a/src/Console/AppMigrateCommand.php b/src/Console/AppMigrateCommand.php deleted file mode 100644 index e8949486..00000000 --- a/src/Console/AppMigrateCommand.php +++ /dev/null @@ -1,126 +0,0 @@ -setName('app:migrate') - ->setDescription('Perform app:install and generate NS entities classes and Doctrine migrations.') - ->addOption( - 'dry-run', - 'd', - InputOption::VALUE_NONE, - 'Do nothing, only print information.' - ); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $io = new SymfonyStyle($input, $output); - - $question = new ConfirmationQuestion( - 'Are you sure to migrate against app config.yml file? This will generate new Doctrine Migrations and execute them. If you just want to import node-types run `bin/console app:install` instead', - !$input->isInteractive() - ); - if ($io->askQuestion($question) === false) { - $io->note('Nothing was done…'); - return 0; - } - - if ($input->getOption('dry-run')) { - $this->runCommand( - 'app:install', - '--dry-run', - null, - $input->isInteractive(), - $output->isQuiet(), - ); - } else { - $this->runCommand( - 'app:install', - '', - null, - $input->isInteractive(), - $output->isQuiet() - ) === 0 ? $io->success('app:install') : $io->error('app:install'); - - $this->runCommand( - 'generate:nsentities', - '', - null, - $input->isInteractive(), - $output->isQuiet() - ) === 0 ? $io->success('generate:nsentities') : $io->error('generate:nsentities'); - - $this->schemaUpdater->updateNodeTypesSchema(); - $this->schemaUpdater->updateSchema(); - $io->success('doctrine-migrations'); - - $this->runCommand( - 'doctrine:cache:clear-metadata', - '', - null, - false, - true - ) === 0 ? $io->success('doctrine:cache:clear-metadata') : $io->error('doctrine:cache:clear-metadata'); - - $this->runCommand( - 'cache:clear', - '', - null, - false, - true - ) === 0 ? $io->success('cache:clear') : $io->error('cache:clear'); - - $this->runCommand( - 'cache:pool:clear', - 'cache.global_clearer', - null, - false, - true - ) === 0 ? $io->success('cache:pool:clear') : $io->error('cache:pool:clear'); - } - return 0; - } - - protected function runCommand( - string $command, - string $args = '', - ?string $environment = null, - bool $interactive = true, - bool $quiet = false - ): int { - $args .= $interactive ? '' : ' --no-interaction '; - $args .= $quiet ? ' --quiet ' : ' -v '; - $args .= is_string($environment) ? (' --env ' . $environment) : ''; - - $process = Process::fromShellCommandline( - 'php bin/console ' . $command . ' ' . $args - ); - $process->setWorkingDirectory($this->projectDir); - $process->setTty($interactive); - $process->run(); - return $process->wait(); - } -} diff --git a/src/Console/CleanLoginAttemptCommand.php b/src/Console/CleanLoginAttemptCommand.php new file mode 100644 index 00000000..758ac145 --- /dev/null +++ b/src/Console/CleanLoginAttemptCommand.php @@ -0,0 +1,43 @@ +managerRegistry = $managerRegistry; + } + + protected function configure(): void + { + $this->setName('login-attempts:clean') + ->setDescription('Clean all login attempts older than 1 day'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $this->managerRegistry->getRepository(LoginAttempt::class)->cleanLoginAttempts(); + + $io->success('All login attempts older than 1 day were deleted.'); + + return 0; + } +} diff --git a/src/Console/CustomFormAnswerPurgeCommand.php b/src/Console/CustomFormAnswerPurgeCommand.php index 07270a01..5363c503 100644 --- a/src/Console/CustomFormAnswerPurgeCommand.php +++ b/src/Console/CustomFormAnswerPurgeCommand.php @@ -19,13 +19,20 @@ final class CustomFormAnswerPurgeCommand extends Command { + private ManagerRegistry $managerRegistry; + private EventDispatcherInterface $eventDispatcher; + private LoggerInterface $logger; + public function __construct( - private readonly ManagerRegistry $managerRegistry, - private readonly EventDispatcherInterface $eventDispatcher, - private readonly LoggerInterface $logger, + ManagerRegistry $managerRegistry, + EventDispatcherInterface $eventDispatcher, + LoggerInterface $logger, string $name = null ) { parent::__construct($name); + $this->managerRegistry = $managerRegistry; + $this->eventDispatcher = $eventDispatcher; + $this->logger = $logger; } protected function configure(): void diff --git a/src/Console/DecodePrivateKeyCommand.php b/src/Console/DecodePrivateKeyCommand.php new file mode 100644 index 00000000..e213f591 --- /dev/null +++ b/src/Console/DecodePrivateKeyCommand.php @@ -0,0 +1,49 @@ +keyChain = $keyChain; + $this->uniqueKeyEncoderFactory = $uniqueKeyEncoderFactory; + } + + protected function configure(): void + { + $this->setName('crypto:private-key:decode') + ->addArgument('key-name', InputArgument::REQUIRED) + ->addArgument('data', InputArgument::REQUIRED) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $keyName = $input->getArgument('key-name'); + $encoder = $this->uniqueKeyEncoderFactory->getEncoder($keyName); + $encoded = $encoder->decode($input->getArgument('data')); + + $io->note($encoded->getString()); + return 0; + } +} diff --git a/src/Console/EncodePrivateKeyCommand.php b/src/Console/EncodePrivateKeyCommand.php new file mode 100644 index 00000000..763b2ffa --- /dev/null +++ b/src/Console/EncodePrivateKeyCommand.php @@ -0,0 +1,50 @@ +keyChain = $keyChain; + $this->uniqueKeyEncoderFactory = $uniqueKeyEncoderFactory; + } + + protected function configure(): void + { + $this->setName('crypto:private-key:encode') + ->addArgument('key-name', InputArgument::REQUIRED) + ->addArgument('data', InputArgument::REQUIRED) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $keyName = $input->getArgument('key-name'); + $encoder = $this->uniqueKeyEncoderFactory->getEncoder($keyName); + $encoded = $encoder->encode(new HiddenString($input->getArgument('data'))); + + $io->note($encoded); + return 0; + } +} diff --git a/src/Console/FilesCommandTrait.php b/src/Console/FilesCommandTrait.php index 15d73514..e215fe70 100644 --- a/src/Console/FilesCommandTrait.php +++ b/src/Console/FilesCommandTrait.php @@ -9,7 +9,7 @@ trait FilesCommandTrait /** * @return string */ - protected function getPublicFolderName(): string + protected function getPublicFolderName() { return '/exported_public'; } @@ -17,7 +17,7 @@ protected function getPublicFolderName(): string /** * @return string */ - protected function getPrivateFolderName(): string + protected function getPrivateFolderName() { return '/exported_private'; } @@ -25,7 +25,7 @@ protected function getPrivateFolderName(): string /** * @return string */ - protected function getFontsFolderName(): string + protected function getFontsFolderName() { return '/exported_fonts'; } diff --git a/src/Console/FilesExportCommand.php b/src/Console/FilesExportCommand.php index adf93e2d..29fac777 100644 --- a/src/Console/FilesExportCommand.php +++ b/src/Console/FilesExportCommand.php @@ -14,17 +14,20 @@ use Symfony\Component\String\Slugger\AsciiSlugger; use ZipArchive; -final class FilesExportCommand extends Command +class FilesExportCommand extends Command { use FilesCommandTrait; - public function __construct( - private readonly FileAwareInterface $fileAware, - private readonly string $exportDir, - private readonly string $appNamespace, - ?string $name = null - ) { - parent::__construct($name); + protected FileAwareInterface $fileAware; + protected string $exportDir; + protected string $appNamespace; + + public function __construct(FileAwareInterface $fileAware, string $exportDir, string $appNamespace) + { + parent::__construct(); + $this->fileAware = $fileAware; + $this->exportDir = $exportDir; + $this->appNamespace = $appNamespace; } protected function configure(): void diff --git a/src/Console/FilesImportCommand.php b/src/Console/FilesImportCommand.php index 4b910575..283cadc3 100644 --- a/src/Console/FilesImportCommand.php +++ b/src/Console/FilesImportCommand.php @@ -15,16 +15,20 @@ use Symfony\Component\String\Slugger\AsciiSlugger; use ZipArchive; -final class FilesImportCommand extends Command +class FilesImportCommand extends Command { use FilesCommandTrait; - public function __construct( - private readonly FileAwareInterface $fileAware, - private readonly string $appNamespace, - ?string $name = null - ) { - parent::__construct($name); + protected FileAwareInterface $fileAware; + protected string $exportDir; + protected string $appNamespace; + + public function __construct(FileAwareInterface $fileAware, string $exportDir, string $appNamespace) + { + parent::__construct(); + $this->fileAware = $fileAware; + $this->exportDir = $exportDir; + $this->appNamespace = $appNamespace; } protected function configure(): void @@ -53,9 +57,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $appNamespace = (new AsciiSlugger())->slug($this->appNamespace, '_'); $tempDir = tempnam(sys_get_temp_dir(), $appNamespace . '_files'); - if (false === $tempDir) { - throw new \RuntimeException('Cannot create temporary directory.'); - } if (file_exists($tempDir)) { unlink($tempDir); } diff --git a/src/Console/GenerateApiResourceCommand.php b/src/Console/GenerateApiResourceCommand.php index 87ae30e3..7bfda603 100644 --- a/src/Console/GenerateApiResourceCommand.php +++ b/src/Console/GenerateApiResourceCommand.php @@ -12,14 +12,18 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -final class GenerateApiResourceCommand extends Command +class GenerateApiResourceCommand extends Command { + protected ManagerRegistry $managerRegistry; + protected ApiResourceGenerator $apiResourceGenerator; + public function __construct( - private readonly ManagerRegistry $managerRegistry, - private readonly ApiResourceGenerator $apiResourceGenerator, - ?string $name = null + ManagerRegistry $managerRegistry, + ApiResourceGenerator $apiResourceGenerator ) { - parent::__construct($name); + parent::__construct(); + $this->managerRegistry = $managerRegistry; + $this->apiResourceGenerator = $apiResourceGenerator; } protected function configure(): void diff --git a/src/Console/GenerateNodeSourceEntitiesCommand.php b/src/Console/GenerateNodeSourceEntitiesCommand.php index 04c3b8dc..1a630456 100644 --- a/src/Console/GenerateNodeSourceEntitiesCommand.php +++ b/src/Console/GenerateNodeSourceEntitiesCommand.php @@ -5,8 +5,6 @@ namespace RZ\Roadiz\CoreBundle\Console; use Doctrine\Persistence\ManagerRegistry; -use Psr\Container\ContainerExceptionInterface; -use Psr\Container\NotFoundExceptionInterface; use RZ\Roadiz\CoreBundle\Entity\NodeType; use RZ\Roadiz\CoreBundle\EntityHandler\HandlerFactory; use RZ\Roadiz\CoreBundle\EntityHandler\NodeTypeHandler; @@ -15,14 +13,23 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -final class GenerateNodeSourceEntitiesCommand extends Command +/** + * Command line utils for managing node-types from terminal. + */ +class GenerateNodeSourceEntitiesCommand extends Command { - public function __construct( - private readonly ManagerRegistry $managerRegistry, - private readonly HandlerFactory $handlerFactory, - ?string $name = null - ) { - parent::__construct($name); + protected ManagerRegistry $managerRegistry; + protected HandlerFactory $handlerFactory; + + /** + * @param ManagerRegistry $managerRegistry + * @param HandlerFactory $handlerFactory + */ + public function __construct(ManagerRegistry $managerRegistry, HandlerFactory $handlerFactory) + { + parent::__construct(); + $this->managerRegistry = $managerRegistry; + $this->handlerFactory = $handlerFactory; } protected function configure(): void @@ -31,10 +38,6 @@ protected function configure(): void ->setDescription('Generate node-sources entities PHP classes.'); } - /** - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface - */ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); diff --git a/src/Console/GeneratePrivateKeyCommand.php b/src/Console/GeneratePrivateKeyCommand.php new file mode 100644 index 00000000..3f46c280 --- /dev/null +++ b/src/Console/GeneratePrivateKeyCommand.php @@ -0,0 +1,47 @@ +keyChain = $keyChain; + $this->privateKeyName = $privateKeyName; + } + + protected function configure(): void + { + $this->setName('crypto:private-key:generate') + ->setDescription('Generate a default private key to encode data in your database.') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $this->keyChain->generate($this->privateKeyName); + $io->success(sprintf('Private key has been generated: %s', $this->privateKeyName)); + return 0; + } +} diff --git a/src/Console/GetCronLastExecDateCommand.php b/src/Console/GetCronLastExecDateCommand.php deleted file mode 100644 index e090c16d..00000000 --- a/src/Console/GetCronLastExecDateCommand.php +++ /dev/null @@ -1,43 +0,0 @@ -settingRepository->findOneByName('cron_last_exec_date'); - if (!($setting instanceof Setting)) { - $io->warning('Last execution date of cron job has not been persisted yet.'); - return Command::FAILURE; - } - - $io->success(sprintf( - 'Last execution date of cron job is %s.', - $setting->getRawValue() - )); - - return Command::SUCCESS; - } -} diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index 913de1ca..0e21d59e 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -18,16 +18,33 @@ use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Yaml\Yaml; -final class InstallCommand extends Command +/** + * Command line utils for installing RZ-CMS v3 from terminal. + */ +class InstallCommand extends Command { + protected ManagerRegistry $managerRegistry; + protected RolesImporter $rolesImporter; + protected GroupsImporter $groupsImporter; + protected SettingsImporter $settingsImporter; + + /** + * @param ManagerRegistry $managerRegistry + * @param RolesImporter $rolesImporter + * @param GroupsImporter $groupsImporter + * @param SettingsImporter $settingsImporter + */ public function __construct( - private readonly ManagerRegistry $managerRegistry, - private readonly RolesImporter $rolesImporter, - private readonly GroupsImporter $groupsImporter, - private readonly SettingsImporter $settingsImporter, - ?string $name = null + ManagerRegistry $managerRegistry, + RolesImporter $rolesImporter, + GroupsImporter $groupsImporter, + SettingsImporter $settingsImporter ) { - parent::__construct($name); + parent::__construct(); + $this->managerRegistry = $managerRegistry; + $this->rolesImporter = $rolesImporter; + $this->groupsImporter = $groupsImporter; + $this->settingsImporter = $settingsImporter; } protected function configure(): void @@ -53,48 +70,26 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io->askQuestion($question) ) { $fixturesRoot = dirname(__DIR__) . '/../config'; - $fixtureFile = file_get_contents($fixturesRoot . "/fixtures.yaml"); - - if (false === $fixtureFile) { - $io->error('No fixtures.yaml file found in ' . $fixturesRoot); - return 1; - } - - $data = Yaml::parse($fixtureFile); + $data = Yaml::parse(file_get_contents($fixturesRoot . "/fixtures.yaml")); if (isset($data["importFiles"]['roles'])) { foreach ($data["importFiles"]['roles'] as $filename) { $filePath = $fixturesRoot . "/" . $filename; - $fileContents = file_get_contents($filePath); - if (false === $fileContents) { - $io->error('No file found in ' . $filePath); - return 1; - } - $this->rolesImporter->import($fileContents); + $this->rolesImporter->import(file_get_contents($filePath)); $io->success('Theme file “' . $filePath . '” has been imported.'); } } if (isset($data["importFiles"]['groups'])) { foreach ($data["importFiles"]['groups'] as $filename) { $filePath = $fixturesRoot . "/" . $filename; - $fileContents = file_get_contents($filePath); - if (false === $fileContents) { - $io->error('No file found in ' . $filePath); - return 1; - } - $this->groupsImporter->import($fileContents); + $this->groupsImporter->import(file_get_contents($filePath)); $io->success('Theme file “' . $filePath . '” has been imported.'); } } if (isset($data["importFiles"]['settings'])) { foreach ($data["importFiles"]['settings'] as $filename) { $filePath = $fixturesRoot . "/" . $filename; - $fileContents = file_get_contents($filePath); - if (false === $fileContents) { - $io->error('No file found in ' . $filePath); - return 1; - } - $this->settingsImporter->import($fileContents); + $this->settingsImporter->import(file_get_contents($filePath)); $io->success('Theme files “' . $filePath . '” has been imported.'); } } diff --git a/src/Console/LogsCleanupCommand.php b/src/Console/LogsCleanupCommand.php index 56fd662a..6e00af9a 100644 --- a/src/Console/LogsCleanupCommand.php +++ b/src/Console/LogsCleanupCommand.php @@ -6,22 +6,25 @@ use Doctrine\ORM\NoResultException; use Doctrine\Persistence\ManagerRegistry; -use RZ\Roadiz\CoreBundle\Logger\Entity\Log; +use RZ\Roadiz\CoreBundle\Entity\Log; use RZ\Roadiz\CoreBundle\Repository\LogRepository; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -final class LogsCleanupCommand extends Command +class LogsCleanupCommand extends Command { - public function __construct( - private readonly ManagerRegistry $managerRegistry, - ?string $name = null - ) { - parent::__construct($name); + protected ManagerRegistry $managerRegistry; + + /** + * @param ManagerRegistry $managerRegistry + */ + public function __construct(ManagerRegistry $managerRegistry) + { + parent::__construct(); + $this->managerRegistry = $managerRegistry; } protected function configure(): void @@ -44,11 +47,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (\is_string($input->getOption('since'))) { $since = '-' . $input->getOption('since'); } - $interval = \DateInterval::createFromDateString($since); - if (false === $interval) { - throw new InvalidArgumentException('Invalid since option format.'); - } - $now->add($interval); + $now->add(\DateInterval::createFromDateString($since)); $io = new SymfonyStyle($input, $output); /** @var LogRepository $logRepository */ diff --git a/src/Console/MailerTestCommand.php b/src/Console/MailerTestCommand.php index 83a66962..f7c43949 100644 --- a/src/Console/MailerTestCommand.php +++ b/src/Console/MailerTestCommand.php @@ -13,15 +13,20 @@ use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Mime\Address; -final class MailerTestCommand extends Command +class MailerTestCommand extends Command { - public function __construct( - private readonly EmailManager $emailManager, - ?string $name = null - ) { - parent::__construct($name); + protected EmailManager $emailManager; + + /** + * @param EmailManager $emailManager + */ + public function __construct(EmailManager $emailManager) + { + parent::__construct(); + $this->emailManager = $emailManager; } + protected function configure(): void { $this->setName('mailer:send:test') diff --git a/src/Console/NodeApplyUniversalFieldsCommand.php b/src/Console/NodeApplyUniversalFieldsCommand.php index 942e2541..a136ac45 100644 --- a/src/Console/NodeApplyUniversalFieldsCommand.php +++ b/src/Console/NodeApplyUniversalFieldsCommand.php @@ -15,14 +15,20 @@ use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; -final class NodeApplyUniversalFieldsCommand extends Command +class NodeApplyUniversalFieldsCommand extends Command { - public function __construct( - private readonly ManagerRegistry $managerRegistry, - private readonly UniversalDataDuplicator $universalDataDuplicator, - ?string $name = null - ) { - parent::__construct($name); + protected ManagerRegistry $managerRegistry; + protected UniversalDataDuplicator $universalDataDuplicator; + + /** + * @param ManagerRegistry $managerRegistry + * @param UniversalDataDuplicator $universalDataDuplicator + */ + public function __construct(ManagerRegistry $managerRegistry, UniversalDataDuplicator $universalDataDuplicator) + { + parent::__construct(); + $this->managerRegistry = $managerRegistry; + $this->universalDataDuplicator = $universalDataDuplicator; } protected function configure(): void diff --git a/src/Console/NodeClearTagCommand.php b/src/Console/NodeClearTagCommand.php index e7e775f7..7ba65879 100644 --- a/src/Console/NodeClearTagCommand.php +++ b/src/Console/NodeClearTagCommand.php @@ -15,13 +15,18 @@ use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; -final class NodeClearTagCommand extends Command +class NodeClearTagCommand extends Command { - public function __construct( - private readonly ManagerRegistry $managerRegistry, - ?string $name = null - ) { - parent::__construct($name); + protected SymfonyStyle $io; + protected ManagerRegistry $managerRegistry; + + /** + * @param ManagerRegistry $managerRegistry + */ + public function __construct(ManagerRegistry $managerRegistry) + { + parent::__construct(); + $this->managerRegistry = $managerRegistry; } protected function configure(): void @@ -43,7 +48,7 @@ protected function getNodeQueryBuilder(Tag $tag): QueryBuilder protected function execute(InputInterface $input, OutputInterface $output): int { $em = $this->managerRegistry->getManagerForClass(Node::class); - $io = new SymfonyStyle($input, $output); + $this->io = new SymfonyStyle($input, $output); $tagId = (int) $input->getArgument('tagId'); if ($tagId <= 0) { @@ -64,12 +69,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int ->getSingleScalarResult(); if ($count <= 0) { - $io->warning('No nodes were found linked with this tag.'); + $this->io->warning('No nodes were found linked with this tag.'); return 0; } if ( - $io->askQuestion(new ConfirmationQuestion( + $this->io->askQuestion(new ConfirmationQuestion( sprintf('Are you sure to delete permanently %d nodes?', $count), false )) @@ -79,7 +84,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int ->getQuery() ->getResult(); - $io->progressStart($count); + $this->io->progressStart($count); /** @var Node $node */ foreach ($results as $node) { $em->remove($node); @@ -87,10 +92,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int $em->flush(); // Executes all updates. } ++$i; - $io->progressAdvance(); + $this->io->progressAdvance(); } $em->flush(); - $io->progressFinish(); + $this->io->progressFinish(); } return 0; diff --git a/src/Console/NodeTypesCommand.php b/src/Console/NodeTypesCommand.php index 262bcaa6..23107664 100644 --- a/src/Console/NodeTypesCommand.php +++ b/src/Console/NodeTypesCommand.php @@ -13,13 +13,20 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -final class NodeTypesCommand extends Command +/** + * Command line utils for managing node-types from terminal. + */ +class NodeTypesCommand extends Command { - public function __construct( - private readonly ManagerRegistry $managerRegistry, - ?string $name = null - ) { - parent::__construct($name); + protected ManagerRegistry $managerRegistry; + + /** + * @param ManagerRegistry $managerRegistry + */ + public function __construct(ManagerRegistry $managerRegistry) + { + parent::__construct(); + $this->managerRegistry = $managerRegistry; } protected function configure(): void diff --git a/src/Console/NodeTypesCreationCommand.php b/src/Console/NodeTypesCreationCommand.php index 25788fec..81c11ba2 100644 --- a/src/Console/NodeTypesCreationCommand.php +++ b/src/Console/NodeTypesCreationCommand.php @@ -23,13 +23,21 @@ */ class NodeTypesCreationCommand extends Command { - public function __construct( - protected readonly ManagerRegistry $managerRegistry, - protected readonly HandlerFactory $handlerFactory, - protected readonly SchemaUpdater $schemaUpdater, - ?string $name = null - ) { - parent::__construct($name); + protected ManagerRegistry $managerRegistry; + protected HandlerFactory $handlerFactory; + protected SchemaUpdater $schemaUpdater; + + /** + * @param ManagerRegistry $managerRegistry + * @param HandlerFactory $handlerFactory + * @param SchemaUpdater $schemaUpdater + */ + public function __construct(ManagerRegistry $managerRegistry, HandlerFactory $handlerFactory, SchemaUpdater $schemaUpdater) + { + parent::__construct(); + $this->managerRegistry = $managerRegistry; + $this->handlerFactory = $handlerFactory; + $this->schemaUpdater = $schemaUpdater; } protected function configure(): void @@ -100,8 +108,7 @@ private function executeCreation(InputInterface $input, OutputInterface $output) protected function addNodeTypeField(NodeType $nodeType, int|float|string $position, SymfonyStyle $io): void { $field = new NodeTypeField(); - $position = floatval($position); - $field->setPosition($position); + $field->setPosition((float) $position); $questionfName = new Question('[Field ' . $position . '] Enter field name', 'content'); $fName = $io->askQuestion($questionfName); diff --git a/src/Console/NodeTypesDefaultValuesCommand.php b/src/Console/NodeTypesDefaultValuesCommand.php index 2c4c3ec7..bf59585e 100644 --- a/src/Console/NodeTypesDefaultValuesCommand.php +++ b/src/Console/NodeTypesDefaultValuesCommand.php @@ -5,6 +5,7 @@ namespace RZ\Roadiz\CoreBundle\Console; use Doctrine\Persistence\ManagerRegistry; +use RZ\Roadiz\Core\AbstractEntities\AbstractEntity; use RZ\Roadiz\Core\AbstractEntities\AbstractField; use RZ\Roadiz\CoreBundle\Entity\NodeTypeField; use RZ\Roadiz\EntityGenerator\Field\DefaultValuesResolverInterface; @@ -14,14 +15,16 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -final class NodeTypesDefaultValuesCommand extends Command +class NodeTypesDefaultValuesCommand extends Command { - public function __construct( - private readonly DefaultValuesResolverInterface $defaultValuesResolver, - private readonly ManagerRegistry $managerRegistry, - string $name = null - ) { + private DefaultValuesResolverInterface $defaultValuesResolver; + private ManagerRegistry $managerRegistry; + + public function __construct(DefaultValuesResolverInterface $defaultValuesResolver, ManagerRegistry $managerRegistry, string $name = null) + { parent::__construct($name); + $this->defaultValuesResolver = $defaultValuesResolver; + $this->managerRegistry = $managerRegistry; } protected function configure(): void diff --git a/src/Console/NodeTypesDeleteCommand.php b/src/Console/NodeTypesDeleteCommand.php index 6e9d47c0..e4bfb545 100644 --- a/src/Console/NodeTypesDeleteCommand.php +++ b/src/Console/NodeTypesDeleteCommand.php @@ -19,15 +19,23 @@ /** * Command line utils for managing node-types from terminal. */ -final class NodeTypesDeleteCommand extends Command +class NodeTypesDeleteCommand extends Command { - public function __construct( - private readonly ManagerRegistry $managerRegistry, - private readonly HandlerFactory $handlerFactory, - private readonly SchemaUpdater $schemaUpdater, - ?string $name = null - ) { - parent::__construct($name); + protected ManagerRegistry $managerRegistry; + protected HandlerFactory $handlerFactory; + protected SchemaUpdater $schemaUpdater; + + /** + * @param ManagerRegistry $managerRegistry + * @param HandlerFactory $handlerFactory + * @param SchemaUpdater $schemaUpdater + */ + public function __construct(ManagerRegistry $managerRegistry, HandlerFactory $handlerFactory, SchemaUpdater $schemaUpdater) + { + parent::__construct(); + $this->managerRegistry = $managerRegistry; + $this->handlerFactory = $handlerFactory; + $this->schemaUpdater = $schemaUpdater; } protected function configure(): void diff --git a/src/Console/NodesCleanNamesCommand.php b/src/Console/NodesCleanNamesCommand.php index 394a55de..bee548c7 100644 --- a/src/Console/NodesCleanNamesCommand.php +++ b/src/Console/NodesCleanNamesCommand.php @@ -15,14 +15,21 @@ use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; +/** + * @package RZ\Roadiz\CoreBundle\Console + */ final class NodesCleanNamesCommand extends Command { - public function __construct( - private readonly ManagerRegistry $managerRegistry, - private readonly NodeNamePolicyInterface $nodeNamePolicy, - ?string $name = null - ) { - parent::__construct($name); + protected NodeNamePolicyInterface $nodeNamePolicy; + protected ManagerRegistry $managerRegistry; + + /** + * @param ManagerRegistry $managerRegistry + */ + public function __construct(ManagerRegistry $managerRegistry) + { + parent::__construct(); + $this->managerRegistry = $managerRegistry; } protected function configure(): void diff --git a/src/Console/NodesCommand.php b/src/Console/NodesCommand.php index cd5e56a0..3442ea0c 100644 --- a/src/Console/NodesCommand.php +++ b/src/Console/NodesCommand.php @@ -13,13 +13,20 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -final class NodesCommand extends Command +/** + * Command line utils for managing nodes from terminal. + */ +class NodesCommand extends Command { - public function __construct( - private readonly ManagerRegistry $managerRegistry, - ?string $name = null - ) { - parent::__construct($name); + protected ManagerRegistry $managerRegistry; + + /** + * @param ManagerRegistry $managerRegistry + */ + public function __construct(ManagerRegistry $managerRegistry) + { + parent::__construct(); + $this->managerRegistry = $managerRegistry; } protected function configure(): void diff --git a/src/Console/NodesCreationCommand.php b/src/Console/NodesCreationCommand.php new file mode 100644 index 00000000..98e3ce6b --- /dev/null +++ b/src/Console/NodesCreationCommand.php @@ -0,0 +1,133 @@ +managerRegistry = $managerRegistry; + $this->nodeFactory = $nodeFactory; + } + + protected function configure(): void + { + $this->setName('nodes:create') + ->setDescription('Create a new node') + ->addArgument( + 'node-name', + InputArgument::REQUIRED, + 'Node name' + ) + ->addArgument( + 'node-type', + InputArgument::REQUIRED, + 'Node-type name' + ) + ->addArgument( + 'locale', + InputArgument::OPTIONAL, + 'Translation locale' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $nodeName = $input->getArgument('node-name'); + $typeName = $input->getArgument('node-type'); + $locale = $input->getArgument('locale'); + $this->io = new SymfonyStyle($input, $output); + + $existingNode = $this->managerRegistry + ->getRepository(Node::class) + ->setDisplayingNotPublishedNodes(true) + ->findOneByNodeName($nodeName); + + if (null === $existingNode) { + $type = $this->managerRegistry + ->getRepository(NodeType::class) + ->findOneByName($typeName); + + if (null !== $type) { + $translation = null; + + if ($locale) { + $translation = $this->managerRegistry + ->getRepository(TranslationInterface::class) + ->findOneBy(['locale' => $locale]); + } + + if ($translation === null) { + $translation = $this->managerRegistry + ->getRepository(TranslationInterface::class) + ->findDefault(); + } + + $this->executeNodeCreation($input->getArgument('node-name'), $type, $translation); + } else { + $this->io->error('"' . $typeName . '" node type does not exist.'); + return 1; + } + return 0; + } else { + $this->io->error($existingNode->getNodeName() . ' node already exists.'); + return 1; + } + } + + /** + * @param string $nodeName + * @param NodeType $type + * @param TranslationInterface $translation + */ + private function executeNodeCreation( + string $nodeName, + NodeType $type, + TranslationInterface $translation + ): void { + $node = $this->nodeFactory->create($nodeName, $type, $translation); + $source = $node->getNodeSources()->first() ?: null; + if (null === $source) { + throw new \InvalidArgumentException('Node source is null'); + } + $fields = $type->getFields(); + + foreach ($fields as $field) { + if (!$field->isVirtual()) { + $question = new Question('[Field ' . $field->getLabel() . '] : ', null); + $fValue = $this->io->askQuestion($question); + $setterName = $field->getSetterName(); + $source->$setterName($fValue); + } + } + + $this->managerRegistry->getManagerForClass(Node::class)->flush(); + $this->io->success('Node “' . $nodeName . '” created at root level.'); + } +} diff --git a/src/Console/NodesDetailsCommand.php b/src/Console/NodesDetailsCommand.php index 53515526..d5095807 100644 --- a/src/Console/NodesDetailsCommand.php +++ b/src/Console/NodesDetailsCommand.php @@ -14,13 +14,17 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -final class NodesDetailsCommand extends Command +class NodesDetailsCommand extends Command { - public function __construct( - private readonly ManagerRegistry $managerRegistry, - ?string $name = null - ) { - parent::__construct($name); + protected ManagerRegistry $managerRegistry; + + /** + * @param ManagerRegistry $managerRegistry + */ + public function __construct(ManagerRegistry $managerRegistry) + { + parent::__construct(); + $this->managerRegistry = $managerRegistry; } protected function configure(): void @@ -63,7 +67,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (is_array($data)) { $data = implode(', ', $data); } - if ($data instanceof \DateTimeInterface) { + if ($data instanceof \DateTime) { $data = $data->format('c'); } if ($data instanceof \stdClass) { diff --git a/src/Console/NodesEmptyTrashCommand.php b/src/Console/NodesEmptyTrashCommand.php index 8e21028b..d9078595 100644 --- a/src/Console/NodesEmptyTrashCommand.php +++ b/src/Console/NodesEmptyTrashCommand.php @@ -15,14 +15,20 @@ use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; -final class NodesEmptyTrashCommand extends Command +class NodesEmptyTrashCommand extends Command { - public function __construct( - private readonly ManagerRegistry $managerRegistry, - private readonly HandlerFactory $handlerFactory, - ?string $name = null - ) { - parent::__construct($name); + protected ManagerRegistry $managerRegistry; + protected HandlerFactory $handlerFactory; + + /** + * @param ManagerRegistry $managerRegistry + * @param HandlerFactory $handlerFactory + */ + public function __construct(ManagerRegistry $managerRegistry, HandlerFactory $handlerFactory) + { + parent::__construct(); + $this->managerRegistry = $managerRegistry; + $this->handlerFactory = $handlerFactory; } protected function configure(): void diff --git a/src/Console/NodesOrphansCommand.php b/src/Console/NodesOrphansCommand.php index e3a68598..60fcf003 100644 --- a/src/Console/NodesOrphansCommand.php +++ b/src/Console/NodesOrphansCommand.php @@ -13,13 +13,20 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -final class NodesOrphansCommand extends Command +/** + * @package RZ\Roadiz\CoreBundle\Console + */ +class NodesOrphansCommand extends Command { - public function __construct( - private readonly ManagerRegistry $managerRegistry, - ?string $name = null - ) { - parent::__construct($name); + protected ManagerRegistry $managerRegistry; + + /** + * @param ManagerRegistry $managerRegistry + */ + public function __construct(ManagerRegistry $managerRegistry) + { + parent::__construct(); + $this->managerRegistry = $managerRegistry; } protected function configure(): void diff --git a/src/Console/PrivateKeyCommand.php b/src/Console/PrivateKeyCommand.php new file mode 100644 index 00000000..9e368498 --- /dev/null +++ b/src/Console/PrivateKeyCommand.php @@ -0,0 +1,57 @@ +keyChain = $keyChain; + } + + protected function configure(): void + { + $this->setName('crypto:private-key:info') + ->addArgument('key-name', InputArgument::REQUIRED) + ->setDescription('Get a private or public key information') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $keyName = $input->getArgument('key-name'); + $key = $this->keyChain->get($keyName); + + $io->table([ + 'name', + 'type', + 'derivation', + 'usage', + 'base64', + ], [[ + $keyName, + $key->isAsymmetricKey() ? 'asymmetric' : 'symmetric', + $key->isPublicKey() ? 'public' : 'private', + $key->isSigningKey() ? 'signing' : 'encryption', + base64_encode($key->getRawKeyMaterial()) + ]]); + return 0; + } +} diff --git a/src/Console/PurgeLoginAttemptCommand.php b/src/Console/PurgeLoginAttemptCommand.php new file mode 100644 index 00000000..95985673 --- /dev/null +++ b/src/Console/PurgeLoginAttemptCommand.php @@ -0,0 +1,54 @@ +managerRegistry = $managerRegistry; + } + + protected function configure(): void + { + $this->setName('login-attempts:purge') + ->setDescription('Purge all login attempts for one IP address') + ->addArgument( + 'ip-address', + InputArgument::REQUIRED + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $this->managerRegistry + ->getRepository(LoginAttempt::class) + ->purgeLoginAttempts($input->getArgument('ip-address')); + + $io->success('All login attempts were deleted for ' . $input->getArgument('ip-address')); + + return 0; + } +} diff --git a/src/Console/RegisterCronLastExecDateCommand.php b/src/Console/RegisterCronLastExecDateCommand.php deleted file mode 100644 index 20bed441..00000000 --- a/src/Console/RegisterCronLastExecDateCommand.php +++ /dev/null @@ -1,46 +0,0 @@ -managerRegistry->getManager(); - $parameter = $this->settingRepository->findOneByName('cron_last_exec_date'); - if (null === $parameter) { - $parameter = new Setting(); - $parameter->setName('cron_last_exec_date'); - $manager->persist($parameter); - } - - $parameter->setValue(new \DateTimeImmutable()); - $manager->flush(); - $io->success('Last execution date of cron job has been persisted.'); - - return Command::SUCCESS; - } -} diff --git a/src/Console/SolrCommand.php b/src/Console/SolrCommand.php index 32396f2b..6e930a20 100644 --- a/src/Console/SolrCommand.php +++ b/src/Console/SolrCommand.php @@ -10,15 +10,21 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +/** + * Command line utils for managing nodes from terminal. + */ class SolrCommand extends Command { protected ?SymfonyStyle $io = null; + protected ClientRegistry $clientRegistry; - public function __construct( - protected readonly ClientRegistry $clientRegistry, - ?string $name = null - ) { - parent::__construct($name); + /** + * @param ClientRegistry $clientRegistry + */ + public function __construct(ClientRegistry $clientRegistry) + { + parent::__construct(); + $this->clientRegistry = $clientRegistry; } protected function configure(): void @@ -51,7 +57,7 @@ protected function displayBasicConfig(): void if (null !== $this->io) { $this->io->error('No Solr search engine server has been configured…'); $this->io->note(<<indexerFactory = $indexerFactory; } protected function configure(): void diff --git a/src/Console/SolrReindexCommand.php b/src/Console/SolrReindexCommand.php index 041a22fb..eb582902 100644 --- a/src/Console/SolrReindexCommand.php +++ b/src/Console/SolrReindexCommand.php @@ -12,20 +12,31 @@ use RZ\Roadiz\CoreBundle\SearchEngine\Indexer\IndexerFactoryInterface; use RZ\Roadiz\CoreBundle\SearchEngine\SolariumDocumentTranslation; use RZ\Roadiz\CoreBundle\SearchEngine\SolariumNodeSource; +use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Stopwatch\Stopwatch; +/** + * Command line utils for managing nodes from terminal. + */ class SolrReindexCommand extends SolrCommand implements ThemeAwareCommandInterface { + protected ?QuestionHelper $questionHelper = null; + protected IndexerFactoryInterface $indexerFactory; + + /** + * @param ClientRegistry $clientRegistry + * @param IndexerFactoryInterface $indexerFactory + */ public function __construct( - protected readonly IndexerFactoryInterface $indexerFactory, ClientRegistry $clientRegistry, - ?string $name = null + IndexerFactoryInterface $indexerFactory ) { - parent::__construct($clientRegistry, $name); + parent::__construct($clientRegistry); + $this->indexerFactory = $indexerFactory; } protected function configure(): void diff --git a/src/Console/SolrResetCommand.php b/src/Console/SolrResetCommand.php index ac48d1f3..6cd34a0f 100644 --- a/src/Console/SolrResetCommand.php +++ b/src/Console/SolrResetCommand.php @@ -13,14 +13,21 @@ use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; -final class SolrResetCommand extends SolrCommand +/** + * Command line utils for managing nodes from terminal. + */ +class SolrResetCommand extends SolrCommand { - public function __construct( - private readonly IndexerFactoryInterface $indexerFactory, - ClientRegistry $clientRegistry, - ?string $name = null - ) { - parent::__construct($clientRegistry, $name); + protected IndexerFactoryInterface $indexerFactory; + + /** + * @param ClientRegistry $clientRegistry + * @param IndexerFactoryInterface $indexerFactory + */ + public function __construct(ClientRegistry $clientRegistry, IndexerFactoryInterface $indexerFactory) + { + parent::__construct($clientRegistry); + $this->indexerFactory = $indexerFactory; } protected function configure(): void diff --git a/src/Console/TranslationsCommand.php b/src/Console/TranslationsCommand.php index e3781525..58217357 100644 --- a/src/Console/TranslationsCommand.php +++ b/src/Console/TranslationsCommand.php @@ -11,13 +11,20 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +/** + * Command line utils for managing translations from terminal. + */ class TranslationsCommand extends Command { - public function __construct( - protected readonly ManagerRegistry $managerRegistry, - ?string $name = null - ) { - parent::__construct($name); + protected ManagerRegistry $managerRegistry; + + /** + * @param ManagerRegistry $managerRegistry + */ + public function __construct(ManagerRegistry $managerRegistry) + { + parent::__construct(); + $this->managerRegistry = $managerRegistry; } protected function configure(): void diff --git a/src/Console/TranslationsCreationCommand.php b/src/Console/TranslationsCreationCommand.php index 9d0f8b90..a4ac9e5d 100644 --- a/src/Console/TranslationsCreationCommand.php +++ b/src/Console/TranslationsCreationCommand.php @@ -4,7 +4,9 @@ namespace RZ\Roadiz\CoreBundle\Console; +use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\CoreBundle\Entity\Translation; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -14,8 +16,19 @@ /** * Command line utils for managing translations */ -final class TranslationsCreationCommand extends TranslationsCommand +class TranslationsCreationCommand extends Command { + protected ManagerRegistry $managerRegistry; + + /** + * @param ManagerRegistry $managerRegistry + */ + public function __construct(ManagerRegistry $managerRegistry) + { + parent::__construct(); + $this->managerRegistry = $managerRegistry; + } + protected function configure(): void { $this->setName('translations:create') diff --git a/src/Console/TranslationsDeleteCommand.php b/src/Console/TranslationsDeleteCommand.php index 60f54562..eb3e5a9d 100644 --- a/src/Console/TranslationsDeleteCommand.php +++ b/src/Console/TranslationsDeleteCommand.php @@ -4,7 +4,9 @@ namespace RZ\Roadiz\CoreBundle\Console; +use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\CoreBundle\Entity\Translation; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -14,8 +16,19 @@ /** * Command line utils for managing translations. */ -final class TranslationsDeleteCommand extends TranslationsCommand +class TranslationsDeleteCommand extends Command { + protected ManagerRegistry $managerRegistry; + + /** + * @param ManagerRegistry $managerRegistry + */ + public function __construct(ManagerRegistry $managerRegistry) + { + parent::__construct(); + $this->managerRegistry = $managerRegistry; + } + protected function configure(): void { $this->setName('translations:delete') diff --git a/src/Console/TranslationsDisableCommand.php b/src/Console/TranslationsDisableCommand.php index 164b7422..abf6317b 100644 --- a/src/Console/TranslationsDisableCommand.php +++ b/src/Console/TranslationsDisableCommand.php @@ -4,7 +4,9 @@ namespace RZ\Roadiz\CoreBundle\Console; +use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\CoreBundle\Entity\Translation; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -14,8 +16,19 @@ /** * Command line utils for managing translations. */ -final class TranslationsDisableCommand extends TranslationsCommand +class TranslationsDisableCommand extends Command { + protected ManagerRegistry $managerRegistry; + + /** + * @param ManagerRegistry $managerRegistry + */ + public function __construct(ManagerRegistry $managerRegistry) + { + parent::__construct(); + $this->managerRegistry = $managerRegistry; + } + protected function configure(): void { $this->setName('translations:disable') diff --git a/src/Console/TranslationsEnableCommand.php b/src/Console/TranslationsEnableCommand.php index f6656694..f7fe0530 100644 --- a/src/Console/TranslationsEnableCommand.php +++ b/src/Console/TranslationsEnableCommand.php @@ -4,7 +4,9 @@ namespace RZ\Roadiz\CoreBundle\Console; +use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\CoreBundle\Entity\Translation; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -14,8 +16,19 @@ /** * Command line utils for managing translations. */ -final class TranslationsEnableCommand extends TranslationsCommand +class TranslationsEnableCommand extends Command { + protected ManagerRegistry $managerRegistry; + + /** + * @param ManagerRegistry $managerRegistry + */ + public function __construct(ManagerRegistry $managerRegistry) + { + parent::__construct(); + $this->managerRegistry = $managerRegistry; + } + protected function configure(): void { $this->setName('translations:enable') diff --git a/src/Console/UsersCommand.php b/src/Console/UsersCommand.php index 2309ff8d..f86c7100 100644 --- a/src/Console/UsersCommand.php +++ b/src/Console/UsersCommand.php @@ -14,13 +14,20 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +/** + * Command line utils for managing users from terminal. + */ class UsersCommand extends Command { - public function __construct( - protected readonly ManagerRegistry $managerRegistry, - string $name = null - ) { - parent::__construct($name); + protected ManagerRegistry $managerRegistry; + + /** + * @param ManagerRegistry $managerRegistry + */ + public function __construct(ManagerRegistry $managerRegistry) + { + parent::__construct(); + $this->managerRegistry = $managerRegistry; } protected function configure(): void @@ -61,9 +68,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($user === null) { $io->error('User “' . $name . '” does not exist… use users:create to add a new user.'); } else { - $tableContent = [ - $this->getUserTableRow($user), - ]; + $tableContent = [$this->getUserTableRow($user)]; $io->table( array_keys($tableContent[0]), $tableContent @@ -115,9 +120,10 @@ protected function getUserForInput(InputInterface $input): User * Get role by name, and create it if it does not exist. * * @param string $roleName + * * @return Role */ - public function getRole(string $roleName = Role::ROLE_SUPERADMIN): Role + public function getRole(string $roleName = Role::ROLE_SUPERADMIN) { $role = $this->managerRegistry ->getRepository(Role::class) diff --git a/src/Console/UsersCreationCommand.php b/src/Console/UsersCreationCommand.php index 2373093e..cd97c713 100644 --- a/src/Console/UsersCreationCommand.php +++ b/src/Console/UsersCreationCommand.php @@ -15,6 +15,9 @@ use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Style\SymfonyStyle; +/** + * Command line utils for managing users from terminal. + */ final class UsersCreationCommand extends UsersCommand { protected function configure(): void @@ -35,32 +38,29 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { - $io = new SymfonyStyle($input, $output); $name = $input->getArgument('username'); - if (!\is_string($name) || empty($name)) { - throw new \InvalidArgumentException('Username argument is required.'); - } - - /** @var User|null $user */ - $user = $this->managerRegistry - ->getRepository(User::class) - ->findOneBy(['username' => $name]); - - if ($user instanceof User) { - $io->warning('User “' . $name . '” already exists.'); - return 1; + if ($name) { + /** @var User|null $user */ + $user = $this->managerRegistry + ->getRepository(User::class) + ->findOneBy(['username' => $name]); + + if (null === $user) { + $user = $this->executeUserCreation($name, $input, $output); + + // Change password right away + $command = $this->getApplication()->find('users:password'); + $arguments = [ + 'username' => $user->getUsername(), + ]; + $passwordInput = new ArrayInput($arguments); + return $command->run($passwordInput, $output); + } else { + throw new \InvalidArgumentException('User “' . $name . '” already exists.'); + } } - - $user = $this->executeUserCreation($name, $input, $output); - - // Change password right away - $command = $this->getApplication()->find('users:password'); - $arguments = [ - 'username' => $user->getUsername(), - ]; - $passwordInput = new ArrayInput($arguments); - return $command->run($passwordInput, $output); + return 0; } /** diff --git a/src/Console/UsersDeleteCommand.php b/src/Console/UsersDeleteCommand.php index 2e8c3062..6fb754da 100644 --- a/src/Console/UsersDeleteCommand.php +++ b/src/Console/UsersDeleteCommand.php @@ -11,6 +11,9 @@ use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; +/** + * Command line utils for managing users from terminal. + */ final class UsersDeleteCommand extends UsersCommand { protected function configure(): void @@ -28,24 +31,33 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $name = $input->getArgument('username'); - $user = $this->getUserForInput($input); - $confirmation = new ConfirmationQuestion( - 'Do you really want to delete user “' . $user->getUsername() . '”?', - false - ); - if ( - !$input->isInteractive() || $io->askQuestion( - $confirmation - ) - ) { - $this->managerRegistry->getManagerForClass(User::class)->remove($user); - $this->managerRegistry->getManagerForClass(User::class)->flush(); - $io->success('User “' . $name . '” deleted.'); - return 0; - } else { - $io->warning('User “' . $name . '” was not deleted.'); - return 1; + if ($name) { + /** @var User|null $user */ + $user = $this->managerRegistry + ->getRepository(User::class) + ->findOneBy(['username' => $name]); + + if (null !== $user) { + $confirmation = new ConfirmationQuestion( + 'Do you really want to delete user “' . $user->getUsername() . '”?', + false + ); + if ( + !$input->isInteractive() || $io->askQuestion( + $confirmation + ) + ) { + $this->managerRegistry->getManagerForClass(User::class)->remove($user); + $this->managerRegistry->getManagerForClass(User::class)->flush(); + $io->success('User “' . $name . '” deleted.'); + } else { + $io->warning('User “' . $name . '” was not deleted.'); + } + } else { + throw new \InvalidArgumentException('User “' . $name . '” does not exist.'); + } } + return 0; } } diff --git a/src/Console/UsersDisableCommand.php b/src/Console/UsersDisableCommand.php index 97b0062a..a5d3c5f7 100644 --- a/src/Console/UsersDisableCommand.php +++ b/src/Console/UsersDisableCommand.php @@ -11,6 +11,9 @@ use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; +/** + * Command line utils for managing users from terminal. + */ final class UsersDisableCommand extends UsersCommand { protected function configure(): void @@ -28,24 +31,33 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $name = $input->getArgument('username'); - $user = $this->getUserForInput($input); - $confirmation = new ConfirmationQuestion( - 'Do you really want to disable user “' . $user->getUsername() . '”?', - false - ); - if ( - !$input->isInteractive() || $io->askQuestion( - $confirmation - ) - ) { - $user->setEnabled(false); - $this->managerRegistry->getManagerForClass(User::class)->flush(); - $io->success('User “' . $name . '” disabled.'); - return 0; - } else { - $io->warning('User “' . $name . '” was not disabled.'); - return 1; + if ($name) { + /** @var User|null $user */ + $user = $this->managerRegistry + ->getRepository(User::class) + ->findOneBy(['username' => $name]); + + if (null !== $user) { + $confirmation = new ConfirmationQuestion( + 'Do you really want to disable user “' . $user->getUsername() . '”?', + false + ); + if ( + !$input->isInteractive() || $io->askQuestion( + $confirmation + ) + ) { + $user->setEnabled(false); + $this->managerRegistry->getManagerForClass(User::class)->flush(); + $io->success('User “' . $name . '” disabled.'); + } else { + $io->warning('User “' . $name . '” was not disabled.'); + } + } else { + throw new \InvalidArgumentException('User “' . $name . '” does not exist.'); + } } + return 0; } } diff --git a/src/Console/UsersEnableCommand.php b/src/Console/UsersEnableCommand.php index b9092656..f166e74b 100644 --- a/src/Console/UsersEnableCommand.php +++ b/src/Console/UsersEnableCommand.php @@ -11,6 +11,9 @@ use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; +/** + * Command line utils for managing users from terminal. + */ final class UsersEnableCommand extends UsersCommand { protected function configure(): void @@ -28,24 +31,32 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $name = $input->getArgument('username'); - $user = $this->getUserForInput($input); - $confirmation = new ConfirmationQuestion( - 'Do you really want to enable user “' . $user->getUsername() . '”?', - false - ); - if ( - !$input->isInteractive() || $io->askQuestion( - $confirmation - ) - ) { - $user->setEnabled(true); - $this->managerRegistry->getManagerForClass(User::class)->flush(); - $io->success('User “' . $name . '” was enabled.'); - return 0; - } else { - $io->warning('User “' . $name . '” was not enabled'); - return 1; + if ($name) { + $user = $this->managerRegistry + ->getRepository(User::class) + ->findOneBy(['username' => $name]); + + if (null !== $user) { + $confirmation = new ConfirmationQuestion( + 'Do you really want to enable user “' . $user->getUsername() . '”?', + false + ); + if ( + !$input->isInteractive() || $io->askQuestion( + $confirmation + ) + ) { + $user->setEnabled(true); + $this->managerRegistry->getManagerForClass(User::class)->flush(); + $io->success('User “' . $name . '” was enabled.'); + } else { + $io->warning('User “' . $name . '” was not enabled'); + } + } else { + throw new \InvalidArgumentException('User “' . $name . '” does not exist.'); + } } + return 0; } } diff --git a/src/Console/UsersPasswordCommand.php b/src/Console/UsersPasswordCommand.php index 9d265e1f..6472dfd7 100644 --- a/src/Console/UsersPasswordCommand.php +++ b/src/Console/UsersPasswordCommand.php @@ -13,14 +13,21 @@ use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; +/** + * Command line utils for managing users from terminal. + */ final class UsersPasswordCommand extends UsersCommand { - public function __construct( - private readonly PasswordGenerator $passwordGenerator, - ManagerRegistry $managerRegistry, - ?string $name = null - ) { - parent::__construct($managerRegistry, $name); + private PasswordGenerator $passwordGenerator; + + /** + * @param ManagerRegistry $managerRegistry + * @param PasswordGenerator $passwordGenerator + */ + public function __construct(ManagerRegistry $managerRegistry, PasswordGenerator $passwordGenerator) + { + parent::__construct($managerRegistry); + $this->passwordGenerator = $passwordGenerator; } protected function configure(): void @@ -38,24 +45,33 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $name = $input->getArgument('username'); - $user = $this->getUserForInput($input); - - $confirmation = new ConfirmationQuestion( - 'Do you really want to regenerate user “' . $user->getUsername() . '” password?', - false - ); - if ( - !$input->isInteractive() || $io->askQuestion( - $confirmation - ) - ) { - $user->setPlainPassword($this->passwordGenerator->generatePassword(12)); - $this->managerRegistry->getManagerForClass(User::class)->flush(); - $io->success('A new password was regenerated for ' . $name . ': ' . $user->getPlainPassword()); - return 0; - } else { - $io->warning('User password was not changed.'); - return 1; + + if ($name) { + /** @var User|null $user */ + $user = $this->managerRegistry + ->getRepository(User::class) + ->findOneBy(['username' => $name]); + + if (null !== $user) { + $confirmation = new ConfirmationQuestion( + 'Do you really want to regenerate user “' . $user->getUsername() . '” password?', + false + ); + if ( + !$input->isInteractive() || $io->askQuestion( + $confirmation + ) + ) { + $user->setPlainPassword($this->passwordGenerator->generatePassword(12)); + $this->managerRegistry->getManagerForClass(User::class)->flush(); + $io->success('A new password was regenerated for ' . $name . ': ' . $user->getPlainPassword()); + } else { + $io->warning('User password was not changed.'); + } + } else { + throw new \InvalidArgumentException('User “' . $name . '” does not exist.'); + } } + return 0; } } diff --git a/src/Console/UsersRolesCommand.php b/src/Console/UsersRolesCommand.php index 047a2d5b..40b725b9 100644 --- a/src/Console/UsersRolesCommand.php +++ b/src/Console/UsersRolesCommand.php @@ -20,12 +20,16 @@ */ final class UsersRolesCommand extends UsersCommand { - public function __construct( - private readonly Roles $rolesBag, - ManagerRegistry $managerRegistry, - ?string $name = null - ) { - parent::__construct($managerRegistry, $name); + private Roles $rolesBag; + + /** + * @param ManagerRegistry $managerRegistry + * @param Roles $rolesBag + */ + public function __construct(ManagerRegistry $managerRegistry, Roles $rolesBag) + { + parent::__construct($managerRegistry); + $this->rolesBag = $rolesBag; } protected function configure(): void @@ -54,43 +58,53 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - $user = $this->getUserForInput($input); + $name = $input->getArgument('username'); - if ($input->getOption('add')) { - $roles = $this->managerRegistry - ->getRepository(Role::class) - ->getAllRoleName(); + if ($name) { + /** @var User|null $user */ + $user = $this->managerRegistry + ->getRepository(User::class) + ->findOneBy(['username' => $name]); - $question = new Question( - 'Enter the role name to add' - ); - $question->setAutocompleterValues($roles); + if (null !== $user) { + if ($input->getOption('add')) { + $roles = $this->managerRegistry + ->getRepository(Role::class) + ->getAllRoleName(); - do { - $role = $io->askQuestion($question); - if ($role != "") { - $user->addRoleEntity($this->rolesBag->get($role)); - $this->managerRegistry->getManagerForClass(User::class)->flush(); - $io->success('Role: ' . $role . ' added.'); - } - } while ($role != ""); - } elseif ($input->getOption('remove')) { - do { - $roles = $user->getRoles(); - $question = new Question( - 'Enter the role name to remove' - ); - $question->setAutocompleterValues($roles); + $question = new Question( + 'Enter the role name to add' + ); + $question->setAutocompleterValues($roles); + + do { + $role = $io->askQuestion($question); + if ($role != "") { + $user->addRoleEntity($this->rolesBag->get($role)); + $this->managerRegistry->getManagerForClass(User::class)->flush(); + $io->success('Role: ' . $role . ' added.'); + } + } while ($role != ""); + } elseif ($input->getOption('remove')) { + do { + $roles = $user->getRoles(); + $question = new Question( + 'Enter the role name to remove' + ); + $question->setAutocompleterValues($roles); - $role = $io->askQuestion($question); - if (in_array($role, $roles)) { - $user->removeRoleEntity($this->rolesBag->get($role)); - $this->managerRegistry->getManagerForClass(User::class)->flush(); - $io->success('Role: ' . $role . ' removed.'); + $role = $io->askQuestion($question); + if (in_array($role, $roles)) { + $user->removeRoleEntity($this->rolesBag->get($role)); + $this->managerRegistry->getManagerForClass(User::class)->flush(); + $io->success('Role: ' . $role . ' removed.'); + } + } while ($role != ""); } - } while ($role != ""); + } else { + throw new \InvalidArgumentException('User “' . $name . '” does not exist.'); + } } - return 0; } } diff --git a/src/Console/VersionsPurgeCommand.php b/src/Console/VersionsPurgeCommand.php index 8891e472..0f83df79 100644 --- a/src/Console/VersionsPurgeCommand.php +++ b/src/Console/VersionsPurgeCommand.php @@ -18,11 +18,15 @@ final class VersionsPurgeCommand extends Command { - public function __construct( - private readonly ManagerRegistry $managerRegistry, - ?string $name = null - ) { - parent::__construct($name); + protected ManagerRegistry $managerRegistry; + + /** + * @param ManagerRegistry $managerRegistry + */ + public function __construct(ManagerRegistry $managerRegistry) + { + parent::__construct(); + $this->managerRegistry = $managerRegistry; } protected function configure(): void diff --git a/src/Controller/CustomFormController.php b/src/Controller/CustomFormController.php index b044580a..78957bce 100644 --- a/src/Controller/CustomFormController.php +++ b/src/Controller/CustomFormController.php @@ -7,14 +7,20 @@ use Doctrine\Persistence\ManagerRegistry; use Exception; use League\Flysystem\FilesystemException; +use League\Flysystem\FilesystemOperator; use Limenius\Liform\LiformInterface; use Psr\Log\LoggerInterface; +use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; use RZ\Roadiz\CoreBundle\Bag\Settings; use RZ\Roadiz\CoreBundle\CustomForm\CustomFormHelperFactory; -use RZ\Roadiz\CoreBundle\CustomForm\Message\CustomFormAnswerNotifyMessage; use RZ\Roadiz\CoreBundle\Entity\CustomForm; +use RZ\Roadiz\CoreBundle\Entity\CustomFormAnswer; use RZ\Roadiz\CoreBundle\Exception\EntityAlreadyExistsException; use RZ\Roadiz\CoreBundle\Form\Error\FormErrorSerializerInterface; +use RZ\Roadiz\CoreBundle\Mailer\EmailManager; +use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; +use RZ\Roadiz\CoreBundle\Repository\TranslationRepository; +use RZ\Roadiz\Documents\Models\DocumentInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormInterface; @@ -26,25 +32,57 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException; -use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Mailer\Exception\TransportExceptionInterface; +use Symfony\Component\Mime\Address; use Symfony\Component\RateLimiter\RateLimiterFactory; use Symfony\Component\Serializer\SerializerInterface; +use Symfony\Contracts\Translation\LocaleAwareInterface; use Symfony\Contracts\Translation\TranslatorInterface; +use Twig\Error\LoaderError; +use Twig\Error\RuntimeError; +use Twig\Error\SyntaxError; final class CustomFormController extends AbstractController { + private EmailManager $emailManager; + private Settings $settingsBag; + private LoggerInterface $logger; + private TranslatorInterface $translator; + private CustomFormHelperFactory $customFormHelperFactory; + private LiformInterface $liform; + private SerializerInterface $serializer; + private FormErrorSerializerInterface $formErrorSerializer; + private ManagerRegistry $registry; + private RateLimiterFactory $customFormLimiter; + private FilesystemOperator $documentsStorage; + private PreviewResolverInterface $previewResolver; + public function __construct( - private readonly Settings $settingsBag, - private readonly LoggerInterface $logger, - private readonly TranslatorInterface $translator, - private readonly CustomFormHelperFactory $customFormHelperFactory, - private readonly LiformInterface $liform, - private readonly SerializerInterface $serializer, - private readonly FormErrorSerializerInterface $formErrorSerializer, - private readonly ManagerRegistry $registry, - private readonly RateLimiterFactory $customFormLimiter, - private readonly MessageBusInterface $messageBus, + EmailManager $emailManager, + Settings $settingsBag, + LoggerInterface $logger, + TranslatorInterface $translator, + CustomFormHelperFactory $customFormHelperFactory, + LiformInterface $liform, + SerializerInterface $serializer, + FormErrorSerializerInterface $formErrorSerializer, + ManagerRegistry $registry, + RateLimiterFactory $customFormLimiter, + FilesystemOperator $documentsStorage, + PreviewResolverInterface $previewResolver ) { + $this->emailManager = $emailManager; + $this->settingsBag = $settingsBag; + $this->logger = $logger; + $this->translator = $translator; + $this->customFormHelperFactory = $customFormHelperFactory; + $this->liform = $liform; + $this->serializer = $serializer; + $this->formErrorSerializer = $formErrorSerializer; + $this->registry = $registry; + $this->customFormLimiter = $customFormLimiter; + $this->documentsStorage = $documentsStorage; + $this->previewResolver = $previewResolver; } protected function validateCustomForm(?CustomForm $customForm): void @@ -57,6 +95,38 @@ protected function validateCustomForm(?CustomForm $customForm): void } } + protected function getTranslationFromRequest(?Request $request): TranslationInterface + { + $locale = null; + + if (null !== $request) { + $locale = $request->query->get('_locale'); + + /* + * If no _locale query param is defined check Accept-Language header + */ + if (null === $locale) { + $locale = $request->getPreferredLanguage($this->getTranslationRepository()->getAllLocales()); + } + } + /* + * Then fallback to default CMS locale + */ + if (null === $locale) { + $translation = $this->getTranslationRepository()->findDefault(); + } elseif ($this->previewResolver->isPreview()) { + $translation = $this->getTranslationRepository() + ->findOneByLocaleOrOverrideLocale((string) $locale); + } else { + $translation = $this->getTranslationRepository() + ->findOneAvailableByLocaleOrOverrideLocale((string) $locale); + } + if (null === $translation) { + throw new NotFoundHttpException('No translation for locale ' . $locale); + } + return $translation; + } + /** * @param Request $request * @param int $id @@ -69,6 +139,11 @@ public function definitionAction(Request $request, int $id): JsonResponse $this->validateCustomForm($customForm); $helper = $this->customFormHelperFactory->createHelper($customForm); + $translation = $this->getTranslationFromRequest($request); + $request->setLocale($translation->getPreferredLocale()); + if ($this->translator instanceof LocaleAwareInterface) { + $this->translator->setLocale($translation->getPreferredLocale()); + } $schema = json_encode($this->liform->transform($helper->getForm($request, false, false))); return new JsonResponse( @@ -104,6 +179,12 @@ public function postAction(Request $request, int $id): Response $customForm = $this->registry->getRepository(CustomForm::class)->find($id); $this->validateCustomForm($customForm); + $translation = $this->getTranslationFromRequest($request); + $request->setLocale($translation->getPreferredLocale()); + if ($this->translator instanceof LocaleAwareInterface) { + $this->translator->setLocale($translation->getPreferredLocale()); + } + $mixed = $this->prepareAndHandleCustomFormAssignation( $request, $customForm, @@ -141,6 +222,9 @@ public function postAction(Request $request, int $id): Response * @param int $customFormId * @return Response * @throws FilesystemException + * @throws LoaderError + * @throws RuntimeError + * @throws SyntaxError */ public function addAction(Request $request, int $customFormId): Response { @@ -159,7 +243,7 @@ public function addAction(Request $request, int $customFormId): Response ) ); - if ($mixed instanceof Response) { + if ($mixed instanceof RedirectResponse) { $mixed->prepare($request); return $mixed->send(); } else { @@ -184,7 +268,60 @@ public function sentAction(Request $request, int $customFormId): Response } /** - * Prepare and handle a CustomForm Form then send a confirmation email. + * Send an answer form by Email. + * + * @param CustomFormAnswer $answer + * @param array $assignation + * @param string|array|null $receiver + * @return bool + * @throws TransportExceptionInterface + * @throws LoaderError + * @throws RuntimeError + * @throws SyntaxError + * @deprecated Use async message handler to send email receipt from CustomFormAnswer. + */ + public function sendAnswer( + CustomFormAnswer $answer, + array $assignation, + $receiver + ): bool { + $defaultSender = $this->settingsBag->get('email_sender'); + $defaultSender = !empty($defaultSender) ? $defaultSender : 'sender@roadiz.io'; + $this->emailManager->setAssignation($assignation); + $this->emailManager->setEmailTemplate('@RoadizCore/email/forms/answerForm.html.twig'); + $this->emailManager->setEmailPlainTextTemplate('@RoadizCore/email/forms/answerForm.txt.twig'); + $this->emailManager->setSubject($assignation['title']); + $this->emailManager->setEmailTitle($assignation['title']); + $this->emailManager->setSender($defaultSender); + + try { + foreach ($answer->getAnswerFields() as $customFormAnswerAttr) { + /** @var DocumentInterface $document */ + foreach ($customFormAnswerAttr->getDocuments() as $document) { + $this->emailManager->addResource( + $this->documentsStorage->readStream($document->getMountPath()), + $document->getFilename(), + $this->documentsStorage->mimeType($document->getMountPath()) + ); + } + } + } catch (FilesystemException $exception) { + $this->logger->error($exception->getMessage()); + } + + if (empty($receiver)) { + $this->emailManager->setReceiver($defaultSender); + } else { + $this->emailManager->setReceiver($receiver); + } + + // Send the message + $this->emailManager->send(); + return true; + } + + /** + * Prepare and handle a CustomForm Form then send a confirm email. * * * This method will return an assignation **array** if form is not validated. * * customForm @@ -199,7 +336,7 @@ public function sentAction(Request $request, int $customFormId): Response * @param string|null $emailSender * @param bool $prefix * @return array|Response - * @throws FilesystemException + * @throws SyntaxError|RuntimeError|LoaderError|FilesystemException */ public function prepareAndHandleCustomFormAssignation( Request $request, @@ -227,38 +364,73 @@ public function prepareAndHandleCustomFormAssignation( */ $answer = $helper->parseAnswerFormData($form, null, $request->getClientIp()); - $answerId = $answer->getId(); - if (!is_int($answerId)) { - throw new \RuntimeException('Answer ID is null'); - } + /* + * Prepare field assignation for email content. + */ + $assignation["emailFields"] = [ + ["name" => "ip.address", "value" => $answer->getIp()], + ["name" => "submittedAt", "value" => $answer->getSubmittedAt()->format('Y-m-d H:i:s')], + ]; + $assignation["emailFields"] = array_merge( + $assignation["emailFields"], + $answer->toArray(false) + ); - if (null === $emailSender || false === filter_var($emailSender, FILTER_VALIDATE_EMAIL)) { - $emailSender = $this->settingsBag->get('email_sender'); + $assignation['title'] = $this->translator->trans( + 'new.answer.form.%site%', + ['%site%' => $customFormsEntity->getDisplayName()] + ); + + if (null !== $emailSender && false !== filter_var($emailSender, FILTER_VALIDATE_EMAIL)) { + $assignation['mailContact'] = $emailSender; + } else { + $assignation['mailContact'] = $this->settingsBag->get('email_sender'); } - $this->messageBus->dispatch(new CustomFormAnswerNotifyMessage( - $answerId, - $this->translator->trans( - 'new.answer.form.%site%', - ['%site%' => $customFormsEntity->getDisplayName()] - ), - $emailSender, - $request->getLocale() - )); - - $msg = $this->translator->trans( - 'customForm.%name%.send', - ['%name%' => $customFormsEntity->getDisplayName()] - ); + /* + * Send answer notification + */ + try { + $receiver = array_filter( + array_map('trim', explode(',', $customFormsEntity->getEmail() ?? '')) + ); + $receiver = array_map(function (string $email) { + return new Address($email); + }, $receiver); + $this->sendAnswer( + $answer, + [ + 'mailContact' => $assignation['mailContact'], + 'fields' => $assignation["emailFields"], + 'customForm' => $customFormsEntity, + 'title' => $this->translator->trans( + 'new.answer.form.%site%', + ['%site%' => $customFormsEntity->getDisplayName()] + ), + ], + $receiver + ); + + $msg = $this->translator->trans( + 'customForm.%name%.send', + ['%name%' => $customFormsEntity->getDisplayName()] + ); - if (!$request->attributes->getBoolean('_stateless') && $request->hasPreviousSession()) { $session = $request->getSession(); if ($session instanceof Session) { $session->getFlashBag()->add('confirm', $msg); } - } + $this->logger->info($msg); - $this->logger->info($msg); + } catch (TransportExceptionInterface $e) { + // Do not fail if answer has been registered but email has not been sent. + $this->logger->warning('Custom form answer has been registered but email could not been sent.', [ + 'exception' => $e, + 'message' => $e->getMessage(), + 'customForm' => $customFormsEntity->getDisplayName(), + 'answerId' => $answer->getId() + ]); + } return $response; } catch (EntityAlreadyExistsException $e) { @@ -270,4 +442,16 @@ public function prepareAndHandleCustomFormAssignation( $assignation['formObject'] = $form; return $assignation; } + + protected function getTranslationRepository(): TranslationRepository + { + $repository = $this->registry->getRepository(TranslationInterface::class); + if (!$repository instanceof TranslationRepository) { + throw new \RuntimeException( + 'Translation repository must be instance of ' . + TranslationRepository::class + ); + } + return $repository; + } } diff --git a/src/Controller/RedirectionController.php b/src/Controller/RedirectionController.php index bffc5dc4..bb478d9c 100644 --- a/src/Controller/RedirectionController.php +++ b/src/Controller/RedirectionController.php @@ -16,6 +16,9 @@ final class RedirectionController { private UrlGeneratorInterface $urlGenerator; + /** + * @param UrlGeneratorInterface $urlGenerator + */ public function __construct(UrlGeneratorInterface $urlGenerator) { $this->urlGenerator = $urlGenerator; diff --git a/src/Crypto/UniqueKeyEncoderFactory.php b/src/Crypto/UniqueKeyEncoderFactory.php new file mode 100644 index 00000000..9747ed07 --- /dev/null +++ b/src/Crypto/UniqueKeyEncoderFactory.php @@ -0,0 +1,55 @@ +keyChain = $keyChain; + $this->defaultKeyName = $defaultKeyName; + } + + public function getEncoder(?string $keyName = null): UniqueKeyEncoderInterface + { + try { + $keyName = $keyName ?? $this->defaultKeyName; + $key = $this->keyChain->get($keyName); + + if ($key instanceof EncryptionSecretKey) { + $publicKey = $key->derivePublicKey(); + return new AsymmetricUniqueKeyEncoder( + $publicKey, + $key + ); + } elseif ($key instanceof EncryptionKey) { + return new SymmetricUniqueKeyEncoder($key); + } + } catch (\Exception $exception) { + throw new InvalidKey( + sprintf('Key %s is not a valid encryption key', $keyName), + 0, + $exception + ); + } + + throw new InvalidKey(sprintf('Key %s is not a valid encryption key', $keyName)); + } +} diff --git a/src/CustomForm/CustomFormHelper.php b/src/CustomForm/CustomFormHelper.php index ce8a28de..c333572c 100644 --- a/src/CustomForm/CustomFormHelper.php +++ b/src/CustomForm/CustomFormHelper.php @@ -213,7 +213,7 @@ protected function getDocumentFolderForCustomForm(): ?Folder */ private function formValueToString($rawValue): string { - if ($rawValue instanceof \DateTimeInterface) { + if ($rawValue instanceof \DateTime) { return $rawValue->format('Y-m-d H:i:s'); } elseif (is_array($rawValue)) { $values = $rawValue; diff --git a/src/CustomForm/Message/CustomFormAnswerNotifyMessage.php b/src/CustomForm/Message/CustomFormAnswerNotifyMessage.php deleted file mode 100644 index 8f092728..00000000 --- a/src/CustomForm/Message/CustomFormAnswerNotifyMessage.php +++ /dev/null @@ -1,49 +0,0 @@ -customFormAnswerId = $customFormAnswerId; - $this->title = $title; - $this->senderAddress = $senderAddress; - $this->locale = $locale; - } - - public function getCustomFormAnswerId(): int - { - return $this->customFormAnswerId; - } - - public function getTitle(): string - { - return $this->title; - } - - public function getSenderAddress(): string - { - return $this->senderAddress; - } - - public function getLocale(): string - { - return $this->locale; - } -} diff --git a/src/CustomForm/Message/Handler/CustomFormAnswerNotifyMessageHandler.php b/src/CustomForm/Message/Handler/CustomFormAnswerNotifyMessageHandler.php deleted file mode 100644 index 60cc59d7..00000000 --- a/src/CustomForm/Message/Handler/CustomFormAnswerNotifyMessageHandler.php +++ /dev/null @@ -1,124 +0,0 @@ -managerRegistry - ->getRepository(CustomFormAnswer::class) - ->find($message->getCustomFormAnswerId()); - - if (!($answer instanceof CustomFormAnswer)) { - throw new UnrecoverableMessageHandlingException('CustomFormAnswer not found'); - } - - $emailFields = [ - ["name" => "ip.address", "value" => $answer->getIp()], - ["name" => "submittedAt", "value" => $answer->getSubmittedAt()->format('Y-m-d H:i:s')], - ]; - $emailFields = array_merge( - $emailFields, - $answer->toArray(false) - ); - - $receiver = array_filter( - array_map('trim', explode(',', $answer->getCustomForm()->getEmail() ?? '')) - ); - $receiver = array_map(function (string $email) { - return new Address($email); - }, $receiver); - $this->sendAnswer( - $answer, - [ - 'mailContact' => $message->getSenderAddress(), - 'fields' => $emailFields, - 'customForm' => $answer->getCustomForm(), - 'title' => $message->getTitle(), - 'requestLocale' => $message->getLocale(), - ], - $receiver - ); - } - - /** - * Send an answer form by Email. - * - * @param CustomFormAnswer $answer - * @param array $assignation - * @param string|array|null $receiver - * @throws TransportExceptionInterface - * @throws LoaderError - * @throws RuntimeError - * @throws SyntaxError - */ - private function sendAnswer( - CustomFormAnswer $answer, - array $assignation, - $receiver - ): void { - $defaultSender = $this->settingsBag->get('email_sender'); - $defaultSender = !empty($defaultSender) ? $defaultSender : 'sender@roadiz.io'; - $this->emailManager->setAssignation($assignation); - $this->emailManager->setEmailTemplate('@RoadizCore/email/forms/answerForm.html.twig'); - $this->emailManager->setEmailPlainTextTemplate('@RoadizCore/email/forms/answerForm.txt.twig'); - $this->emailManager->setSubject($assignation['title']); - $this->emailManager->setEmailTitle($assignation['title']); - $this->emailManager->setSender($defaultSender); - - try { - foreach ($answer->getAnswerFields() as $customFormAnswerAttr) { - /** @var DocumentInterface $document */ - foreach ($customFormAnswerAttr->getDocuments() as $document) { - $this->emailManager->addResource( - $this->documentsStorage->readStream($document->getMountPath()), - $document->getFilename(), - $this->documentsStorage->mimeType($document->getMountPath()) - ); - } - } - } catch (FilesystemException $exception) { - $this->logger->error($exception->getMessage(), [ - 'entity' => $answer - ]); - } - - if (empty($receiver)) { - $this->emailManager->setReceiver($defaultSender); - } else { - $this->emailManager->setReceiver($receiver); - } - - // Send the message - $this->emailManager->send(); - } -} diff --git a/src/DependencyInjection/Compiler/MediaFinderCompilerPass.php b/src/DependencyInjection/Compiler/MediaFinderCompilerPass.php index ee69f857..b6deacab 100644 --- a/src/DependencyInjection/Compiler/MediaFinderCompilerPass.php +++ b/src/DependencyInjection/Compiler/MediaFinderCompilerPass.php @@ -15,7 +15,6 @@ class MediaFinderCompilerPass implements CompilerPassInterface public function process(ContainerBuilder $container): void { if ($container->hasParameter('roadiz_core.medias.supported_platforms')) { - /** @var array $parameter */ $parameter = $container->getParameter('roadiz_core.medias.supported_platforms'); $taggedServices = $container->findTaggedServiceIds( 'roadiz_core.media_finder' diff --git a/src/DependencyInjection/Compiler/NodesSourcesEntitiesPathCompilerPass.php b/src/DependencyInjection/Compiler/NodesSourcesEntitiesPathCompilerPass.php index 037afe46..d771a7ca 100644 --- a/src/DependencyInjection/Compiler/NodesSourcesEntitiesPathCompilerPass.php +++ b/src/DependencyInjection/Compiler/NodesSourcesEntitiesPathCompilerPass.php @@ -15,9 +15,6 @@ class NodesSourcesEntitiesPathCompilerPass implements CompilerPassInterface public function process(ContainerBuilder $container): void { $projectDir = $container->getParameter('kernel.project_dir'); - if (!\is_string($projectDir)) { - throw new \RuntimeException('kernel.project_dir parameter must be a string.'); - } $container->setParameter('roadiz_core.generated_entities_dir', $projectDir . '/src/GeneratedEntity'); $container->setParameter('roadiz_core.serialized_node_types_dir', $projectDir . '/src/Resources/node-types'); $container->setParameter('roadiz_core.import_files_config_path', $projectDir . '/src/Resources/config.yml'); diff --git a/src/DependencyInjection/Compiler/TreeWalkerDefinitionFactoryCompilerPass.php b/src/DependencyInjection/Compiler/TreeWalkerDefinitionFactoryCompilerPass.php deleted file mode 100644 index b8c0d188..00000000 --- a/src/DependencyInjection/Compiler/TreeWalkerDefinitionFactoryCompilerPass.php +++ /dev/null @@ -1,43 +0,0 @@ -has(TreeWalkerGenerator::class)) { - $definition = $container->findDefinition(TreeWalkerGenerator::class); - $serviceIds = $container->findTaggedServiceIds( - 'roadiz_core.tree_walker_definition_factory', - ); - foreach ($serviceIds as $serviceId => $tags) { - foreach ($tags as $tag) { - if (isset($tag['classname']) && \is_string($tag['classname'])) { - /* - * TreeWalkerGenerator::addDefinitionFactoryConfiguration($classname, $serviceId, $onlyVisible = true) - */ - $definition->addMethodCall( - 'addDefinitionFactoryConfiguration', - [ - $tag['classname'], - new Reference($serviceId), - $tag['onlyVisible'] ?? true - ] - ); - } - } - } - } - } -} diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 7f5e8948..5529c8d4 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -4,7 +4,6 @@ namespace RZ\Roadiz\CoreBundle\DependencyInjection; -use RZ\Roadiz\CoreBundle\Api\Model\WebResponse; use RZ\Roadiz\CoreBundle\Controller\DefaultNodeSourceController; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\NodeDefinition; @@ -38,25 +37,15 @@ public function getConfigTreeBuilder(): TreeBuilder ->scalarNode('maxVersionsShowed') ->defaultValue(10) ->end() - ->scalarNode('previewRequiredRoleName') - ->info('Role name required to access preview mode.') - ->defaultValue('ROLE_BACKEND_USER') - ->end() ->scalarNode('defaultNodeSourceController') ->defaultValue(DefaultNodeSourceController::class) ->end() - ->scalarNode('webResponseClass') - ->defaultValue(WebResponse::class) - ->end() ->booleanNode('useNativeJsonColumnType') ->defaultValue(true) ->end() ->booleanNode('hideRoadizVersion') ->defaultValue(false) ->end() - ->booleanNode('useGravatar') - ->defaultTrue() - ->end() ->scalarNode('documentsLibDir')->defaultValue( 'vendor/roadiz/documents/src' )->info('Relative path to Roadiz Documents lib sources from project directory.')->end() @@ -76,6 +65,19 @@ public function getConfigTreeBuilder(): TreeBuilder their node-type to avoid name conflicts with reachable nodes (pages). EOT) ->end() + ->arrayNode('security') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('private_key_dir') + ->defaultValue('%kernel.project_dir%/var/secret') + ->info('Asymmetric cryptographic key directory.') + ->end() + ->scalarNode('private_key_name') + ->defaultValue('default') + ->info('Asymmetric cryptographic key name.') + ->end() + ->end() + ->end() ->append($this->addSolrNode()) ->append($this->addInheritanceNode()) ->append($this->addReverseProxyCacheNode()) diff --git a/src/DependencyInjection/RoadizCoreExtension.php b/src/DependencyInjection/RoadizCoreExtension.php index dce12f58..fc150208 100644 --- a/src/DependencyInjection/RoadizCoreExtension.php +++ b/src/DependencyInjection/RoadizCoreExtension.php @@ -6,9 +6,12 @@ use League\CommonMark\Environment\Environment; use League\CommonMark\MarkdownConverter; +use RZ\Crypto\KeyChain\AsymmetricFilesystemKeyChain; +use RZ\Crypto\KeyChain\KeyChainInterface; use RZ\Roadiz\CoreBundle\Cache\CloudflareProxyCache; use RZ\Roadiz\CoreBundle\Cache\ReverseProxyCache; use RZ\Roadiz\CoreBundle\Cache\ReverseProxyCacheLocator; +use RZ\Roadiz\CoreBundle\Crypto\UniqueKeyEncoderFactory; use RZ\Roadiz\CoreBundle\Entity\CustomForm; use RZ\Roadiz\CoreBundle\Entity\Document; use RZ\Roadiz\CoreBundle\Entity\Node; @@ -18,7 +21,6 @@ use RZ\Roadiz\CoreBundle\Entity\NodeType; use RZ\Roadiz\CoreBundle\Entity\Translation; use RZ\Roadiz\CoreBundle\Repository\NodesSourcesRepository; -use RZ\Roadiz\CoreBundle\SearchEngine\SolariumLogger; use RZ\Roadiz\CoreBundle\Webhook\Message\GenericJsonPostMessage; use RZ\Roadiz\CoreBundle\Webhook\Message\GitlabPipelineTriggerMessage; use RZ\Roadiz\CoreBundle\Webhook\Message\NetlifyBuildHookMessage; @@ -57,18 +59,21 @@ public function load(array $configs, ContainerBuilder $container): void $container->setParameter('roadiz_core.app_namespace', $config['appNamespace']); $container->setParameter('roadiz_core.app_version', $config['appVersion']); - $container->setParameter('roadiz_core.use_gravatar', $config['useGravatar']); $container->setParameter('roadiz_core.health_check_token', $config['healthCheckToken']); $container->setParameter('roadiz_core.inheritance_type', $config['inheritance']['type']); $container->setParameter('roadiz_core.max_versions_showed', $config['maxVersionsShowed']); $container->setParameter('roadiz_core.static_domain_name', $config['staticDomainName'] ?? ''); + $container->setParameter('roadiz_core.private_key_name', $config['security']['private_key_name']); + $container->setParameter('roadiz_core.private_key_dir', $config['security']['private_key_dir']); + $container->setParameter( + 'roadiz_core.private_key_path', + $config['security']['private_key_dir'] . DIRECTORY_SEPARATOR . $config['security']['private_key_name'] + ); $container->setParameter('roadiz_core.default_node_source_controller', $config['defaultNodeSourceController']); $container->setParameter('roadiz_core.use_native_json_column_type', $config['useNativeJsonColumnType']); $container->setParameter('roadiz_core.use_typed_node_names', $config['useTypedNodeNames']); $container->setParameter('roadiz_core.hide_roadiz_version', $config['hideRoadizVersion']); $container->setParameter('roadiz_core.use_accept_language_header', $config['useAcceptLanguageHeader']); - $container->setParameter('roadiz_core.web_response_class', $config['webResponseClass']); - $container->setParameter('roadiz_core.preview_required_role_name', $config['previewRequiredRoleName']); /* * Assets config @@ -80,11 +85,9 @@ public function load(array $configs, ContainerBuilder $container): void $container->setParameter('roadiz_core.assets_processing.supports_webp', false); } - /** @var string $projectDir */ - $projectDir = $container->getParameter('kernel.project_dir'); $container->setParameter( 'roadiz_core.documents_lib_dir', - $projectDir . DIRECTORY_SEPARATOR . trim($config['documentsLibDir'], "/ \t\n\r\0\x0B") + $container->getParameter('kernel.project_dir') . DIRECTORY_SEPARATOR . trim($config['documentsLibDir'], "/ \t\n\r\0\x0B") ); /* * Media config @@ -125,6 +128,31 @@ public function load(array $configs, ContainerBuilder $container): void $this->registerReverseProxyCache($config, $container); $this->registerSolr($config, $container); $this->registerMarkdown($config, $container); + $this->registerCrypto($config, $container); + } + + private function registerCrypto(array $config, ContainerBuilder $container): void + { + $container->setDefinition( + UniqueKeyEncoderFactory::class, + (new Definition()) + ->setClass(UniqueKeyEncoderFactory::class) + ->setPublic(true) + ->setArguments([ + new Reference(KeyChainInterface::class), + $container->getParameter('roadiz_core.private_key_name') + ]) + ); + + $container->setDefinition( + KeyChainInterface::class, + (new Definition()) + ->setClass(AsymmetricFilesystemKeyChain::class) + ->setPublic(true) + ->setArguments([ + $container->getParameter('roadiz_core.private_key_dir') + ]) + ); } private function registerReverseProxyCache(array $config, ContainerBuilder $container): void @@ -230,7 +258,6 @@ private function registerSolr(array $config, ContainerBuilder $container): void } } if (count($solrEndpoints) > 0) { - $logger = new Reference(SolariumLogger::class); $container->setDefinition( 'roadiz_core.solr.client', (new Definition()) @@ -242,7 +269,6 @@ private function registerSolr(array $config, ContainerBuilder $container): void new Reference('roadiz_core.solr.adapter'), new Reference(EventDispatcherInterface::class) ]) - ->addMethodCall('registerPlugin', ['roadiz_core.solr.client.logger', $logger]) ->addMethodCall('setEndpoints', [array_map(function (string $endpointId) { return new Reference($endpointId); }, $solrEndpoints)]) @@ -260,23 +286,21 @@ private function registerMarkdown(array $config, ContainerBuilder $container): v 'noreferrer' => 'external', ] ]); - /** @var array $defaultConfig */ - $defaultConfig = $container->getParameter('roadiz_core.markdown_config_default'); $container->setParameter( 'roadiz_core.markdown_config_text_converter', - array_merge($defaultConfig, [ + array_merge($container->getParameter('roadiz_core.markdown_config_default'), [ 'html_input' => 'allow' ]) ); $container->setParameter( 'roadiz_core.markdown_config_text_extra_converter', - array_merge($defaultConfig, [ + array_merge($container->getParameter('roadiz_core.markdown_config_default'), [ 'html_input' => 'allow' ]) ); $container->setParameter( 'roadiz_core.markdown_config_line_converter', - array_merge($defaultConfig, [ + array_merge($container->getParameter('roadiz_core.markdown_config_default'), [ 'html_input' => 'escape' ]) ); diff --git a/src/Doctrine/Event/FilterQueryBuilderEvent.php b/src/Doctrine/Event/FilterQueryBuilderEvent.php index 202811ca..983b74a8 100644 --- a/src/Doctrine/Event/FilterQueryBuilderEvent.php +++ b/src/Doctrine/Event/FilterQueryBuilderEvent.php @@ -23,7 +23,7 @@ abstract class FilterQueryBuilderEvent extends Event /** * @param QueryBuilder $queryBuilder - * @param class-string $entityClass + * @param string $entityClass */ public function __construct(QueryBuilder $queryBuilder, string $entityClass) { @@ -51,10 +51,10 @@ public function setQueryBuilder(QueryBuilder $queryBuilder) /** - * @param class-string $entityClass + * @param string $entityClass * @return bool */ - public function supports(string $entityClass): bool + public function supports($entityClass): bool { return $this->entityClass === $entityClass; } diff --git a/src/Doctrine/Event/QueryEvent.php b/src/Doctrine/Event/QueryEvent.php index dc85f161..b164dcbd 100644 --- a/src/Doctrine/Event/QueryEvent.php +++ b/src/Doctrine/Event/QueryEvent.php @@ -18,7 +18,7 @@ class QueryEvent extends Event /** * @param Query $query - * @param class-string $entityClass + * @param string $entityClass */ public function __construct(Query $query, string $entityClass) { @@ -35,7 +35,7 @@ public function getQuery(): Query } /** - * @return class-string + * @return string */ public function getEntityClass(): string { diff --git a/src/Doctrine/Event/QueryNodesSourcesEvent.php b/src/Doctrine/Event/QueryNodesSourcesEvent.php index 03d2c385..f4dc8a38 100644 --- a/src/Doctrine/Event/QueryNodesSourcesEvent.php +++ b/src/Doctrine/Event/QueryNodesSourcesEvent.php @@ -16,7 +16,7 @@ final class QueryNodesSourcesEvent extends QueryEvent /** * @param Query $query - * @param class-string $actualEntityName + * @param string $actualEntityName */ public function __construct(Query $query, string $actualEntityName) { @@ -25,7 +25,7 @@ public function __construct(Query $query, string $actualEntityName) } /** - * @return class-string + * @return string */ public function getActualEntityName(): string { diff --git a/src/Doctrine/EventSubscriber/AttributeValueLifeCycleSubscriber.php b/src/Doctrine/EventSubscriber/AttributeValueLifeCycleSubscriber.php index 4e7ada54..9e0b1e5e 100644 --- a/src/Doctrine/EventSubscriber/AttributeValueLifeCycleSubscriber.php +++ b/src/Doctrine/EventSubscriber/AttributeValueLifeCycleSubscriber.php @@ -32,13 +32,6 @@ public function prePersist(LifecycleEventArgs $event): void { $entity = $event->getObject(); if ($entity instanceof AttributeValueInterface) { - if ( - null !== $entity->getAttribute() && - null !== $entity->getAttribute()->getDefaultRealm() - ) { - $entity->setRealm($entity->getAttribute()->getDefaultRealm()); - } - /* * Automatically set position only if not manually set before. */ diff --git a/src/Doctrine/EventSubscriber/SettingLifeCycleSubscriber.php b/src/Doctrine/EventSubscriber/SettingLifeCycleSubscriber.php new file mode 100644 index 00000000..96e5a485 --- /dev/null +++ b/src/Doctrine/EventSubscriber/SettingLifeCycleSubscriber.php @@ -0,0 +1,123 @@ +uniqueKeyEncoderFactory = $uniqueKeyEncoderFactory; + $this->privateKeyName = $privateKeyName; + $this->logger = $logger; + } + + /** + * {@inheritdoc} + */ + public function getSubscribedEvents(): array + { + return [ + Events::preUpdate, + Events::postLoad + ]; + } + + /** + * @param PreUpdateEventArgs $event + * @throws InvalidKey + */ + public function preUpdate(PreUpdateEventArgs $event): void + { + $setting = $event->getObject(); + if ($setting instanceof Setting) { + if ( + $event->hasChangedField('encrypted') && + $event->getNewValue('encrypted') === false && + null !== $setting->getRawValue() + ) { + /* + * Set raw value and do not encode it if setting is not encrypted anymore. + */ + $setting->setValue($setting->getRawValue()); + } elseif ( + $event->hasChangedField('encrypted') && + $event->getNewValue('encrypted') === true && + null !== $setting->getRawValue() + ) { + /* + * Encode value for the first time. + */ + $setting->setValue($this->getEncoder()->encode(new HiddenString($setting->getRawValue()))); + } elseif ( + $setting->isEncrypted() && + $event->hasChangedField('value') && + null !== $event->getNewValue('value') + ) { + /* + * Encode setting if value has changed + */ + $event->setNewValue('value', $this->getEncoder()->encode(new HiddenString($event->getNewValue('value')))); + $setting->setClearValue($event->getNewValue('value')); + } + } + } + + /** + * @param LifecycleEventArgs $event + */ + public function postLoad(LifecycleEventArgs $event): void + { + $setting = $event->getObject(); + if ( + $setting instanceof Setting && + $setting->isEncrypted() && + null !== $setting->getRawValue() + ) { + try { + $setting->setClearValue($this->getEncoder()->decode($setting->getRawValue())->getString()); + } catch (InvalidKey $exception) { + $this->logger->error( + sprintf('Failed to decode "%s" setting value', $setting->getName()), + [ + 'exception_message' => $exception->getMessage(), + 'trace' => $exception->getTraceAsString(), + ] + ); + } catch (InvalidMessage $exception) { + $this->logger->error( + sprintf('Failed to decode "%s" setting value', $setting->getName()), + [ + 'exception_message' => $exception->getMessage(), + 'trace' => $exception->getTraceAsString(), + ] + ); + } + } + } + + /** + * @throws InvalidKey + */ + protected function getEncoder(): UniqueKeyEncoderInterface + { + return $this->uniqueKeyEncoderFactory->getEncoder($this->privateKeyName); + } +} diff --git a/src/Doctrine/EventSubscriber/UserLifeCycleSubscriber.php b/src/Doctrine/EventSubscriber/UserLifeCycleSubscriber.php index 0e3b98f8..eb342cdb 100644 --- a/src/Doctrine/EventSubscriber/UserLifeCycleSubscriber.php +++ b/src/Doctrine/EventSubscriber/UserLifeCycleSubscriber.php @@ -24,13 +24,27 @@ final class UserLifeCycleSubscriber implements EventSubscriber { + private UserViewer $userViewer; + private EventDispatcherInterface $dispatcher; + private PasswordHasherFactoryInterface $passwordHasherFactory; + private LoggerInterface $logger; + + /** + * @param UserViewer $userViewer + * @param EventDispatcherInterface $dispatcher + * @param PasswordHasherFactoryInterface $passwordHasherFactory + * @param LoggerInterface $logger + */ public function __construct( - private readonly UserViewer $userViewer, - private readonly EventDispatcherInterface $dispatcher, - private readonly PasswordHasherFactoryInterface $passwordHasherFactory, - private readonly LoggerInterface $logger, - private readonly bool $useGravatar + UserViewer $userViewer, + EventDispatcherInterface $dispatcher, + PasswordHasherFactoryInterface $passwordHasherFactory, + LoggerInterface $logger ) { + $this->userViewer = $userViewer; + $this->dispatcher = $dispatcher; + $this->logger = $logger; + $this->passwordHasherFactory = $passwordHasherFactory; } /** @@ -80,11 +94,9 @@ public function preUpdate(PreUpdateEventArgs $event): void $user->setPictureUrl($url); } catch (\Exception $e) { $user->setFacebookName(''); - if ($this->useGravatar) { - $user->setPictureUrl($user->getGravatarUrl()); - } + $user->setPictureUrl($user->getGravatarUrl()); } - } elseif ($this->useGravatar) { + } else { $user->setPictureUrl($user->getGravatarUrl()); } } @@ -156,7 +168,8 @@ public function postPersist(LifecycleEventArgs $event): void /** * @param LifecycleEventArgs $event - * @throws \Throwable + * + * @throws \Exception */ public function prePersist(LifecycleEventArgs $event): void { @@ -176,8 +189,8 @@ public function prePersist(LifecycleEventArgs $event): void $user->setPasswordRequestedAt(new \DateTime()); $user->setConfirmationToken($tokenGenerator->generateToken()); + $this->userViewer->setUser($user); $this->userViewer->sendPasswordResetLink( - $user, 'loginResetPage', '@RoadizCore/email/users/welcome_user_email.html.twig', '@RoadizCore/email/users/welcome_user_email.txt.twig' @@ -189,7 +202,7 @@ public function prePersist(LifecycleEventArgs $event): void /* * Force a Gravatar image if not defined */ - if (empty($user->getPictureUrl()) && $this->useGravatar) { + if (empty($user->getPictureUrl())) { $user->setPictureUrl($user->getGravatarUrl()); } } diff --git a/src/Doctrine/ORM/Filter/ANodesFilter.php b/src/Doctrine/ORM/Filter/ANodesFilter.php index ebfcb49e..8548be74 100644 --- a/src/Doctrine/ORM/Filter/ANodesFilter.php +++ b/src/Doctrine/ORM/Filter/ANodesFilter.php @@ -40,6 +40,14 @@ protected function getNodeJoinAlias(): string return 'a_n'; } + /** + * @return string + */ + protected function getNodeFieldJoinAlias(): string + { + return 'a_n_f'; + } + /** * @param QueryBuilderBuildEvent $event */ @@ -64,9 +72,24 @@ public function onNodeQueryBuilderBuild(QueryBuilderBuildEvent $event): void $this->getNodeJoinAlias() ); } - - $prefix = $this->getNodeJoinAlias() . '.'; - $key = str_replace($this->getProperty() . '.', '', $event->getProperty()); + if (str_contains($event->getProperty(), $this->getProperty() . '.field.')) { + if ( + !$simpleQB->joinExists( + $simpleQB->getRootAlias(), + $this->getNodeFieldJoinAlias() + ) + ) { + $qb->innerJoin( + $this->getNodeJoinAlias() . '.field', + $this->getNodeFieldJoinAlias() + ); + } + $prefix = $this->getNodeFieldJoinAlias() . '.'; + $key = str_replace($this->getProperty() . '.field.', '', $event->getProperty()); + } else { + $prefix = $this->getNodeJoinAlias() . '.'; + $key = str_replace($this->getProperty() . '.', '', $event->getProperty()); + } $qb->andWhere($simpleQB->buildExpressionWithoutBinding($event->getValue(), $prefix, $key, $baseKey)); } @@ -109,9 +132,24 @@ public function onNodesSourcesQueryBuilderBuild(QueryBuilderNodesSourcesBuildEve $this->getNodeJoinAlias() ); } - - $prefix = $this->getNodeJoinAlias() . '.'; - $key = str_replace('node.' . $this->getProperty() . '.', '', $event->getProperty()); + if (str_contains($event->getProperty(), 'node.' . $this->getProperty() . '.field.')) { + if ( + !$simpleQB->joinExists( + $simpleQB->getRootAlias(), + $this->getNodeFieldJoinAlias() + ) + ) { + $qb->innerJoin( + $this->getNodeJoinAlias() . '.field', + $this->getNodeFieldJoinAlias() + ); + } + $prefix = $this->getNodeFieldJoinAlias() . '.'; + $key = str_replace('node.' . $this->getProperty() . '.field.', '', $event->getProperty()); + } else { + $prefix = $this->getNodeJoinAlias() . '.'; + $key = str_replace('node.' . $this->getProperty() . '.', '', $event->getProperty()); + } $qb->andWhere($simpleQB->buildExpressionWithoutBinding($event->getValue(), $prefix, $key, $baseKey)); } diff --git a/src/Doctrine/ORM/Filter/BNodesFilter.php b/src/Doctrine/ORM/Filter/BNodesFilter.php index 8cb6f894..032b9e7a 100644 --- a/src/Doctrine/ORM/Filter/BNodesFilter.php +++ b/src/Doctrine/ORM/Filter/BNodesFilter.php @@ -35,4 +35,12 @@ protected function getNodeJoinAlias(): string { return 'b_n'; } + + /** + * @return string + */ + protected function getNodeFieldJoinAlias(): string + { + return 'b_n_f'; + } } diff --git a/src/Doctrine/ORM/SimpleQueryBuilder.php b/src/Doctrine/ORM/SimpleQueryBuilder.php index 90c7713b..eed46053 100644 --- a/src/Doctrine/ORM/SimpleQueryBuilder.php +++ b/src/Doctrine/ORM/SimpleQueryBuilder.php @@ -55,7 +55,7 @@ public function buildExpressionWithBinding($value, string $prefix, string $key): * * @return Comparison|Func|string */ - public function buildExpressionWithoutBinding(mixed $value, string $prefix, string $key, string $baseKey = null) + public function buildExpressionWithoutBinding($value, string $prefix, string $key, string $baseKey = null) { if (\mb_strlen($prefix) > 0 && \mb_substr($prefix, -\mb_strlen('.')) !== '.') { $prefix .= '.'; @@ -127,11 +127,14 @@ public function buildExpressionWithoutBinding(mixed $value, string $prefix, stri if ($value instanceof PersistableInterface) { return $this->queryBuilder->expr()->eq($prefix . $key, ':' . $baseKey); } + if (isset($value)) { + return $this->queryBuilder->expr()->eq($prefix . $key, ':' . $baseKey); + } if (null === $value) { return $this->queryBuilder->expr()->isNull($prefix . $key); } - return $this->queryBuilder->expr()->eq($prefix . $key, ':' . $baseKey); + throw new \InvalidArgumentException('Value is not supported for expression.'); } /** @@ -178,11 +181,14 @@ public function bindValue(string $key, $value): QueryBuilder if ($value instanceof PersistableInterface) { return $this->queryBuilder->setParameter($key, $value->getId()); } + if (isset($value)) { + return $this->queryBuilder->setParameter($key, $value); + } if (null === $value) { return $this->queryBuilder; } - return $this->queryBuilder->setParameter($key, $value); + throw new \InvalidArgumentException('Value is not supported for binding.'); } /** diff --git a/src/Doctrine/SchemaUpdater.php b/src/Doctrine/SchemaUpdater.php index a176b299..7a7ea7a2 100644 --- a/src/Doctrine/SchemaUpdater.php +++ b/src/Doctrine/SchemaUpdater.php @@ -98,15 +98,23 @@ public function updateSchema(): void */ public function updateNodeTypesSchema(): void { - $this->clearMetadata(); + /* + * Execute pending application migrations + */ + $this->updateSchema(); + + /* + * Update schema with new node-types + * without creating any migration + */ $process = $this->runCommand( - 'doctrine:migrations:diff', - '--namespace=DoctrineMigrations --quiet --allow-empty-diff', + 'doctrine:schema:update', + '--dump-sql --force', ); $process->run(); + if ($process->wait() === 0) { - $this->logger->info('New migration has been generated.'); - $this->updateSchema(); + $this->logger->info('DB schema has been updated.'); } else { throw new \RuntimeException('DB schema update failed. ' . $process->getErrorOutput()); } diff --git a/src/Document/EventSubscriber/DocumentMessageDispatchSubscriber.php b/src/Document/EventSubscriber/DocumentMessageDispatchSubscriber.php index 8dfb1350..6abfdccf 100644 --- a/src/Document/EventSubscriber/DocumentMessageDispatchSubscriber.php +++ b/src/Document/EventSubscriber/DocumentMessageDispatchSubscriber.php @@ -49,19 +49,18 @@ public function onFilterDocumentEvent(FilterDocumentEvent $event): void $document = $event->getDocument(); if ( $document instanceof Document && - \is_numeric($document->getId()) && + null !== $document->getId() && $document->isLocal() && null !== $document->getRelativePath() ) { - $id = (int) $document->getId(); - $this->bus->dispatch(new Envelope(new DocumentRawMessage($id))); - $this->bus->dispatch(new Envelope(new DocumentFilesizeMessage($id))); - $this->bus->dispatch(new Envelope(new DocumentSizeMessage($id))); - $this->bus->dispatch(new Envelope(new DocumentAverageColorMessage($id))); - $this->bus->dispatch(new Envelope(new DocumentExifMessage($id))); - $this->bus->dispatch(new Envelope(new DocumentSvgMessage($id))); - $this->bus->dispatch(new Envelope(new DocumentAudioVideoMessage($id))); - $this->bus->dispatch(new Envelope(new DocumentPdfMessage($id))); + $this->bus->dispatch(new Envelope(new DocumentRawMessage($document->getId()))); + $this->bus->dispatch(new Envelope(new DocumentFilesizeMessage($document->getId()))); + $this->bus->dispatch(new Envelope(new DocumentSizeMessage($document->getId()))); + $this->bus->dispatch(new Envelope(new DocumentAverageColorMessage($document->getId()))); + $this->bus->dispatch(new Envelope(new DocumentExifMessage($document->getId()))); + $this->bus->dispatch(new Envelope(new DocumentSvgMessage($document->getId()))); + $this->bus->dispatch(new Envelope(new DocumentAudioVideoMessage($document->getId()))); + $this->bus->dispatch(new Envelope(new DocumentPdfMessage($document->getId()))); } } } diff --git a/src/Document/MessageHandler/DocumentAudioVideoMessageHandler.php b/src/Document/MessageHandler/DocumentAudioVideoMessageHandler.php index 92830140..a9393dc0 100644 --- a/src/Document/MessageHandler/DocumentAudioVideoMessageHandler.php +++ b/src/Document/MessageHandler/DocumentAudioVideoMessageHandler.php @@ -67,18 +67,12 @@ protected function processMessage(AbstractDocumentMessage $message, DocumentInte * This process requires document files to be locally stored! */ $videoPath = \tempnam(\sys_get_temp_dir(), 'video_'); - if (false === $videoPath) { - throw new UnrecoverableMessageHandlingException('Unable to create temporary file for video processing.'); - } \rename($videoPath, $videoPath .= $document->getFilename()); /* * Copy AV locally */ $videoPathResource = \fopen($videoPath, 'w'); - if (false === $videoPathResource) { - throw new UnrecoverableMessageHandlingException('Unable to open temporary file for video processing.'); - } \stream_copy_to_stream($this->documentsStorage->readStream($document->getMountPath()), $videoPathResource); \fclose($videoPathResource); @@ -120,9 +114,6 @@ protected function extractMediaThumbnail(DocumentInterface $document, string $lo } $thumbnailPath = \tempnam(\sys_get_temp_dir(), 'thumbnail_'); - if (false === $thumbnailPath) { - throw new UnrecoverableMessageHandlingException('Unable to create temporary file for thumbnail processing.'); - } \rename($thumbnailPath, $thumbnailPath .= '.jpg'); $process = new Process([$this->ffmpegPath, '-y', '-i', $localMediaPath, '-vframes', '1', $thumbnailPath]); diff --git a/src/Document/MessageHandler/DocumentPdfMessageHandler.php b/src/Document/MessageHandler/DocumentPdfMessageHandler.php index 292de562..1a8340e3 100644 --- a/src/Document/MessageHandler/DocumentPdfMessageHandler.php +++ b/src/Document/MessageHandler/DocumentPdfMessageHandler.php @@ -51,18 +51,12 @@ protected function processMessage(AbstractDocumentMessage $message, DocumentInte * This process requires document files to be locally stored! */ $pdfPath = \tempnam(\sys_get_temp_dir(), 'pdf_'); - if (false === $pdfPath) { - throw new UnrecoverableMessageHandlingException('Cannot create temporary file for PDF thumbnail.'); - } \rename($pdfPath, $pdfPath .= $document->getFilename()); /* * Copy AV locally */ $pdfPathResource = \fopen($pdfPath, 'w'); - if (false === $pdfPathResource) { - throw new UnrecoverableMessageHandlingException('Cannot open temporary file for PDF thumbnail.'); - } \stream_copy_to_stream($this->documentsStorage->readStream($document->getMountPath()), $pdfPathResource); \fclose($pdfPathResource); @@ -81,9 +75,6 @@ protected function extractPdfThumbnail(DocumentInterface $document, string $loca } $thumbnailPath = \tempnam(\sys_get_temp_dir(), 'thumbnail_'); - if (false === $thumbnailPath) { - throw new UnrecoverableMessageHandlingException('Cannot create temporary file for PDF thumbnail.'); - } \rename($thumbnailPath, $thumbnailPath .= $document->getFilename() . '.jpg'); try { diff --git a/src/Entity/AbstractDateTimedPositioned.php b/src/Entity/AbstractDateTimedPositioned.php deleted file mode 100644 index 9f33830a..00000000 --- a/src/Entity/AbstractDateTimedPositioned.php +++ /dev/null @@ -1,42 +0,0 @@ - 0]), - Serializer\Type("integer"), - Serializer\Groups(["attribute", "node", "nodes_sources"]), - SymfonySerializer\Groups(["attribute", "node", "nodes_sources"]), - ApiFilter(OrderFilter::class), - Range(min: 0, max: 9999), - NotNull, - ] - protected int $weight = 0; - public function __construct() { $this->attributeTranslations = new ArrayCollection(); @@ -109,28 +76,6 @@ public function setAttributeDocuments(Collection $attributeDocuments): Attribute return $this; } - public function getDefaultRealm(): ?RealmInterface - { - return $this->defaultRealm; - } - - public function setDefaultRealm(?RealmInterface $defaultRealm): Attribute - { - $this->defaultRealm = $defaultRealm; - return $this; - } - - public function getWeight(): int - { - return $this->weight; - } - - public function setWeight(?int $weight): Attribute - { - $this->weight = $weight ?? 0; - return $this; - } - /** * @return Collection */ diff --git a/src/Entity/AttributeDocuments.php b/src/Entity/AttributeDocuments.php index 596a3947..fed01b4b 100644 --- a/src/Entity/AttributeDocuments.php +++ b/src/Entity/AttributeDocuments.php @@ -32,13 +32,12 @@ class AttributeDocuments extends AbstractPositioned ORM\JoinColumn( name: "attribute_id", referencedColumnName: "id", - nullable: false, onDelete: "CASCADE" ), Serializer\Exclude(), SymfonySerializer\Ignore() ] - protected Attribute $attribute; + protected ?Attribute $attribute = null; #[ ORM\ManyToOne( @@ -50,45 +49,72 @@ class AttributeDocuments extends AbstractPositioned ORM\JoinColumn( name: "document_id", referencedColumnName: "id", - nullable: false, onDelete: "CASCADE" ), Serializer\Groups(["attribute"]), SymfonySerializer\Groups(["attribute"]), Serializer\Type(Document::class) ] - protected Document $document; + protected ?Document $document = null; - public function __construct(Attribute $attribute, Document $document) + /** + * @param Attribute|null $attribute + * @param Document|null $document + */ + public function __construct(Attribute $attribute = null, Document $document = null) { $this->document = $document; $this->attribute = $attribute; } + /** + * + */ public function __clone() { if ($this->id) { $this->id = null; + $this->attribute = null; } } - public function getDocument(): Document + /** + * Gets the value of document. + * + * @return Document|null + */ + public function getDocument(): ?Document { return $this->document; } - public function setDocument(Document $document): AttributeDocuments + /** + * Sets the value of document. + * + * @param Document|null $document the document + * + * @return AttributeDocuments + */ + public function setDocument(?Document $document): AttributeDocuments { $this->document = $document; + return $this; } - public function getAttribute(): Attribute + /** + * @return Attribute|null + */ + public function getAttribute(): ?Attribute { return $this->attribute; } - public function setAttribute(Attribute $attribute): AttributeDocuments + /** + * @param Attribute|null $attribute + * @return AttributeDocuments + */ + public function setAttribute(?Attribute $attribute): AttributeDocuments { $this->attribute = $attribute; return $this; diff --git a/src/Entity/AttributeValue.php b/src/Entity/AttributeValue.php index f5df8d02..59df1d87 100644 --- a/src/Entity/AttributeValue.php +++ b/src/Entity/AttributeValue.php @@ -4,9 +4,9 @@ namespace RZ\Roadiz\CoreBundle\Entity; -use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter as BaseFilter; +use ApiPlatform\Core\Serializer\Filter\PropertyFilter; use ApiPlatform\Metadata\ApiFilter; -use ApiPlatform\Serializer\Filter\PropertyFilter; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; @@ -15,7 +15,6 @@ use RZ\Roadiz\CoreBundle\Model\AttributeValueInterface; use RZ\Roadiz\CoreBundle\Model\AttributeValueTrait; use RZ\Roadiz\CoreBundle\Model\AttributeValueTranslationInterface; -use RZ\Roadiz\CoreBundle\Model\RealmInterface; use RZ\Roadiz\CoreBundle\Repository\AttributeValueRepository; use Symfony\Component\Serializer\Annotation as SymfonySerializer; @@ -23,48 +22,32 @@ ORM\Entity(repositoryClass: AttributeValueRepository::class), ORM\Table(name: "attribute_values"), ORM\Index(columns: ["attribute_id", "node_id"]), - ORM\Index(columns: ["node_id", "position"], name: "idx_attribute_value_node_position"), - ORM\Index(columns: ["position"], name: "idx_attribute_value_position"), ORM\HasLifecycleCallbacks, - ApiFilter(PropertyFilter::class), - ApiFilter(BaseFilter\OrderFilter::class, properties: [ - "position", - ]), + ApiFilter(PropertyFilter::class) ] class AttributeValue extends AbstractPositioned implements AttributeValueInterface { use AttributeValueTrait; + /** + * @var Node|null + */ #[ ORM\ManyToOne(targetEntity: Node::class, inversedBy: "attributeValues"), - ORM\JoinColumn(name: "node_id", referencedColumnName: "id", nullable: false, onDelete: "CASCADE"), + ORM\JoinColumn(name: "node_id", onDelete: "CASCADE"), Serializer\Groups(["attribute_node"]), SymfonySerializer\Groups(["attribute_node"]), SymfonySerializer\MaxDepth(1), ApiFilter(BaseFilter\SearchFilter::class, properties: [ "node" => "exact", "node.id" => "exact", - "node.nodeName" => "exact", - "node.nodeType" => "exact", - "node.nodeType.name" => "exact" + "node.nodeName" => "exact" ]), ApiFilter(BaseFilter\BooleanFilter::class, properties: [ "node.visible" ]) ] - protected Node $node; - - #[ORM\ManyToOne(targetEntity: Realm::class)] - #[ORM\JoinColumn( - name: 'realm_id', - referencedColumnName: 'id', - unique: false, - nullable: true, - onDelete: 'SET NULL' - )] - #[SymfonySerializer\Ignore] - #[Serializer\Exclude] - private ?RealmInterface $realm = null; + protected ?Node $node = null; public function __construct() { @@ -82,7 +65,10 @@ public function getPosition(): float return $this->position; } - public function getAttributable(): Node + /** + * @inheritDoc + */ + public function getAttributable(): ?AttributableInterface { return $this->node; } @@ -92,36 +78,33 @@ public function getAttributable(): Node */ public function setAttributable(?AttributableInterface $attributable) { - if ($attributable instanceof Node) { + if (null === $attributable || $attributable instanceof Node) { $this->node = $attributable; return $this; } throw new \InvalidArgumentException('Attributable have to be an instance of Node.'); } - public function getNode(): Node + /** + * @return Node|null + */ + public function getNode(): ?Node { return $this->node; } - public function setNode(Node $node): AttributeValue + /** + * @param Node|null $node + * + * @return AttributeValue + */ + public function setNode(?Node $node): AttributeValue { $this->node = $node; return $this; } - public function getRealm(): ?RealmInterface - { - return $this->realm; - } - - public function setRealm(?RealmInterface $realm): AttributeValue - { - $this->realm = $realm; - return $this; - } - /** * After clone method. * @@ -132,12 +115,14 @@ public function __clone() if ($this->id) { $this->id = null; $attributeValueTranslations = $this->getAttributeValueTranslations(); - $this->attributeValueTranslations = new ArrayCollection(); - /** @var AttributeValueTranslationInterface $attributeValueTranslation */ - foreach ($attributeValueTranslations as $attributeValueTranslation) { - $cloneAttributeValueTranslation = clone $attributeValueTranslation; - $cloneAttributeValueTranslation->setAttributeValue($this); - $this->attributeValueTranslations->add($cloneAttributeValueTranslation); + if ($attributeValueTranslations !== null) { + $this->attributeValueTranslations = new ArrayCollection(); + /** @var AttributeValueTranslationInterface $attributeValueTranslation */ + foreach ($attributeValueTranslations as $attributeValueTranslation) { + $cloneAttributeValueTranslation = clone $attributeValueTranslation; + $cloneAttributeValueTranslation->setAttributeValue($this); + $this->attributeValueTranslations->add($cloneAttributeValueTranslation); + } } } } diff --git a/src/Entity/CustomForm.php b/src/Entity/CustomForm.php index 9f91c796..3194e69b 100644 --- a/src/Entity/CustomForm.php +++ b/src/Entity/CustomForm.php @@ -26,22 +26,19 @@ ORM\Entity(repositoryClass: CustomFormRepository::class), ORM\Table(name: "custom_forms"), ORM\HasLifecycleCallbacks, - UniqueEntity(fields: ["name"]), - ORM\Index(columns: ["created_at"], name: "custom_form_created_at"), - ORM\Index(columns: ["updated_at"], name: "custom_form_updated_at"), + UniqueEntity(fields: ["name"]) ] class CustomForm extends AbstractDateTimed { #[ - ORM\Column(name: "color", type: "string", length: 7, unique: false, nullable: true), + ORM\Column(name: "color", type: "string", unique: false, nullable: true), Serializer\Groups(["custom_form", "nodes_sources"]), - Assert\Length(max: 7), SymfonySerializer\Ignore() ] protected ?string $color = '#000000'; #[ - ORM\Column(type: "string", length: 250, unique: true), + ORM\Column(type: "string", unique: true), Serializer\Groups(["custom_form", "nodes_sources"]), SymfonySerializer\Groups(["custom_form", "nodes_sources"]), Assert\NotNull(), @@ -52,7 +49,7 @@ class CustomForm extends AbstractDateTimed private string $name = 'Untitled'; #[ - ORM\Column(name: "display_name", type: "string", length: 250), + ORM\Column(name: "display_name", type: "string"), Serializer\Groups(["custom_form", "nodes_sources"]), SymfonySerializer\Groups(["custom_form", "nodes_sources"]), Assert\NotNull(), @@ -82,7 +79,6 @@ class CustomForm extends AbstractDateTimed ORM\Column(type: "string", length: 15, nullable: true), Serializer\Groups(["custom_form"]), SymfonySerializer\Groups(["custom_form"]), - Assert\Length(max: 15), SymfonySerializer\Ignore() ] private ?string $retentionTime = null; @@ -108,12 +104,7 @@ class CustomForm extends AbstractDateTimed * @var Collection */ #[ - ORM\OneToMany( - mappedBy: "customForm", - targetEntity: CustomFormField::class, - cascade: ["ALL"], - orphanRemoval: true - ), + ORM\OneToMany(mappedBy: "customForm", targetEntity: CustomFormField::class, cascade: ["ALL"]), ORM\OrderBy(["position" => "ASC"]), Serializer\Groups(["custom_form"]), SymfonySerializer\Groups(["custom_form"]), @@ -128,8 +119,7 @@ class CustomForm extends AbstractDateTimed ORM\OneToMany( mappedBy: "customForm", targetEntity: CustomFormAnswer::class, - cascade: ["ALL"], - orphanRemoval: true + cascade: ["ALL"] ), Serializer\Exclude, SymfonySerializer\Ignore @@ -345,6 +335,7 @@ public function removeField(CustomFormField $field): CustomForm { if ($this->getFields()->contains($field)) { $this->getFields()->removeElement($field); + $field->setCustomForm(null); } return $this; diff --git a/src/Entity/CustomFormAnswer.php b/src/Entity/CustomFormAnswer.php index f73abec6..81b05aed 100644 --- a/src/Entity/CustomFormAnswer.php +++ b/src/Entity/CustomFormAnswer.php @@ -11,7 +11,6 @@ use RZ\Roadiz\CoreBundle\Repository\CustomFormAnswerRepository; use Symfony\Component\Serializer\Annotation as SymfonySerializer; use RZ\Roadiz\Core\AbstractEntities\AbstractEntity; -use Symfony\Component\Validator\Constraints as Assert; #[ ORM\Entity(repositoryClass: CustomFormAnswerRepository::class), @@ -23,10 +22,9 @@ class CustomFormAnswer extends AbstractEntity { #[ - ORM\Column(name: "ip", type: "string", length: 46, nullable: false), + ORM\Column(name: "ip", type: "string", nullable: false), Serializer\Groups(["custom_form_answer"]), - SymfonySerializer\Groups(["custom_form_answer"]), - Assert\Length(max: 46) + SymfonySerializer\Groups(["custom_form_answer"]) ] private string $ip = ''; @@ -44,8 +42,7 @@ class CustomFormAnswer extends AbstractEntity ORM\OneToMany( mappedBy: "customFormAnswer", targetEntity: CustomFormFieldAttribute::class, - cascade: ["ALL"], - orphanRemoval: true + cascade: ["ALL"] ), Serializer\Groups(["custom_form_answer"]), SymfonySerializer\Groups(["custom_form_answer"]) @@ -57,11 +54,11 @@ class CustomFormAnswer extends AbstractEntity targetEntity: CustomForm::class, inversedBy: "customFormAnswers" ), - ORM\JoinColumn(name: "custom_form_id", referencedColumnName: "id", nullable: false, onDelete: "CASCADE"), + ORM\JoinColumn(name: "custom_form_id", referencedColumnName: "id", onDelete: "CASCADE"), Serializer\Exclude, SymfonySerializer\Ignore ] - private CustomForm $customForm; + private ?CustomForm $customForm = null; public function __construct() { diff --git a/src/Entity/CustomFormField.php b/src/Entity/CustomFormField.php index d1c6ee20..f7b90f28 100644 --- a/src/Entity/CustomFormField.php +++ b/src/Entity/CustomFormField.php @@ -50,11 +50,11 @@ class CustomFormField extends AbstractField #[ ORM\ManyToOne(targetEntity: CustomForm::class, inversedBy: "fields"), - ORM\JoinColumn(name: "custom_form_id", referencedColumnName: "id", nullable: false, onDelete: "CASCADE"), + ORM\JoinColumn(name: "custom_form_id", referencedColumnName: "id", onDelete: "CASCADE"), Serializer\Exclude, SymfonySerializer\Ignore ] - private CustomForm $customForm; + private ?CustomForm $customForm = null; /** * @var Collection @@ -84,7 +84,7 @@ public function __construct() * * @return $this */ - public function setLabel($label): CustomFormField + public function setLabel($label) { parent::setLabel($label); $this->setName($label); @@ -92,15 +92,25 @@ public function setLabel($label): CustomFormField return $this; } - public function getCustomForm(): CustomForm + /** + * @return CustomForm|null + */ + public function getCustomForm(): ?CustomForm { return $this->customForm; } - public function setCustomForm(CustomForm $customForm): CustomFormField + /** + * @param CustomForm|null $customForm + * + * @return $this + */ + public function setCustomForm(CustomForm $customForm = null): CustomFormField { $this->customForm = $customForm; - $this->customForm->addField($this); + if (null !== $customForm) { + $this->customForm->addField($this); + } return $this; } @@ -152,6 +162,7 @@ public function __clone() { if ($this->id) { $this->id = null; + $this->customForm = null; $this->customFormFieldAttributes = new ArrayCollection(); } } diff --git a/src/Entity/CustomFormFieldAttribute.php b/src/Entity/CustomFormFieldAttribute.php index 65ec6285..63c441d1 100644 --- a/src/Entity/CustomFormFieldAttribute.php +++ b/src/Entity/CustomFormFieldAttribute.php @@ -24,15 +24,15 @@ class CustomFormFieldAttribute extends AbstractEntity { #[ ORM\ManyToOne(targetEntity: CustomFormAnswer::class, inversedBy: "answerFields"), - ORM\JoinColumn(name: "custom_form_answer_id", referencedColumnName: "id", nullable: false, onDelete: "CASCADE") + ORM\JoinColumn(name: "custom_form_answer_id", referencedColumnName: "id", onDelete: "CASCADE") ] - protected CustomFormAnswer $customFormAnswer; + protected ?CustomFormAnswer $customFormAnswer = null; #[ ORM\ManyToOne(targetEntity: CustomFormField::class, inversedBy: "customFormFieldAttributes"), - ORM\JoinColumn(name: "custom_form_field_id", referencedColumnName: "id", nullable: false, onDelete: "CASCADE") + ORM\JoinColumn(name: "custom_form_field_id", referencedColumnName: "id", onDelete: "CASCADE") ] - protected CustomFormField $customFormField; + protected ?CustomFormField $customFormField = null; /** * @var Collection @@ -83,11 +83,23 @@ public function setValue(?string $value): CustomFormFieldAttribute return $this; } - public function getCustomFormAnswer(): CustomFormAnswer + /** + * Gets the value of customFormAnswer. + * + * @return CustomFormAnswer|null + */ + public function getCustomFormAnswer(): ?CustomFormAnswer { return $this->customFormAnswer; } + /** + * Sets the value of customFormAnswer. + * + * @param CustomFormAnswer $customFormAnswer the custom form answer + * + * @return self + */ public function setCustomFormAnswer(CustomFormAnswer $customFormAnswer): CustomFormFieldAttribute { $this->customFormAnswer = $customFormAnswer; @@ -104,11 +116,20 @@ public function __toString(): string return $this->getValue() ?? ''; } - public function getCustomFormField(): CustomFormField + /** + * @return CustomFormField|null + */ + public function getCustomFormField(): ?CustomFormField { return $this->customFormField; } + /** + * Sets the value of customFormField. + * + * @param CustomFormField $customFormField the custom form field + * @return self + */ public function setCustomFormField(CustomFormField $customFormField): CustomFormFieldAttribute { $this->customFormField = $customFormField; diff --git a/src/Entity/Document.php b/src/Entity/Document.php index 2f8db769..7b7d4e46 100644 --- a/src/Entity/Document.php +++ b/src/Entity/Document.php @@ -4,10 +4,9 @@ namespace RZ\Roadiz\CoreBundle\Entity; -use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter as BaseFilter; +use ApiPlatform\Core\Serializer\Filter\PropertyFilter; use ApiPlatform\Metadata\ApiFilter; -use ApiPlatform\Metadata\ApiProperty; -use ApiPlatform\Serializer\Filter\PropertyFilter; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Criteria; @@ -19,6 +18,7 @@ use RZ\Roadiz\CoreBundle\Api\Filter\CopyrightValidFilter; use RZ\Roadiz\CoreBundle\Repository\DocumentRepository; use RZ\Roadiz\Documents\Models\AdvancedDocumentInterface; +use RZ\Roadiz\Documents\Models\DisplayableInterface; use RZ\Roadiz\Documents\Models\DocumentInterface; use RZ\Roadiz\Documents\Models\DocumentTrait; use RZ\Roadiz\Documents\Models\FileHashInterface; @@ -27,7 +27,6 @@ use RZ\Roadiz\Documents\Models\TimeableInterface; use RZ\Roadiz\Utils\StringHandler; use Symfony\Component\Serializer\Annotation as SymfonySerializer; -use Symfony\Component\Validator\Constraints as Assert; /** * Documents entity represent a file on server with datetime and naming. @@ -70,7 +69,7 @@ ]), ApiFilter(CopyrightValidFilter::class) ] -class Document extends AbstractDateTimed implements AdvancedDocumentInterface, HasThumbnailInterface, TimeableInterface, FileHashInterface +class Document extends AbstractDateTimed implements AdvancedDocumentInterface, HasThumbnailInterface, TimeableInterface, DisplayableInterface, FileHashInterface { use DocumentTrait; @@ -80,9 +79,6 @@ class Document extends AbstractDateTimed implements AdvancedDocumentInterface, H #[ORM\Column(name: 'copyright_valid_since', type: 'datetime', nullable: true)] #[SymfonySerializer\Groups(['document_copyright'])] #[Serializer\Groups(['document_copyright'])] - #[ApiProperty( - description: 'Document copyright starting date', - )] protected ?\DateTime $copyrightValidSince = null; /** @@ -91,42 +87,8 @@ class Document extends AbstractDateTimed implements AdvancedDocumentInterface, H #[ORM\Column(name: 'copyright_valid_until', type: 'datetime', nullable: true)] #[SymfonySerializer\Groups(['document_copyright'])] #[Serializer\Groups(['document_copyright'])] - #[ApiProperty( - description: 'Document copyright expiry date', - )] protected ?\DateTime $copyrightValidUntil = null; - /** - * @var string|null Image crop alignment. - * - * The possible values are: - * - * top-left - * top - * top-right - * left - * center (default) - * right - * bottom-left - * bottom - * bottom-right - */ - #[ORM\Column(name: 'image_crop_alignment', type: 'string', length: 12, nullable: true)] - #[SymfonySerializer\Ignore] - #[Assert\Length(max: 12)] - #[Assert\Choice(choices: [ - 'top-left', - 'top', - 'top-right', - 'left', - 'center', - 'right', - 'bottom-left', - 'bottom', - 'bottom-right' - ])] - protected ?string $imageCropAlignment = null; - #[ORM\ManyToOne( targetEntity: Document::class, cascade: ['all'], @@ -146,42 +108,30 @@ class Document extends AbstractDateTimed implements AdvancedDocumentInterface, H ] protected bool $raw = false; - #[ORM\Column(name: 'embedId', type: 'string', length: 250, unique: false, nullable: true)] + #[ORM\Column(name: 'embedId', type: 'string', unique: false, nullable: true)] #[SymfonySerializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Type("string")] - #[ApiProperty( - description: 'Embed ID on external platforms', - example: 'FORSwsjtQSE', - )] - #[Assert\Length(max: 250)] protected ?string $embedId = null; #[ORM\Column(name: 'file_hash', type: 'string', length: 64, unique: false, nullable: true)] #[SymfonySerializer\Ignore] #[Serializer\Exclude] #[Serializer\Type('string')] - #[Assert\Length(max: 64)] protected ?string $fileHash = null; #[ORM\Column(name: 'file_hash_algorithm', type: 'string', length: 15, unique: false, nullable: true)] #[SymfonySerializer\Ignore] #[Serializer\Exclude] #[Serializer\Type('string')] - #[Assert\Length(max: 15)] protected ?string $fileHashAlgorithm = null; #[ApiFilter(BaseFilter\SearchFilter::class, strategy: "exact")] #[ApiFilter(RoadizFilter\NotFilter::class)] - #[ORM\Column(name: 'embedPlatform', type: 'string', length: 100, unique: false, nullable: true)] + #[ORM\Column(name: 'embedPlatform', type: 'string', unique: false, nullable: true)] #[SymfonySerializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Type('string')] - #[Assert\Length(max: 100)] - #[ApiProperty( - description: 'Embed platform name', - example: 'youtube', - )] protected ?string $embedPlatform = null; /** * @var Collection @@ -224,6 +174,7 @@ class Document extends AbstractDateTimed implements AdvancedDocumentInterface, H #[ORM\OneToMany( mappedBy: 'document', targetEntity: DocumentTranslation::class, + fetch: 'EAGER', orphanRemoval: true )] #[SymfonySerializer\Ignore] @@ -234,26 +185,20 @@ class Document extends AbstractDateTimed implements AdvancedDocumentInterface, H * @var string|null */ #[ApiFilter(BaseFilter\SearchFilter::class, strategy: "partial")] - #[ORM\Column(name: 'filename', type: 'string', length: 250, nullable: true)] + #[ORM\Column(name: 'filename', type: 'string', nullable: true)] #[SymfonySerializer\Ignore] #[Serializer\Groups(['document', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Type('string')] - #[Assert\Length(max: 250)] private ?string $filename = null; /** * @var string|null */ #[ApiFilter(BaseFilter\SearchFilter::class, strategy: "exact")] #[ApiFilter(RoadizFilter\NotFilter::class)] - #[ORM\Column(name: 'mime_type', type: 'string', length: 255, nullable: true)] + #[ORM\Column(name: 'mime_type', type: 'string', nullable: true)] #[SymfonySerializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Type('string')] - #[Assert\Length(max: 255)] - #[ApiProperty( - description: 'Document file mime type', - example: 'image/jpeg', - )] private ?string $mimeType = null; /** * @var Collection @@ -265,9 +210,8 @@ class Document extends AbstractDateTimed implements AdvancedDocumentInterface, H /** * @var string */ - #[ORM\Column(type: 'string', length: 100)] + #[ORM\Column(type: 'string')] #[SymfonySerializer\Ignore] - #[Assert\Length(max: 100)] #[Serializer\Groups(['document', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Type('string')] private string $folder = ''; @@ -286,10 +230,6 @@ class Document extends AbstractDateTimed implements AdvancedDocumentInterface, H #[SymfonySerializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Type('int')] - #[ApiProperty( - description: 'When document has visual size: width in pixels', - example: '1280', - )] private int $imageWidth = 0; /** * @var integer @@ -298,10 +238,6 @@ class Document extends AbstractDateTimed implements AdvancedDocumentInterface, H #[SymfonySerializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Type('int')] - #[ApiProperty( - description: 'When document has visual size: height in pixels', - example: '800', - )] private int $imageHeight = 0; /** * @var integer @@ -310,10 +246,6 @@ class Document extends AbstractDateTimed implements AdvancedDocumentInterface, H #[SymfonySerializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Type('int')] - #[ApiProperty( - description: 'When document is audio or video: duration in seconds', - example: '300', - )] private int $mediaDuration = 0; /** * @var string|null @@ -322,11 +254,6 @@ class Document extends AbstractDateTimed implements AdvancedDocumentInterface, H #[SymfonySerializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Type('string')] - #[Assert\Length(max: 7)] - #[ApiProperty( - description: 'When document is image: average color in hexadecimal format', - example: '#ffffff' - )] private ?string $imageAverageColor = null; /** * @var int|null The filesize in bytes. @@ -654,10 +581,6 @@ public function setFilesize(?int $filesize): static Serializer\SerializedName("alt"), SymfonySerializer\Groups(["document", "document_display", "nodes_sources", "tag", "attribute"]), SymfonySerializer\SerializedName("alt"), - ApiProperty( - description: 'Document alternative text, for img HTML tag.', - writable: false, - ) ] public function getAlternativeText(): string { @@ -881,15 +804,4 @@ public function setEmbedId(?string $embedId): static $this->embedId = $embedId; return $this; } - - public function getImageCropAlignment(): ?string - { - return $this->imageCropAlignment; - } - - public function setImageCropAlignment(?string $imageCropAlignment): Document - { - $this->imageCropAlignment = $imageCropAlignment; - return $this; - } } diff --git a/src/Entity/DocumentTranslation.php b/src/Entity/DocumentTranslation.php index e7ac1609..0dd1f9db 100644 --- a/src/Entity/DocumentTranslation.php +++ b/src/Entity/DocumentTranslation.php @@ -13,7 +13,6 @@ use RZ\Roadiz\CoreBundle\Repository\DocumentTranslationRepository; use RZ\Roadiz\Documents\Models\DocumentInterface; use Symfony\Component\Serializer\Annotation as SymfonySerializer; -use Symfony\Component\Validator\Constraints as Assert; #[ ORM\Entity(repositoryClass: DocumentTranslationRepository::class), @@ -23,10 +22,9 @@ ] class DocumentTranslation extends AbstractEntity implements Loggable { - #[ORM\Column(type: 'string', length: 250, nullable: true)] + #[ORM\Column(type: 'string', nullable: true)] #[SymfonySerializer\Groups(['document', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Groups(['document', 'nodes_sources', 'tag', 'attribute'])] - #[Assert\Length(max: 250)] #[Gedmo\Versioned] protected ?string $name = null; @@ -43,16 +41,16 @@ class DocumentTranslation extends AbstractEntity implements Loggable protected ?string $externalUrl = null; #[ORM\ManyToOne(targetEntity: Translation::class, fetch: 'EXTRA_LAZY', inversedBy: 'documentTranslations')] - #[ORM\JoinColumn(name: 'translation_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + #[ORM\JoinColumn(name: 'translation_id', referencedColumnName: 'id', onDelete: 'CASCADE')] #[SymfonySerializer\Groups(['document', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Groups(['document', 'nodes_sources', 'tag', 'attribute'])] - protected TranslationInterface $translation; + protected ?TranslationInterface $translation = null; #[ORM\ManyToOne(targetEntity: Document::class, fetch: 'EXTRA_LAZY', inversedBy: 'documentTranslations')] - #[ORM\JoinColumn(name: 'document_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + #[ORM\JoinColumn(name: 'document_id', referencedColumnName: 'id', onDelete: 'CASCADE')] #[SymfonySerializer\Ignore] #[Serializer\Exclude] - protected DocumentInterface $document; + protected ?DocumentInterface $document; #[ORM\Column(type: 'text', nullable: true)] #[SymfonySerializer\Groups(['document', 'nodes_sources', 'tag', 'attribute'])] @@ -79,11 +77,19 @@ public function setName(?string $name): DocumentTranslation return $this; } + /** + * @return string + */ public function getDescription(): ?string { return $this->description; } + /** + * @param string|null $description + * + * @return $this + */ public function setDescription(?string $description): DocumentTranslation { $this->description = $description; @@ -159,7 +165,7 @@ public function getDocument(): DocumentInterface * @param DocumentInterface $document * @return $this */ - public function setDocument(DocumentInterface $document): DocumentTranslation + public function setDocument(DocumentInterface $document) { $this->document = $document; return $this; diff --git a/src/Entity/FieldAwareEntityTrait.php b/src/Entity/FieldAwareEntityTrait.php deleted file mode 100644 index be0611eb..00000000 --- a/src/Entity/FieldAwareEntityTrait.php +++ /dev/null @@ -1,44 +0,0 @@ -fieldName; - } - - public function setFieldName(string $fieldName): self - { - $this->fieldName = $fieldName; - return $this; - } - - /** - * @deprecated Use setFieldName method instead - */ - public function setField(NodeTypeFieldInterface $field): self - { - $this->fieldName = $field->getName(); - return $this; - } - - protected function initializeFieldAwareEntityTrait(?NodeTypeFieldInterface $nodeTypeField = null): void - { - if (null === $nodeTypeField) { - return; - } - $this->fieldName = $nodeTypeField->getName(); - } -} diff --git a/src/Entity/Folder.php b/src/Entity/Folder.php index bcb5834d..549c6e65 100644 --- a/src/Entity/Folder.php +++ b/src/Entity/Folder.php @@ -4,14 +4,15 @@ namespace RZ\Roadiz\CoreBundle\Entity; -use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter as BaseFilter; +use ApiPlatform\Core\Serializer\Filter\PropertyFilter; use ApiPlatform\Metadata\ApiFilter; -use ApiPlatform\Serializer\Filter\PropertyFilter; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Criteria; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; +use RZ\Roadiz\Core\AbstractEntities\AbstractDateTimedPositioned; use RZ\Roadiz\Core\AbstractEntities\LeafInterface; use RZ\Roadiz\Core\AbstractEntities\LeafTrait; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; @@ -100,12 +101,11 @@ class Folder extends AbstractDateTimedPositioned implements FolderInterface, Lea nullable: false, options: ['default' => '#000000'] )] - #[Assert\Length(max: 7)] #[SymfonySerializer\Groups(['folder', 'folder_color'])] protected string $color = '#000000'; #[ApiFilter(BaseFilter\SearchFilter::class, strategy: "partial")] - #[ORM\Column(name: 'folder_name', type: 'string', length: 250, unique: true, nullable: false)] + #[ORM\Column(name: 'folder_name', type: 'string', unique: true, nullable: false)] #[Serializer\Groups(['folder', 'document_folders'])] #[SymfonySerializer\Groups(['folder', 'document_folders'])] #[SymfonySerializer\SerializedName('slug')] diff --git a/src/Entity/FolderTranslation.php b/src/Entity/FolderTranslation.php index 76a78b84..eb7577a6 100644 --- a/src/Entity/FolderTranslation.php +++ b/src/Entity/FolderTranslation.php @@ -26,24 +26,28 @@ ] class FolderTranslation extends AbstractEntity { - #[ORM\Column(type: 'string', length: 250)] + #[ORM\Column(type: 'string')] #[SymfonySerializer\Groups(['folder', 'document'])] #[Serializer\Groups(['folder', 'document'])] #[Assert\Length(max: 250)] protected string $name = ''; #[ORM\ManyToOne(targetEntity: Folder::class, inversedBy: 'translatedFolders')] - #[ORM\JoinColumn(name: 'folder_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + #[ORM\JoinColumn(name: 'folder_id', referencedColumnName: 'id', onDelete: 'CASCADE')] #[SymfonySerializer\Ignore] #[Serializer\Exclude] - protected Folder $folder; + protected ?Folder $folder = null; #[ORM\ManyToOne(targetEntity: Translation::class, fetch: 'EXTRA_LAZY', inversedBy: 'folderTranslations')] - #[ORM\JoinColumn(name: 'translation_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + #[ORM\JoinColumn(name: 'translation_id', referencedColumnName: 'id', onDelete: 'CASCADE')] #[SymfonySerializer\Groups(['folder', 'document'])] #[Serializer\Groups(['folder', 'document'])] - protected TranslationInterface $translation; + protected ?TranslationInterface $translation = null; + /** + * @param Folder $original + * @param TranslationInterface $translation + */ public function __construct(Folder $original, TranslationInterface $translation) { $this->setFolder($original); @@ -63,7 +67,7 @@ public function getName(): string * @param string $name * @return $this */ - public function setName(string $name): FolderTranslation + public function setName(string $name) { $this->name = $name; return $this; diff --git a/src/Entity/Group.php b/src/Entity/Group.php index 5bb7efd0..146cefdf 100644 --- a/src/Entity/Group.php +++ b/src/Entity/Group.php @@ -24,7 +24,7 @@ ] class Group extends AbstractEntity { - #[ORM\Column(type: 'string', length: 250, unique: true)] + #[ORM\Column(type: 'string', unique: true)] #[SymfonySerializer\Groups(['user', 'role', 'group'])] #[Serializer\Groups(['user', 'role', 'group'])] #[Assert\NotBlank] diff --git a/src/Logger/Entity/Log.php b/src/Entity/Log.php similarity index 60% rename from src/Logger/Entity/Log.php rename to src/Entity/Log.php index 8dbdf02f..c42579f1 100644 --- a/src/Logger/Entity/Log.php +++ b/src/Entity/Log.php @@ -2,26 +2,20 @@ declare(strict_types=1); -namespace RZ\Roadiz\CoreBundle\Logger\Entity; +namespace RZ\Roadiz\CoreBundle\Entity; use Doctrine\ORM\Mapping as ORM; -use JMS\Serializer\Annotation as Serializer; use Monolog\Logger; use RZ\Roadiz\Core\AbstractEntities\AbstractEntity; -use RZ\Roadiz\CoreBundle\Entity\NodesSources; -use RZ\Roadiz\CoreBundle\Entity\User; +use JMS\Serializer\Annotation as Serializer; use RZ\Roadiz\CoreBundle\Repository\LogRepository; use Symfony\Component\Serializer\Annotation as SymfonySerializer; -use Symfony\Component\Validator\Constraints as Assert; #[ ORM\Entity(repositoryClass: LogRepository::class), ORM\Table(name: "log"), ORM\Index(columns: ["datetime"]), - ORM\Index(columns: ["entity_class"]), - ORM\Index(columns: ["entity_class", "entity_id"]), - ORM\Index(columns: ["entity_class", "datetime"], name: "log_entity_class_datetime"), - ORM\Index(columns: ["entity_class", "entity_id", "datetime"], name: "log_entity_class_id_datetime"), + ORM\Index(columns: ["node_source_id", "datetime"], name: "log_ns_datetime"), ORM\Index(columns: ["username", "datetime"], name: "log_username_datetime"), ORM\Index(columns: ["user_id", "datetime"], name: "log_user_datetime"), ORM\Index(columns: ["level", "datetime"], name: "log_level_datetime"), @@ -33,16 +27,25 @@ ] class Log extends AbstractEntity { - #[ORM\Column(name: 'user_id', type: 'string', length: 36, unique: false, nullable: true)] + public const EMERGENCY = Logger::EMERGENCY; + public const CRITICAL = Logger::CRITICAL; + public const ALERT = Logger::ALERT; + public const ERROR = Logger::ERROR; + public const WARNING = Logger::WARNING; + public const NOTICE = Logger::NOTICE; + public const INFO = Logger::INFO; + public const DEBUG = Logger::DEBUG; + public const LOG = Logger::INFO; + + #[ORM\ManyToOne(targetEntity: User::class)] + #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', unique: false, onDelete: 'SET NULL')] #[SymfonySerializer\Groups(['log_user'])] #[Serializer\Groups(['log_user'])] - // @phpstan-ignore-next-line - protected int|string|null $userId = null; + protected ?User $user = null; - #[ORM\Column(name: 'username', type: 'string', length: 255, nullable: true)] + #[ORM\Column(name: 'username', type: 'string', nullable: true)] #[SymfonySerializer\Groups(['log_user'])] #[Serializer\Groups(['log_user'])] - #[Assert\Length(max: 255)] protected ?string $username = null; #[ORM\Column(name: 'message', type: 'text')] @@ -53,45 +56,29 @@ class Log extends AbstractEntity #[ORM\Column(name: 'level', type: 'integer', nullable: false)] #[SymfonySerializer\Groups(['log'])] #[Serializer\Groups(['log'])] - protected int $level = Logger::DEBUG; + protected int $level = Log::DEBUG; #[ORM\Column(name: 'datetime', type: 'datetime', nullable: false)] #[SymfonySerializer\Groups(['log'])] #[Serializer\Groups(['log'])] protected \DateTime $datetime; - #[ORM\Column(name: 'client_ip', type: 'string', length: 46, unique: false, nullable: true)] + #[ORM\ManyToOne(targetEntity: NodesSources::class, inversedBy: 'logs')] + #[ORM\JoinColumn(name: 'node_source_id', referencedColumnName: 'id', onDelete: 'SET NULL')] + #[SymfonySerializer\Groups(['log_sources'])] + #[Serializer\Groups(['log_sources'])] + protected ?NodesSources $nodeSource = null; + + #[ORM\Column(name: 'client_ip', type: 'string', unique: false, nullable: true)] #[SymfonySerializer\Groups(['log'])] #[Serializer\Groups(['log'])] - #[Assert\Length(max: 46)] protected ?string $clientIp = null; - #[ORM\Column(name: 'channel', type: 'string', length: 64, unique: false, nullable: true)] + #[ORM\Column(name: 'channel', type: 'string', unique: false, nullable: true)] #[SymfonySerializer\Groups(['log'])] #[Serializer\Groups(['log'])] - #[Assert\Length(max: 64)] protected ?string $channel = null; - /** - * @var class-string|null - */ - #[ORM\Column(name: 'entity_class', type: 'string', length: 255, unique: false, nullable: true)] - #[SymfonySerializer\Groups(['log'])] - #[Serializer\Groups(['log'])] - #[Assert\Length(max: 255)] - // @phpstan-ignore-next-line - protected ?string $entityClass = null; - - /** - * @var string|int|null - */ - #[ORM\Column(name: 'entity_id', type: 'string', length: 36, unique: false, nullable: true)] - #[SymfonySerializer\Groups(['log'])] - #[Serializer\Groups(['log'])] - #[Assert\Length(max: 36)] - // @phpstan-ignore-next-line - protected string|int|null $entityId = null; - #[ORM\Column(name: 'additional_data', type: 'json', unique: false, nullable: true)] #[SymfonySerializer\Groups(['log'])] #[Serializer\Groups(['log'])] @@ -110,22 +97,9 @@ public function __construct(int $level, string $message) $this->datetime = new \DateTime("now"); } - /** - * @return int|string|null - */ - public function getUserId(): int|string|null + public function getUser(): ?User { - return $this->userId; - } - - /** - * @param int|string|null $userId - * @return Log - */ - public function setUserId(int|string|null $userId): Log - { - $this->userId = $userId; - return $this; + return $this->user; } /** @@ -135,7 +109,7 @@ public function setUserId(int|string|null $userId): Log */ public function setUser(User $user): Log { - $this->userId = $user->getId(); + $this->user = $user; $this->username = $user->getUsername(); return $this; } @@ -185,22 +159,27 @@ public function getDatetime(): \DateTime } /** - * BC setter. + * Get log related node-source. * + * @return NodesSources|null + */ + public function getNodeSource(): ?NodesSources + { + return $this->nodeSource; + } + + /** * @param NodesSources|null $nodeSource * @return $this */ public function setNodeSource(?NodesSources $nodeSource): Log { - if (null !== $nodeSource) { - $this->entityClass = NodesSources::class; - $this->entityId = $nodeSource->getId(); - } + $this->nodeSource = $nodeSource; return $this; } /** - * @return string|null + * @return string */ public function getClientIp(): ?string { @@ -208,7 +187,7 @@ public function getClientIp(): ?string } /** - * @param string|null $clientIp + * @param string $clientIp * @return Log */ public function setClientIp(?string $clientIp): Log @@ -257,42 +236,6 @@ public function setChannel(?string $channel): Log return $this; } - /** - * @return class-string|null - */ - public function getEntityClass(): ?string - { - return $this->entityClass; - } - - /** - * @param class-string|null $entityClass - * @return Log - */ - public function setEntityClass(?string $entityClass): Log - { - $this->entityClass = $entityClass; - return $this; - } - - /** - * @return int|string|null - */ - public function getEntityId(): int|string|null - { - return $this->entityId; - } - - /** - * @param int|string|null $entityId - * @return Log - */ - public function setEntityId(int|string|null $entityId): Log - { - $this->entityId = $entityId; - return $this; - } - #[ORM\PrePersist] public function prePersist(): void { diff --git a/src/Entity/LoginAttempt.php b/src/Entity/LoginAttempt.php new file mode 100644 index 00000000..db44dc88 --- /dev/null +++ b/src/Entity/LoginAttempt.php @@ -0,0 +1,106 @@ +ipAddress = $ipAddress; + $this->username = $username; + $this->date = new \DateTimeImmutable('now'); + $this->blocksLoginUntil = new \DateTime('now'); + $this->attemptCount = 0; + } + + public function getId(): int + { + return $this->id; + } + + public function getIpAddress(): ?string + { + return $this->ipAddress; + } + + public function getDate(): \DateTimeImmutable + { + return $this->date; + } + + public function getUsername(): ?string + { + return $this->username; + } + + /** + * @return \DateTime|null + */ + public function getBlocksLoginUntil(): ?\DateTime + { + return $this->blocksLoginUntil; + } + + /** + * @param \DateTime $blocksLoginUntil + * + * @return LoginAttempt + */ + public function setBlocksLoginUntil(\DateTime $blocksLoginUntil): LoginAttempt + { + $this->blocksLoginUntil = $blocksLoginUntil; + + return $this; + } + + /** + * @return int + */ + public function getAttemptCount(): int + { + return $this->attemptCount; + } + + /** + * @return LoginAttempt + */ + public function addAttemptCount(): LoginAttempt + { + $this->attemptCount++; + return $this; + } +} diff --git a/src/Entity/Node.php b/src/Entity/Node.php index 18b73384..02875828 100644 --- a/src/Entity/Node.php +++ b/src/Entity/Node.php @@ -4,10 +4,9 @@ namespace RZ\Roadiz\CoreBundle\Entity; -use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter as BaseFilter; +use ApiPlatform\Core\Serializer\Filter\PropertyFilter; use ApiPlatform\Metadata\ApiFilter; -use ApiPlatform\Metadata\ApiProperty; -use ApiPlatform\Serializer\Filter\PropertyFilter; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Criteria; @@ -15,8 +14,8 @@ use Gedmo\Loggable\Loggable; use Gedmo\Mapping\Annotation as Gedmo; use JMS\Serializer\Annotation as Serializer; -use RZ\Roadiz\Contracts\NodeType\NodeTypeFieldInterface; use RZ\Roadiz\Contracts\NodeType\NodeTypeInterface; +use RZ\Roadiz\Core\AbstractEntities\AbstractDateTimedPositioned; use RZ\Roadiz\Core\AbstractEntities\LeafInterface; use RZ\Roadiz\Core\AbstractEntities\LeafTrait; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; @@ -45,7 +44,6 @@ ORM\Index(columns: ["created_at"]), ORM\Index(columns: ["updated_at"]), ORM\Index(columns: ["hide_children"]), - ORM\Index(columns: ["home"]), ORM\Index(columns: ["node_name", "status"]), ORM\Index(columns: ["visible", "status"]), ORM\Index(columns: ["visible", "status", "parent_node_id"], name: "node_visible_status_parent"), @@ -53,17 +51,12 @@ ORM\Index(columns: ["nodeType_id", "status", "parent_node_id"], name: "node_nodetype_status_parent"), ORM\Index(columns: ["nodeType_id", "status", "parent_node_id", "position"], name: "node_nodetype_status_parent_position"), ORM\Index(columns: ["visible", "parent_node_id"], name: "node_visible_parent"), - ORM\Index(columns: ["parent_node_id", "position"], name: "node_parent_position"), ORM\Index(columns: ["visible", "parent_node_id", "position"], name: "node_visible_parent_position"), ORM\Index(columns: ["status", "visible", "parent_node_id", "position"], name: "node_status_visible_parent_position"), + ORM\Index(columns: ["home"]), ORM\HasLifecycleCallbacks, Gedmo\Loggable(logEntryClass: UserLogEntry::class), - // Need to override repository method to see all nodes - UniqueEntity( - fields: 'nodeName', - message: 'nodeName.alreadyExists', - repositoryMethod: 'findOneWithoutSecurity' - ), + UniqueEntity(fields: ["nodeName"]), ApiFilter(PropertyFilter::class) ] class Node extends AbstractDateTimedPositioned implements LeafInterface, AttributableInterface, Loggable @@ -87,17 +80,13 @@ class Node extends AbstractDateTimedPositioned implements LeafInterface, Attribu 'publishedAt' => 'ns.publishedAt', ]; - #[ORM\Column(name: 'node_name', type: 'string', length: 255, unique: true)] + #[ORM\Column(name: 'node_name', type: 'string', unique: true)] #[SymfonySerializer\Groups(['nodes_sources', 'nodes_sources_base', 'node', 'log_sources'])] #[Serializer\Groups(['nodes_sources', 'nodes_sources_base', 'node', 'log_sources'])] #[Serializer\Accessor(getter: "getNodeName", setter: "setNodeName")] #[Assert\NotNull] #[Assert\NotBlank] #[Assert\Length(max: 255)] - #[ApiProperty( - description: 'Unique node name (slug) used to build content URL', - example: 'this-is-a-node-name', - )] private string $nodeName = ''; #[ORM\Column(name: 'dynamic_node_name', type: 'boolean', nullable: false, options: ['default' => true])] @@ -113,10 +102,6 @@ class Node extends AbstractDateTimedPositioned implements LeafInterface, Attribu #[SymfonySerializer\Groups(['nodes_sources_base', 'nodes_sources', 'node'])] #[Serializer\Groups(['nodes_sources_base', 'nodes_sources', 'node'])] #[Gedmo\Versioned] - #[ApiProperty( - description: 'Is this node visible in website navigation?', - example: 'true', - )] private bool $visible = true; /** @@ -129,82 +114,59 @@ class Node extends AbstractDateTimedPositioned implements LeafInterface, Attribu #[ORM\Column(type: 'integer', nullable: false, options: ['default' => 0])] #[Assert\GreaterThanOrEqual(value: 0)] - #[Assert\NotNull] #[SymfonySerializer\Ignore] #[Serializer\Exclude] #[Gedmo\Versioned] - // @phpstan-ignore-next-line - private ?int $ttl = 0; + private int $ttl = 0; #[ORM\Column(type: 'boolean', nullable: false, options: ['default' => false])] #[SymfonySerializer\Groups(['node'])] #[Serializer\Groups(['node'])] #[Gedmo\Versioned] - #[ApiProperty( - description: 'Is this node locked to prevent deletion and renaming?', - example: 'false', - )] private bool $locked = false; + /** + * @var float|string|int + */ + #[ORM\Column(type: 'decimal', precision: 2, scale: 1)] + #[SymfonySerializer\Groups(['node'])] + #[Serializer\Groups(['node'])] + #[Gedmo\Versioned] + private string|float|int $priority = 0.8; + #[ORM\Column(name: 'hide_children', type: 'boolean', nullable: false, options: ['default' => false])] #[SymfonySerializer\Groups(['node'])] #[Serializer\Groups(['node'])] #[Gedmo\Versioned] - #[ApiProperty( - description: 'Does this node act as a container for other nodes?', - example: 'false', - )] private bool $hideChildren = false; #[ORM\Column(type: 'boolean', nullable: false, options: ['default' => false])] #[SymfonySerializer\Groups(['node'])] #[Serializer\Groups(['node'])] #[Gedmo\Versioned] - #[ApiProperty( - description: 'Can this node hold other nodes inside?', - example: 'false', - )] private bool $sterile = false; - #[ORM\Column(name: 'children_order', type: 'string', length: 50)] - #[SymfonySerializer\Groups(['node', 'node_listing'])] - #[Serializer\Groups(['node', 'node_listing'])] - #[Assert\Length(max: 50)] + #[ORM\Column(name: 'children_order', type: 'string')] + #[SymfonySerializer\Groups(['node'])] + #[Serializer\Groups(['node'])] #[Gedmo\Versioned] - #[ApiProperty( - description: 'This node children will be sorted by a given field', - example: 'position', - schema: [ - 'type' => 'string', - 'enum' => ['position', 'nodeName', 'createdAt', 'updatedAt', 'publishedAt'], - 'example' => 'position' - ], - )] private string $childrenOrder = 'position'; #[ORM\Column(name: 'children_order_direction', type: 'string', length: 4)] - #[SymfonySerializer\Groups(['node', 'node_listing'])] - #[Serializer\Groups(['node', 'node_listing'])] - #[Assert\Length(max: 4)] - #[Assert\Choice(choices: ['ASC', 'DESC'])] + #[SymfonySerializer\Groups(['node'])] + #[Serializer\Groups(['node'])] #[Gedmo\Versioned] - #[ApiProperty( - description: 'This node children will be sorted ascendant or descendant', - example: 'ASC', - schema: [ - 'type' => 'string', - 'enum' => ['ASC', 'DESC'], - 'example' => 'ASC' - ], - )] private string $childrenOrderDirection = 'ASC'; + /** + * @var NodeTypeInterface|null + */ #[ORM\ManyToOne(targetEntity: NodeTypeInterface::class)] - #[ORM\JoinColumn(name: 'nodeType_id', referencedColumnName: 'id', nullable:false, onDelete: 'CASCADE')] + #[ORM\JoinColumn(name: 'nodeType_id', referencedColumnName: 'id', onDelete: 'CASCADE')] #[SymfonySerializer\Groups(['node'])] #[Serializer\Groups(['node'])] #[SymfonySerializer\Ignore] - private NodeTypeInterface $nodeType; + private ?NodeTypeInterface $nodeType = null; /** * @var Node|null @@ -319,7 +281,7 @@ class Node extends AbstractDateTimedPositioned implements LeafInterface, Attribu /** * Create a new empty Node according to given node-type. */ - public function __construct() + public function __construct(NodeTypeInterface $nodeType = null) { $this->nodesTags = new ArrayCollection(); $this->children = new ArrayCollection(); @@ -329,6 +291,8 @@ public function __construct() $this->aNodes = new ArrayCollection(); $this->bNodes = new ArrayCollection(); $this->attributeValues = new ArrayCollection(); + + $this->setNodeType($nodeType); $this->initAbstractDateTimed(); } @@ -419,15 +383,15 @@ public function setStatus(int|string $status): Node */ public function getTtl(): int { - return $this->ttl ?? 0; + return $this->ttl; } /** - * @param int|null $ttl + * @param int $ttl * * @return Node */ - public function setTtl(?int $ttl): Node + public function setTtl(int $ttl): Node { $this->ttl = $ttl; return $this; @@ -483,6 +447,24 @@ public function setLocked(bool $locked): static return $this; } + /** + * @return float|string + */ + public function getPriority() + { + return $this->priority; + } + + /** + * @param float|string $priority + * @return $this + */ + public function setPriority($priority): static + { + $this->priority = $priority; + return $this; + } + /** * @return bool */ @@ -796,10 +778,10 @@ public function addNodeSources(NodesSources $ns): static * @return Collection */ #[SymfonySerializer\Ignore] - public function getBNodesByField(NodeTypeFieldInterface $field): Collection + public function getBNodesByField(NodeTypeField $field): Collection { $criteria = Criteria::create(); - $criteria->andWhere(Criteria::expr()->eq('fieldName', $field->getName())); + $criteria->andWhere(Criteria::expr()->eq('field', $field)); $criteria->orderBy(['position' => 'ASC']); return $this->getBNodes()->matching($criteria); } @@ -820,6 +802,9 @@ public function getBNodes(): Collection */ public function setBNodes(Collection $bNodes): static { + foreach ($this->bNodes as $bNode) { + $bNode->setNodeA(null); + } $this->bNodes->clear(); foreach ($bNodes as $bNode) { if (!$this->hasBNode($bNode)) { @@ -834,7 +819,7 @@ public function hasBNode(NodesToNodes $bNode): bool return $this->getBNodes()->exists(function ($key, NodesToNodes $element) use ($bNode) { return $bNode->getNodeB()->getId() !== null && $element->getNodeB()->getId() === $bNode->getNodeB()->getId() && - $element->getFieldName() === $bNode->getFieldName(); + $element->getField()->getId() === $bNode->getField()->getId(); }); } @@ -851,14 +836,15 @@ public function addBNode(NodesToNodes $bNode): static return $this; } - public function clearBNodesForField(NodeTypeFieldInterface $field): Node + public function clearBNodesForField(NodeTypeField $nodeTypeField): Node { - $toRemoveCollection = $this->getBNodes()->filter(function (NodesToNodes $element) use ($field) { - return $element->getFieldName() === $field->getName(); + $toRemoveCollection = $this->getBNodes()->filter(function (NodesToNodes $element) use ($nodeTypeField) { + return $element->getField()->getId() === $nodeTypeField->getId(); }); /** @var NodesToNodes $toRemove */ foreach ($toRemoveCollection as $toRemove) { $this->getBNodes()->removeElement($toRemove); + $toRemove->setNodeA(null); } return $this; } @@ -901,12 +887,19 @@ public function setNodeName(string $nodeName): static return $this; } - public function getNodeType(): NodeTypeInterface + /** + * @return NodeTypeInterface|null + */ + public function getNodeType(): ?NodeTypeInterface { return $this->nodeType; } - public function setNodeType(NodeTypeInterface $nodeType): Node + /** + * @param NodeTypeInterface|null $nodeType + * @return $this + */ + public function setNodeType(?NodeTypeInterface $nodeType = null): static { $this->nodeType = $nodeType; return $this; @@ -936,19 +929,11 @@ public function setVisible(bool $visible): Node #[SymfonySerializer\Ignore] public function getOneLineSourceSummary(): string { - $text = "Source " . - ( - $this->getNodeSources()->first() ? - $this->getNodeSources()->first()->getId() : - '' - ) . - PHP_EOL; + $text = "Source " . $this->getNodeSources()->first()->getId() . PHP_EOL; foreach ($this->getNodeType()->getFields() as $field) { $getterName = $field->getGetterName(); - $text .= '[' . $field->getLabel() . ']: ' . - ($this->getNodeSources()->first() ? $this->getNodeSources()->first()->$getterName() : '') . - PHP_EOL; + $text .= '[' . $field->getLabel() . ']: ' . $this->getNodeSources()->first()->$getterName() . PHP_EOL; } return $text; @@ -999,14 +984,9 @@ public function __clone() // Get a random string after node-name. // This is for safety reasons // NodeDuplicator service will override it - $nodeSource = $this->getNodeSources()->first(); - if ($nodeSource !== false) { - $namePrefix = $nodeSource->getTitle() != "" ? - $nodeSource->getTitle() : - $this->nodeName; - } else { - $namePrefix = $this->nodeName; - } + $namePrefix = $this->getNodeSources()->first()->getTitle() != "" ? + $this->getNodeSources()->first()->getTitle() : + $this->nodeName; $this->setNodeName($namePrefix . "-" . uniqid()); $this->setCreatedAt(new \DateTime()); $this->setUpdatedAt(new \DateTime()); diff --git a/src/Entity/NodeType.php b/src/Entity/NodeType.php index 53ab01ff..1f272b77 100644 --- a/src/Entity/NodeType.php +++ b/src/Entity/NodeType.php @@ -9,6 +9,7 @@ use Doctrine\Common\Collections\Criteria; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; +use RZ\Roadiz\Contracts\NodeType\NodeTypeFieldInterface; use RZ\Roadiz\Contracts\NodeType\NodeTypeInterface; use RZ\Roadiz\Core\AbstractEntities\AbstractEntity; use RZ\Roadiz\CoreBundle\Form\Constraint as RoadizAssert; @@ -28,7 +29,6 @@ ORM\Index(columns: ["name"], name: "node_type_name"), ORM\Index(columns: ["visible"]), ORM\Index(columns: ["publishable"]), - ORM\Index(columns: ["attributable"]), ORM\Index(columns: ["hiding_nodes"]), ORM\Index(columns: ["hiding_non_reachable_nodes"]), ORM\Index(columns: ["reachable"]), @@ -39,15 +39,14 @@ class NodeType extends AbstractEntity implements NodeTypeInterface { #[ - ORM\Column(name: "color", type: "string", length: 7, unique: false, nullable: true), + ORM\Column(name: "color", type: "string", unique: false, nullable: true), Serializer\Groups(["node_type", "color"]), SymfonySerializer\Groups(["node_type", "color"]), - Serializer\Type("string"), - Assert\Length(max: 7), + Serializer\Type("string") ] protected ?string $color = '#000000'; #[ - ORM\Column(type: "string", length: 30, unique: true), + ORM\Column(type: "string", unique: true), Serializer\Groups(["node_type", "node"]), SymfonySerializer\Groups(["node_type", "node"]), Serializer\Type("string"), @@ -60,7 +59,7 @@ class NodeType extends AbstractEntity implements NodeTypeInterface ] private string $name = ''; #[ - ORM\Column(name: "display_name", type: "string", length: 250), + ORM\Column(name: "display_name", type: "string"), Serializer\Groups(["node_type", "node"]), SymfonySerializer\Groups(["node_type", "node"]), Serializer\Type("string"), @@ -90,24 +89,6 @@ class NodeType extends AbstractEntity implements NodeTypeInterface Serializer\Type("boolean") ] private bool $publishable = false; - - /** - * @var bool Define if this node-type produces nodes that will have attributes. - */ - #[ - ORM\Column(type: "boolean", nullable: false, options: ["default" => true]), - Serializer\Groups(["node_type"]), - SymfonySerializer\Groups(["node_type"]), - Serializer\Type("boolean") - ] - private bool $attributable = false; - #[ - ORM\Column(name: "attributable_by_weight", type: "boolean", nullable: false, options: ["default" => false]), - Serializer\Groups(["node_type"]), - SymfonySerializer\Groups(["node_type"]), - Serializer\Type("boolean") - ] - private bool $sortingAttributesByWeight = false; /** * Define if this node-type produces nodes that will be * viewable from a Controller. @@ -139,12 +120,7 @@ class NodeType extends AbstractEntity implements NodeTypeInterface * @var Collection */ #[ - ORM\OneToMany( - mappedBy: "nodeType", - targetEntity: NodeTypeField::class, - cascade: ["all"], - orphanRemoval: true - ), + ORM\OneToMany(mappedBy: "nodeType", targetEntity: NodeTypeField::class, cascade: ["persist", "merge"]), ORM\OrderBy(["position" => "ASC"]), Serializer\Groups(["node_type"]), SymfonySerializer\Groups(["node_type"]), @@ -157,11 +133,9 @@ class NodeType extends AbstractEntity implements NodeTypeInterface Serializer\Groups(["node_type"]), SymfonySerializer\Groups(["node_type"]), Serializer\Type("int"), - Assert\GreaterThanOrEqual(value: 0), - Assert\NotNull + Assert\GreaterThanOrEqual(value: 0) ] - // @phpstan-ignore-next-line - private ?int $defaultTtl = 0; + private int $defaultTtl = 0; /** * Define if this node-type title will be indexed during its parent indexation. */ @@ -354,15 +328,15 @@ public function setColor(?string $color): NodeType */ public function getDefaultTtl(): int { - return $this->defaultTtl ?? 0; + return $this->defaultTtl; } /** - * @param int|null $defaultTtl + * @param int $defaultTtl * * @return NodeType */ - public function setDefaultTtl(?int $defaultTtl): NodeType + public function setDefaultTtl(int $defaultTtl): NodeType { $this->defaultTtl = $defaultTtl; @@ -473,7 +447,6 @@ public function removeField(NodeTypeField $field): NodeType #[SymfonySerializer\Ignore] public function getSourceEntityFullQualifiedClassName(): string { - // @phpstan-ignore-next-line return static::getGeneratedEntitiesNamespace() . '\\' . $this->getSourceEntityClassName(); } @@ -546,26 +519,4 @@ public function setSearchable(bool $searchable): NodeType $this->searchable = $searchable; return $this; } - - public function isAttributable(): bool - { - return $this->attributable; - } - - public function setAttributable(bool $attributable): NodeType - { - $this->attributable = $attributable; - return $this; - } - - public function isSortingAttributesByWeight(): bool - { - return $this->sortingAttributesByWeight; - } - - public function setSortingAttributesByWeight(bool $sortingAttributesByWeight): NodeType - { - $this->sortingAttributesByWeight = $sortingAttributesByWeight; - return $this; - } } diff --git a/src/Entity/NodeTypeField.php b/src/Entity/NodeTypeField.php index 07dd18bf..dedfcb7c 100644 --- a/src/Entity/NodeTypeField.php +++ b/src/Entity/NodeTypeField.php @@ -29,7 +29,6 @@ ORM\Index(columns: ["group_name"]), ORM\Index(columns: ["group_name_canonical"]), ORM\Index(columns: ["type"]), - ORM\Index(columns: ["name"], name: 'ntf_name'), ORM\Index(columns: ["universal"]), ORM\Index(columns: ["node_type_id", "position"], name: "ntf_type_position"), ORM\UniqueConstraint(columns: ["name", "node_type_id"]), @@ -40,11 +39,11 @@ class NodeTypeField extends AbstractField implements NodeTypeFieldInterface, SerializableInterface { #[ - ORM\Column(type: "string", length: 50), + ORM\Column(type: "string"), Serializer\Expose, Serializer\Groups(["node_type", "setting"]), SymfonySerializer\Groups(["node_type", "setting"]), - Assert\Length(max: 50), + Assert\Length(max: 250), Serializer\Type("string"), RoadizAssert\NonSqlReservedWord(), RoadizAssert\SimpleLatinString() @@ -75,11 +74,11 @@ class NodeTypeField extends AbstractField implements NodeTypeFieldInterface, Ser #[ ORM\ManyToOne(targetEntity: NodeType::class, inversedBy: "fields"), - ORM\JoinColumn(name: "node_type_id", nullable: false, onDelete: "CASCADE"), + ORM\JoinColumn(name: "node_type_id", onDelete: "CASCADE"), Serializer\Exclude(), SymfonySerializer\Ignore ] - private NodeTypeInterface $nodeType; + private ?NodeTypeInterface $nodeType = null; #[ Serializer\Groups(["node_type"]), @@ -146,32 +145,53 @@ class NodeTypeField extends AbstractField implements NodeTypeFieldInterface, Ser private bool $visible = true; #[ + Serializer\VirtualProperty(), + Serializer\Type("string"), + Serializer\Groups(["node_type"]), SymfonySerializer\Groups(["node_type"]) ] public function getNodeTypeName(): string { - return $this->getNodeType()->getName(); + return $this->getNodeType() ? $this->getNodeType()->getName() : ''; } - public function getNodeType(): NodeTypeInterface + /** + * @return NodeTypeInterface|null + */ + public function getNodeType(): ?NodeTypeInterface { return $this->nodeType; } - public function setNodeType(NodeTypeInterface $nodeType): NodeTypeField + /** + * @param NodeTypeInterface|null $nodeType + * + * @return $this + */ + public function setNodeType(?NodeTypeInterface $nodeType) { $this->nodeType = $nodeType; + return $this; } + /** + * @return int|null + */ public function getMinLength(): ?int { return $this->minLength; } - public function setMinLength(?int $minLength): NodeTypeField + /** + * @param int|null $minLength + * + * @return $this + */ + public function setMinLength(?int $minLength) { $this->minLength = $minLength; + return $this; } @@ -183,9 +203,15 @@ public function getMaxLength(): ?int return $this->maxLength; } - public function setMaxLength(?int $maxLength): NodeTypeField + /** + * @param int|null $maxLength + * + * @return $this + */ + public function setMaxLength(?int $maxLength) { $this->maxLength = $maxLength; + return $this; } @@ -203,7 +229,7 @@ public function isSearchable(): bool * @return string */ #[SymfonySerializer\Ignore] - public function getOneLineSummary(): string + public function getOneLineSummary() { return $this->getId() . " — " . $this->getLabel() . ' [' . $this->getName() . ']' . ' - ' . $this->getTypeName() . @@ -220,7 +246,11 @@ public function isIndexed(): bool return $this->indexed && $this->getDoctrineType() !== 'json'; } - public function setIndexed(bool $indexed): NodeTypeField + /** + * @param bool $indexed + * @return $this + */ + public function setIndexed(bool $indexed) { $this->indexed = $indexed; return $this; @@ -234,9 +264,14 @@ public function isVisible(): bool return $this->visible; } - public function setVisible(bool $visible): NodeTypeField + /** + * @param bool $visible + * @return $this + */ + public function setVisible(bool $visible) { $this->visible = $visible; + return $this; } @@ -261,7 +296,7 @@ public function getUniversal(): bool * @param bool $universal * @return NodeTypeField */ - public function setUniversal(bool $universal): NodeTypeField + public function setUniversal(bool $universal) { $this->universal = $universal; return $this; @@ -283,18 +318,29 @@ public function getExcludeFromSearch(): bool return $this->excludeFromSearch; } + /** + * @return bool + */ public function isExcludeFromSearch(): bool { return $this->getExcludeFromSearch(); } - public function setExcludeFromSearch(bool $excludeFromSearch): NodeTypeField + /** + * @param bool $excludeFromSearch + * + * @return NodeTypeField + */ + public function setExcludeFromSearch(bool $excludeFromSearch) { $this->excludeFromSearch = $excludeFromSearch; return $this; } + /** + * @return string|null + */ public function getSerializationExclusionExpression(): ?string { return $this->serializationExclusionExpression; diff --git a/src/Entity/NodesCustomForms.php b/src/Entity/NodesCustomForms.php index 8f7d4a39..97335583 100644 --- a/src/Entity/NodesCustomForms.php +++ b/src/Entity/NodesCustomForms.php @@ -18,50 +18,62 @@ ORM\Table(name: "nodes_custom_forms"), ORM\Index(columns: ["position"]), ORM\Index(columns: ["node_id", "position"], name: "customform_node_position"), - ORM\Index(columns: ["node_id", "field_name", "position"], name: "customform_node_field_position") + ORM\Index(columns: ["node_id", "node_type_field_id", "position"], name: "customform_node_field_position") ] class NodesCustomForms extends AbstractPositioned { - use FieldAwareEntityTrait; - + /** + * @var Node|null + */ #[ORM\ManyToOne(targetEntity: Node::class, fetch: 'EAGER', inversedBy: 'customForms')] - #[ORM\JoinColumn(name: 'node_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] - protected Node $node; + #[ORM\JoinColumn(name: 'node_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + protected ?Node $node = null; + /** + * @var CustomForm|null + */ #[ORM\ManyToOne(targetEntity: CustomForm::class, fetch: 'EAGER', inversedBy: 'nodes')] - #[ORM\JoinColumn(name: 'custom_form_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] - protected CustomForm $customForm; + #[ORM\JoinColumn(name: 'custom_form_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + protected ?CustomForm $customForm = null; + + /** + * @var NodeTypeField|null + */ + #[ORM\ManyToOne(targetEntity: NodeTypeField::class)] + #[ORM\JoinColumn(name: 'node_type_field_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + protected ?NodeTypeField $field = null; /** * Create a new relation between a Node, a CustomForm and a NodeTypeField. * - * @param Node $node - * @param CustomForm $customForm - * @param NodeTypeFieldInterface|null $field NodeTypeField + * @param Node $node + * @param CustomForm $customForm + * @param NodeTypeFieldInterface $field NodeTypeField */ - public function __construct(Node $node, CustomForm $customForm, ?NodeTypeFieldInterface $field = null) + public function __construct(Node $node, CustomForm $customForm, NodeTypeFieldInterface $field) { if (!$field instanceof NodeTypeField) { throw new \InvalidArgumentException('NodesCustomForms only accept NodeTypeField'); } $this->node = $node; $this->customForm = $customForm; - $this->initializeFieldAwareEntityTrait($field); + $this->field = $field; } public function __clone() { if ($this->id) { $this->id = null; + $this->node = null; } } /** * Gets the value of node. * - * @return Node + * @return Node|null */ - public function getNode(): Node + public function getNode(): ?Node { return $this->node; } @@ -99,6 +111,31 @@ public function getCustomForm(): CustomForm public function setCustomForm(CustomForm $customForm): NodesCustomForms { $this->customForm = $customForm; + + return $this; + } + + /** + * Gets the value of field. + * + * @return NodeTypeField + */ + public function getField(): NodeTypeField + { + return $this->field; + } + + /** + * Sets the value of field. + * + * @param NodeTypeField $field the field + * + * @return self + */ + public function setField(NodeTypeField $field): NodesCustomForms + { + $this->field = $field; + return $this; } } diff --git a/src/Entity/NodesSources.php b/src/Entity/NodesSources.php index 8ebc470e..50b327b5 100644 --- a/src/Entity/NodesSources.php +++ b/src/Entity/NodesSources.php @@ -4,10 +4,9 @@ namespace RZ\Roadiz\CoreBundle\Entity; -use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter as BaseFilter; +use ApiPlatform\Core\Serializer\Filter\PropertyFilter; use ApiPlatform\Metadata\ApiFilter; -use ApiPlatform\Metadata\ApiProperty; -use ApiPlatform\Serializer\Filter\PropertyFilter; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Criteria; @@ -16,15 +15,13 @@ use Gedmo\Loggable\Loggable; use Gedmo\Mapping\Annotation as Gedmo; use JMS\Serializer\Annotation as Serializer; -use RZ\Roadiz\Contracts\NodeType\NodeTypeFieldInterface; +use RuntimeException; use RZ\Roadiz\Core\AbstractEntities\AbstractEntity; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; use RZ\Roadiz\CoreBundle\Api\Filter as RoadizFilter; use RZ\Roadiz\CoreBundle\Repository\NodesSourcesRepository; -use RZ\Roadiz\Documents\Models\DocumentInterface; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation as SymfonySerializer; -use Symfony\Component\Validator\Constraints as Assert; /** * NodesSources store Node content according to a translation and a NodeType. @@ -60,6 +57,15 @@ class NodesSources extends AbstractEntity implements Loggable #[Serializer\Exclude] protected ?ObjectManager $objectManager = null; + /** + * @var Collection + */ + #[ORM\OneToMany(mappedBy: 'nodeSource', targetEntity: Log::class)] + #[ORM\OrderBy(['datetime' => 'DESC'])] + #[SymfonySerializer\Ignore] + #[Serializer\Exclude] + protected Collection $logs; + /** * @var Collection */ @@ -69,15 +75,10 @@ class NodesSources extends AbstractEntity implements Loggable protected Collection $redirections; #[ApiFilter(BaseFilter\SearchFilter::class, strategy: "partial")] - #[ORM\Column(name: 'title', type: 'string', length: 250, unique: false, nullable: true)] + #[ORM\Column(name: 'title', type: 'string', unique: false, nullable: true)] #[SymfonySerializer\Groups(['nodes_sources', 'nodes_sources_base', 'log_sources'])] #[Serializer\Groups(['nodes_sources', 'nodes_sources_base', 'log_sources'])] #[Gedmo\Versioned] - #[Assert\Length(max: 250)] - #[ApiProperty( - description: 'Content title', - example: 'This is a title', - )] protected ?string $title = null; #[ApiFilter(BaseFilter\DateFilter::class)] @@ -87,32 +88,26 @@ class NodesSources extends AbstractEntity implements Loggable #[SymfonySerializer\Groups(['nodes_sources', 'nodes_sources_base'])] #[Serializer\Groups(['nodes_sources', 'nodes_sources_base'])] #[Gedmo\Versioned] - #[ApiProperty( - description: 'Content publication date and time', - )] protected ?\DateTime $publishedAt = null; #[ApiFilter(BaseFilter\SearchFilter::class, strategy: "partial")] - #[ORM\Column(name: 'meta_title', type: 'string', length: 150, unique: false)] + #[ORM\Column(name: 'meta_title', type: 'string', unique: false)] #[SymfonySerializer\Groups(['nodes_sources'])] #[Serializer\Groups(['nodes_sources'])] #[Gedmo\Versioned] - #[Assert\Length(max: 150)] - #[ApiProperty( - description: 'Title for search engine optimization, used in HTML title tag', - example: 'This is a title', - )] protected string $metaTitle = ''; + #[ORM\Column(name: 'meta_keywords', type: 'text')] + #[SymfonySerializer\Groups(['nodes_sources'])] + #[Serializer\Groups(['nodes_sources'])] + #[Gedmo\Versioned] + protected string $metaKeywords = ''; + #[ApiFilter(BaseFilter\SearchFilter::class, strategy: "partial")] #[ORM\Column(name: 'meta_description', type: 'text')] #[SymfonySerializer\Groups(['nodes_sources'])] #[Serializer\Groups(['nodes_sources'])] #[Gedmo\Versioned] - #[ApiProperty( - description: 'Description for search engine optimization, used in HTML meta description tag', - example: 'This is a description', - )] protected string $metaDescription = ''; #[ApiFilter(BaseFilter\BooleanFilter::class)] @@ -120,10 +115,6 @@ class NodesSources extends AbstractEntity implements Loggable #[SymfonySerializer\Groups(['nodes_sources'])] #[Serializer\Groups(['nodes_sources'])] #[Gedmo\Versioned] - #[ApiProperty( - description: 'Do not allow robots to index this content, used in HTML meta robots tag', - example: 'false', - )] protected bool $noIndex = false; #[ApiFilter(BaseFilter\SearchFilter::class, properties: [ @@ -141,12 +132,6 @@ class NodesSources extends AbstractEntity implements Loggable "node.createdAt", "node.updatedAt" ])] - #[ApiFilter(BaseFilter\NumericFilter::class, properties: [ - "node.position", - ])] - #[ApiFilter(BaseFilter\RangeFilter::class, properties: [ - "node.position", - ])] #[ApiFilter(BaseFilter\DateFilter::class, properties: [ "node.createdAt", "node.updatedAt" @@ -168,23 +153,20 @@ class NodesSources extends AbstractEntity implements Loggable "node.nodesTags.tag.tagName", ])] #[ORM\ManyToOne(targetEntity: Node::class, cascade: ['persist'], fetch: 'EAGER', inversedBy: 'nodeSources')] - #[ORM\JoinColumn(name: 'node_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + #[ORM\JoinColumn(name: 'node_id', referencedColumnName: 'id', onDelete: 'CASCADE')] #[SymfonySerializer\Groups(['nodes_sources', 'nodes_sources_base', 'log_sources'])] #[Serializer\Groups(['nodes_sources', 'nodes_sources_base', 'log_sources'])] - #[Assert\Valid] - #[Assert\NotNull] - private Node $node; + private ?Node $node = null; #[ApiFilter(BaseFilter\SearchFilter::class, properties: [ "translation.id" => "exact", "translation.locale" => "exact", ])] #[ORM\ManyToOne(targetEntity: Translation::class, inversedBy: 'nodeSources')] - #[ORM\JoinColumn(name: 'translation_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + #[ORM\JoinColumn(name: 'translation_id', referencedColumnName: 'id', onDelete: 'CASCADE')] #[SymfonySerializer\Groups(['translation_base'])] #[Serializer\Groups(['translation_base'])] - #[Assert\NotNull] - private TranslationInterface $translation; + private ?TranslationInterface $translation = null; /** * @var Collection @@ -223,10 +205,14 @@ public function __construct(Node $node, TranslationInterface $translation) $this->translation = $translation; $this->urlAliases = new ArrayCollection(); $this->documentsByFields = new ArrayCollection(); + $this->logs = new ArrayCollection(); $this->redirections = new ArrayCollection(); } - #[Serializer\Exclude] + /** + * @inheritDoc + * @Serializer\Exclude + */ public function injectObjectManager(ObjectManager $objectManager): void { $this->objectManager = $objectManager; @@ -235,18 +221,28 @@ public function injectObjectManager(ObjectManager $objectManager): void #[ORM\PreUpdate] public function preUpdate(): void { - $this->getNode()->setUpdatedAt(new \DateTime("now")); + $this->getNode()?->setUpdatedAt(new \DateTime("now")); } - public function getNode(): Node + /** + * @return Node|null + */ + public function getNode(): ?Node { return $this->node; } - public function setNode(Node $node): NodesSources + /** + * @param Node|null $node + * + * @return $this + */ + public function setNode(Node $node = null): NodesSources { $this->node = $node; - $node->addNodeSources($this); + if (null !== $node) { + $node->addNodeSources($this); + } return $this; } @@ -265,16 +261,17 @@ public function addUrlAlias(UrlAlias $urlAlias): NodesSources return $this; } - public function clearDocumentsByFields(NodeTypeFieldInterface $field): NodesSources + public function clearDocumentsByFields(NodeTypeField $nodeTypeField): NodesSources { $toRemoveCollection = $this->getDocumentsByFields()->filter( - function (NodesSourcesDocuments $element) use ($field) { - return $element->getFieldName() === $field->getName(); + function (NodesSourcesDocuments $element) use ($nodeTypeField) { + return $element->getField()->getId() === $nodeTypeField->getId(); } ); /** @var NodesSourcesDocuments $toRemove */ foreach ($toRemoveCollection as $toRemove) { $this->getDocumentsByFields()->removeElement($toRemove); + $toRemove->setNodeSource(null); } return $this; @@ -288,24 +285,6 @@ public function getDocumentsByFields(): Collection return $this->documentsByFields; } - /** - * Get at least one document to represent this node-source as image. - * - * @return DocumentInterface|null - */ - #[SymfonySerializer\Ignore] - public function getOneDisplayableDocument(): ?DocumentInterface - { - return $this->getDocumentsByFields()->filter(function (NodesSourcesDocuments $nsd) { - return null !== $nsd->getDocument() && - !$nsd->getDocument()->isPrivate() && - ($nsd->getDocument()->isImage() || $nsd->getDocument()->isSvg()) && - $nsd->getDocument()->isProcessable(); - })->map(function (NodesSourcesDocuments $nsd) { - return $nsd->getDocument(); - })->first() ?: null; - } - /** * @param Collection $documentsByFields * @@ -313,6 +292,9 @@ public function getOneDisplayableDocument(): ?DocumentInterface */ public function setDocumentsByFields(Collection $documentsByFields): NodesSources { + foreach ($this->documentsByFields as $documentsByField) { + $documentsByField->setNodeSource(null); + } $this->documentsByFields->clear(); foreach ($documentsByFields as $documentsByField) { if (!$this->hasNodesSourcesDocuments($documentsByField)) { @@ -334,7 +316,7 @@ public function hasNodesSourcesDocuments(NodesSourcesDocuments $nodesSourcesDocu function ($key, NodesSourcesDocuments $element) use ($nodesSourcesDocuments) { return $nodesSourcesDocuments->getDocument()->getId() !== null && $element->getDocument()->getId() === $nodesSourcesDocuments->getDocument()->getId() && - $element->getFieldName() === $nodesSourcesDocuments->getFieldName(); + $element->getField()->getId() === $nodesSourcesDocuments->getField()->getId(); } ); } @@ -360,14 +342,14 @@ public function addDocumentsByFields(NodesSourcesDocuments $nodesSourcesDocument * * @return Document[] */ - public function getDocumentsByFieldsWithField(NodeTypeFieldInterface $field): array + public function getDocumentsByFieldsWithField(NodeTypeField $field): array { $criteria = Criteria::create(); $criteria->orderBy(['position' => 'ASC']); return $this->getDocumentsByFields() ->matching($criteria) ->filter(function (NodesSourcesDocuments $element) use ($field) { - return $element->getFieldName() === $field->getName(); + return $element->getField() === $field; }) ->map(function (NodesSourcesDocuments $nodesSourcesDocuments) { return $nodesSourcesDocuments->getDocument(); @@ -387,7 +369,7 @@ public function getDocumentsByFieldsWithName(string $fieldName): array return $this->getDocumentsByFields() ->matching($criteria) ->filter(function (NodesSourcesDocuments $element) use ($fieldName) { - return $element->getFieldName() === $fieldName; + return $element->getField()->getName() === $fieldName; }) ->map(function (NodesSourcesDocuments $nodesSourcesDocuments) { return $nodesSourcesDocuments->getDocument(); @@ -396,6 +378,27 @@ public function getDocumentsByFieldsWithName(string $fieldName): array ; } + /** + * Logs related to this node-source. + * + * @return Collection + */ + public function getLogs(): Collection + { + return $this->logs; + } + + /** + * @param Collection $logs + * @return $this + */ + public function setLogs(Collection $logs): NodesSources + { + $this->logs = $logs; + + return $this; + } + /** * @return Collection */ @@ -452,6 +455,26 @@ public function setMetaTitle(?string $metaTitle): NodesSources return $this; } + /** + * @return string + */ + public function getMetaKeywords(): string + { + return $this->metaKeywords; + } + + /** + * @param string|null $metaKeywords + * + * @return $this + */ + public function setMetaKeywords(?string $metaKeywords): NodesSources + { + $this->metaKeywords = null !== $metaKeywords ? trim($metaKeywords) : ''; + + return $this; + } + /** * @return string */ @@ -567,6 +590,9 @@ public function setTitle(?string $title): NodesSources */ public function getTranslation(): TranslationInterface { + if (null === $this->translation) { + throw new RuntimeException('Node source translation cannot be null.'); + } return $this->translation; } @@ -614,37 +640,6 @@ public function isReachable(): bool return $this->getNode()->getNodeType()->isReachable(); } - /** - * Returns current listing sort options OR parent node's if parent node is hiding children. - * - * @return array - */ - #[Serializer\Groups(['node_listing'])] - #[SymfonySerializer\Groups(['node_listing'])] - public function getListingSortOptions(): array - { - if (null !== $this->getParent() && $this->getParent()->getNode()->isHidingChildren()) { - return $this->getParent()->getListingSortOptions(); - } - return match ($this->getNode()->getChildrenOrder()) { - 'position' => [ - 'node.position' => $this->getNode()->getChildrenOrderDirection() - ], - 'nodeName' => [ - 'node.nodeName' => $this->getNode()->getChildrenOrderDirection() - ], - 'createdAt' => [ - 'node.createdAt' => $this->getNode()->getChildrenOrderDirection() - ], - 'updatedAt' => [ - 'node.updatedAt' => $this->getNode()->getChildrenOrderDirection() - ], - default => [ - 'publishedAt' => $this->getNode()->getChildrenOrderDirection() - ], - }; - } - /** * After clone method. * @@ -664,6 +659,8 @@ public function __clone() } // Clear url-aliases before cloning. $this->urlAliases->clear(); + // Clear logs before cloning. + $this->logs->clear(); } } } diff --git a/src/Entity/NodesSourcesDocuments.php b/src/Entity/NodesSourcesDocuments.php index 6842bba9..4e3fadc3 100644 --- a/src/Entity/NodesSourcesDocuments.php +++ b/src/Entity/NodesSourcesDocuments.php @@ -8,7 +8,6 @@ use RZ\Roadiz\Contracts\NodeType\NodeTypeFieldInterface; use RZ\Roadiz\Core\AbstractEntities\AbstractPositioned; use RZ\Roadiz\CoreBundle\Repository\NodesSourcesDocumentsRepository; -use Symfony\Component\Validator\Constraints as Assert; /** * Describes a complex ManyToMany relation @@ -18,15 +17,13 @@ ORM\Entity(repositoryClass: NodesSourcesDocumentsRepository::class), ORM\Table(name: "nodes_sources_documents"), ORM\Index(columns: ["position"]), - ORM\Index(columns: ["ns_id", "field_name"], name: "nsdoc_field"), - ORM\Index(columns: ["ns_id", "field_name", "position"], name: "nsdoc_field_position") + ORM\Index(columns: ["ns_id", "node_type_field_id"], name: "nsdoc_field"), + ORM\Index(columns: ["ns_id", "node_type_field_id", "position"], name: "nsdoc_field_position") ] class NodesSourcesDocuments extends AbstractPositioned { - use FieldAwareEntityTrait; - /** - * @var NodesSources + * @var NodesSources|null */ #[ORM\ManyToOne( targetEntity: NodesSources::class, @@ -34,12 +31,11 @@ class NodesSourcesDocuments extends AbstractPositioned fetch: 'EAGER', inversedBy: 'documentsByFields' )] - #[Assert\NotNull] - #[ORM\JoinColumn(name: 'ns_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] - protected NodesSources $nodeSource; + #[ORM\JoinColumn(name: 'ns_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + protected ?NodesSources $nodeSource; /** - * @var Document + * @var Document|null */ #[ORM\ManyToOne( targetEntity: Document::class, @@ -47,40 +43,47 @@ class NodesSourcesDocuments extends AbstractPositioned fetch: 'EAGER', inversedBy: 'nodesSourcesByFields' )] - #[Assert\NotNull] - #[ORM\JoinColumn(name: 'document_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] - protected Document $document; + #[ORM\JoinColumn(name: 'document_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + protected ?Document $document; + + /** + * @var NodeTypeField|null + */ + #[ORM\ManyToOne(targetEntity: NodeTypeField::class)] + #[ORM\JoinColumn(name: 'node_type_field_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + protected ?NodeTypeField $field; /** * Create a new relation between NodeSource, a Document and a NodeTypeField. * * @param NodesSources $nodeSource NodesSources and inherited types - * @param Document $document Document to link - * @param NodeTypeFieldInterface|null $field NodeTypeField + * @param Document $document Document to link + * @param NodeTypeFieldInterface $field NodeTypeField */ - public function __construct(NodesSources $nodeSource, Document $document, ?NodeTypeFieldInterface $field = null) + public function __construct(NodesSources $nodeSource, Document $document, NodeTypeFieldInterface $field) { if (!$field instanceof NodeTypeField) { throw new \InvalidArgumentException('NodesSourcesDocuments field must be a NodeTypeField instance.'); } $this->nodeSource = $nodeSource; $this->document = $document; - $this->initializeFieldAwareEntityTrait($field); + $this->field = $field; } public function __clone() { if ($this->id) { $this->id = null; + $this->nodeSource = null; } } /** * Gets the value of nodeSource. * - * @return NodesSources + * @return NodesSources|null */ - public function getNodeSource(): NodesSources + public function getNodeSource(): ?NodesSources { return $this->nodeSource; } @@ -88,11 +91,11 @@ public function getNodeSource(): NodesSources /** * Sets the value of nodeSource. * - * @param NodesSources $nodeSource the node source + * @param NodesSources|null $nodeSource the node source * * @return self */ - public function setNodeSource(NodesSources $nodeSource): NodesSourcesDocuments + public function setNodeSource(?NodesSources $nodeSource): NodesSourcesDocuments { $this->nodeSource = $nodeSource; @@ -102,9 +105,9 @@ public function setNodeSource(NodesSources $nodeSource): NodesSourcesDocuments /** * Gets the value of document. * - * @return Document + * @return Document|null */ - public function getDocument(): Document + public function getDocument(): ?Document { return $this->document; } @@ -112,14 +115,38 @@ public function getDocument(): Document /** * Sets the value of document. * - * @param Document $document the document + * @param Document|null $document the document * * @return self */ - public function setDocument(Document $document): NodesSourcesDocuments + public function setDocument(?Document $document): NodesSourcesDocuments { $this->document = $document; return $this; } + + /** + * Gets the value of field. + * + * @return NodeTypeField|null + */ + public function getField(): ?NodeTypeField + { + return $this->field; + } + + /** + * Sets the value of field. + * + * @param NodeTypeField|null $field the field + * + * @return self + */ + public function setField(?NodeTypeField $field): NodesSourcesDocuments + { + $this->field = $field; + + return $this; + } } diff --git a/src/Entity/NodesToNodes.php b/src/Entity/NodesToNodes.php index b80836c9..ebcb4b6c 100644 --- a/src/Entity/NodesToNodes.php +++ b/src/Entity/NodesToNodes.php @@ -5,7 +5,6 @@ namespace RZ\Roadiz\CoreBundle\Entity; use Doctrine\ORM\Mapping as ORM; -use RZ\Roadiz\Contracts\NodeType\NodeTypeFieldInterface; use RZ\Roadiz\Core\AbstractEntities\AbstractPositioned; use RZ\Roadiz\CoreBundle\Repository\NodesToNodesRepository; @@ -17,43 +16,62 @@ ORM\Entity(repositoryClass: NodesToNodesRepository::class), ORM\Table(name: "nodes_to_nodes"), ORM\Index(columns: ["position"]), - ORM\Index(columns: ["node_a_id", "field_name"], name: "node_a_field"), - ORM\Index(columns: ["node_a_id", "field_name", "position"], name: "node_a_field_position"), - ORM\Index(columns: ["node_b_id", "field_name"], name: "node_b_field"), - ORM\Index(columns: ["node_b_id", "field_name", "position"], name: "node_b_field_position") + ORM\Index(columns: ["node_a_id", "node_type_field_id"], name: "node_a_field"), + ORM\Index(columns: ["node_a_id", "node_type_field_id", "position"], name: "node_a_field_position"), + ORM\Index(columns: ["node_b_id", "node_type_field_id"], name: "node_b_field"), + ORM\Index(columns: ["node_b_id", "node_type_field_id", "position"], name: "node_b_field_position") ] class NodesToNodes extends AbstractPositioned { - use FieldAwareEntityTrait; - + /** + * @var Node|null + */ #[ORM\ManyToOne(targetEntity: Node::class, cascade: ['persist'], fetch: 'EAGER', inversedBy: 'bNodes')] - #[ORM\JoinColumn(name: 'node_a_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] - protected Node $nodeA; + #[ORM\JoinColumn(name: 'node_a_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + protected ?Node $nodeA; + /** + * @var Node|null + */ #[ORM\ManyToOne(targetEntity: Node::class, cascade: ['persist'], fetch: 'EAGER', inversedBy: 'aNodes')] - #[ORM\JoinColumn(name: 'node_b_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] - protected Node $nodeB; + #[ORM\JoinColumn(name: 'node_b_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + protected ?Node $nodeB; + + /** + * @var NodeTypeField|null + */ + #[ORM\ManyToOne(targetEntity: NodeTypeField::class)] + #[ORM\JoinColumn(name: 'node_type_field_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + protected ?NodeTypeField $field; - public function __construct(Node $nodeA, Node $nodeB, ?NodeTypeFieldInterface $field = null) + /** + * Create a new relation between two Nodes and a NodeTypeField. + * + * @param Node $nodeA + * @param Node $nodeB + * @param NodeTypeField $field NodeTypeField + */ + public function __construct(Node $nodeA, Node $nodeB, NodeTypeField $field) { $this->nodeA = $nodeA; $this->nodeB = $nodeB; - $this->initializeFieldAwareEntityTrait($field); + $this->field = $field; } public function __clone() { if ($this->id) { $this->id = null; + $this->nodeA = null; } } /** * Gets the value of nodeA. * - * @return Node + * @return Node|null */ - public function getNodeA(): Node + public function getNodeA(): ?Node { return $this->nodeA; } @@ -61,11 +79,11 @@ public function getNodeA(): Node /** * Sets the value of nodeA. * - * @param Node $nodeA the node + * @param Node|null $nodeA the node * * @return self */ - public function setNodeA(Node $nodeA): NodesToNodes + public function setNodeA(?Node $nodeA): NodesToNodes { $this->nodeA = $nodeA; @@ -75,9 +93,9 @@ public function setNodeA(Node $nodeA): NodesToNodes /** * Gets the value of nodeB. * - * @return Node + * @return Node|null */ - public function getNodeB(): Node + public function getNodeB(): ?Node { return $this->nodeB; } @@ -85,13 +103,38 @@ public function getNodeB(): Node /** * Sets the value of nodeB. * - * @param Node $nodeB the node + * @param Node|null $nodeB the node * * @return self */ - public function setNodeB(Node $nodeB): NodesToNodes + public function setNodeB(?Node $nodeB): NodesToNodes { $this->nodeB = $nodeB; + + return $this; + } + + /** + * Gets the value of field. + * + * @return NodeTypeField|null + */ + public function getField(): ?NodeTypeField + { + return $this->field; + } + + /** + * Sets the value of field. + * + * @param NodeTypeField|null $field the field + * + * @return self + */ + public function setField(?NodeTypeField $field): NodesToNodes + { + $this->field = $field; + return $this; } } diff --git a/src/Entity/Realm.php b/src/Entity/Realm.php index 9cf68e49..118fdab2 100644 --- a/src/Entity/Realm.php +++ b/src/Entity/Realm.php @@ -4,19 +4,20 @@ namespace RZ\Roadiz\CoreBundle\Entity; -use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; +use ApiPlatform\Core\Serializer\Filter\PropertyFilter; use ApiPlatform\Metadata\ApiFilter; -use ApiPlatform\Serializer\Filter\PropertyFilter; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; -use RZ\Roadiz\Core\AbstractEntities\AbstractEntity; -use RZ\Roadiz\CoreBundle\Model\RealmInterface; use RZ\Roadiz\CoreBundle\Repository\RealmRepository; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation as SymfonySerializer; +use RZ\Roadiz\Core\AbstractEntities\AbstractEntity; +use RZ\Roadiz\CoreBundle\Model\RealmInterface; +use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\String\Slugger\AsciiSlugger; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter as BaseFilter; use Symfony\Component\Validator\Constraints as Assert; /** @@ -43,13 +44,11 @@ class Realm extends AbstractEntity implements RealmInterface #[ORM\Column(name: 'type', type: 'string', length: 30)] #[SymfonySerializer\Groups(['get', 'realm'])] #[Serializer\Groups(['get', 'realm'])] - #[Assert\Length(max: 30)] private string $type = RealmInterface::TYPE_PLAIN_PASSWORD; #[ORM\Column(name: 'behaviour', type: 'string', length: 30, nullable: false, options: ['default' => 'none'])] #[SymfonySerializer\Groups(['get', 'realm', 'web_response'])] #[Serializer\Groups(['get', 'realm', 'web_response'])] - #[Assert\Length(max: 30)] private string $behaviour = RealmInterface::BEHAVIOUR_NONE; #[ORM\Column(name: 'name', unique: true)] @@ -65,9 +64,8 @@ class Realm extends AbstractEntity implements RealmInterface * @var string|null * @Serializer\Exclude() */ - #[ORM\Column(name: 'plain_password', type: 'string', length: 255, unique: false, nullable: true)] + #[ORM\Column(name: 'plain_password', unique: false, type: 'string', length: 255, nullable: true)] #[SymfonySerializer\Ignore] - #[Assert\Length(max: 255)] private ?string $plainPassword = null; #[ORM\ManyToOne(targetEntity: Role::class)] @@ -79,7 +77,6 @@ class Realm extends AbstractEntity implements RealmInterface #[ORM\Column(name: 'serialization_group', type: 'string', length: 200, nullable: true)] #[SymfonySerializer\Ignore] #[Serializer\Exclude] - #[Assert\Length(max: 200)] private ?string $serializationGroup = null; /** @@ -150,7 +147,7 @@ public function setSerializationGroup(?string $serializationGroup): Realm { $this->serializationGroup = null !== $serializationGroup ? (new AsciiSlugger())->slug($serializationGroup, '_')->lower()->toString() : - null; + (new AsciiSlugger())->slug($this->getName(), '_')->lower()->toString(); return $this; } diff --git a/src/Entity/RealmNode.php b/src/Entity/RealmNode.php index 8007a309..d51a8b69 100644 --- a/src/Entity/RealmNode.php +++ b/src/Entity/RealmNode.php @@ -11,7 +11,6 @@ use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation as SymfonySerializer; use RZ\Roadiz\CoreBundle\Model\RealmInterface; -use Symfony\Component\Validator\Constraints as Assert; #[ ORM\Entity(repositoryClass: RealmNodeRepository::class), @@ -42,16 +41,15 @@ class RealmNode extends AbstractEntity name: 'realm_id', referencedColumnName: 'id', unique: false, - nullable: false, + nullable: true, onDelete: 'CASCADE' )] #[SymfonySerializer\Ignore] #[Serializer\Exclude] - private Realm $realm; + private ?Realm $realm = null; #[ORM\Column(name: 'inheritance_type', type: 'string', length: 10, nullable: false)] #[SymfonySerializer\Ignore] - #[Assert\Length(max: 10)] #[Serializer\Exclude] private string $inheritanceType = RealmInterface::INHERITANCE_AUTO; @@ -74,18 +72,18 @@ public function setNode(Node $node): RealmNode } /** - * @return Realm + * @return Realm|null */ - public function getRealm(): Realm + public function getRealm(): ?Realm { return $this->realm; } /** - * @param Realm $realm + * @param Realm|null $realm * @return RealmNode */ - public function setRealm(Realm $realm): RealmNode + public function setRealm(?Realm $realm): RealmNode { $this->realm = $realm; return $this; diff --git a/src/Entity/Redirection.php b/src/Entity/Redirection.php index 469222ef..2428d2e9 100644 --- a/src/Entity/Redirection.php +++ b/src/Entity/Redirection.php @@ -18,26 +18,25 @@ ORM\Entity(repositoryClass: RedirectionRepository::class), ORM\Table(name: "redirections"), ORM\HasLifecycleCallbacks, - UniqueEntity(fields: ["query"]), - ORM\Index(columns: ["use_count"], name: 'redirection_use_count'), - ORM\Index(columns: ["created_at"], name: "redirection_created_at"), - ORM\Index(columns: ["updated_at"], name: "redirection_updated_at"), + UniqueEntity(fields: ["query"]) ] class Redirection extends AbstractDateTimed { + /** + * @var string + */ #[ORM\Column(type: 'string', length: 255, unique: true)] #[Assert\NotBlank] #[Assert\Length(max: 255)] private string $query = ""; + /** + * @var string|null + */ #[ORM\Column(name: 'redirectUri', type: 'text', length: 2048, nullable: true)] #[Assert\Length(max: 2048)] private ?string $redirectUri = null; - #[ORM\Column(name: 'use_count', type: 'integer', nullable: false, options: ['default' => 0])] - #[Assert\Length(max: 2048)] - private int $useCount = 0; - /** * @var NodesSources|null */ @@ -60,12 +59,12 @@ public function getQuery(): string } /** - * @param string|null $query + * @param string $query * @return Redirection */ - public function setQuery(?string $query): Redirection + public function setQuery($query): Redirection { - $this->query = $query ?? ''; + $this->query = $query; return $this; } @@ -81,7 +80,7 @@ public function getRedirectUri(): ?string * @param string|null $redirectUri * @return Redirection */ - public function setRedirectUri(?string $redirectUri): Redirection + public function setRedirectUri($redirectUri): Redirection { $this->redirectUri = $redirectUri; return $this; @@ -141,18 +140,4 @@ public function __construct() $this->type = Response::HTTP_MOVED_PERMANENTLY; $this->initAbstractDateTimed(); } - - /** - * @return int - */ - public function getUseCount(): int - { - return $this->useCount; - } - - public function incrementUseCount(): self - { - $this->useCount++; - return $this; - } } diff --git a/src/Entity/Role.php b/src/Entity/Role.php index d1a03ca8..593b2dc4 100644 --- a/src/Entity/Role.php +++ b/src/Entity/Role.php @@ -36,7 +36,7 @@ class Role implements PersistableInterface ] protected ?int $id = null; - #[ORM\Column(type: 'string', length: 250, unique: true)] + #[ORM\Column(type: 'string', unique: true)] #[SymfonySerializer\Groups(['user', 'role', 'group'])] #[Serializer\Groups(['user', 'role', 'group'])] #[Assert\NotNull] diff --git a/src/Entity/Setting.php b/src/Entity/Setting.php index 2670a463..6fa25d8b 100644 --- a/src/Entity/Setting.php +++ b/src/Entity/Setting.php @@ -54,7 +54,7 @@ class Setting extends AbstractEntity AbstractField::MULTIPLE_T => 'multiple-choice.type', ]; - #[ORM\Column(type: 'string', length: 250, unique: true)] + #[ORM\Column(type: 'string', unique: true)] #[SymfonySerializer\Groups(['setting', 'nodes_sources'])] #[Serializer\Groups(['setting', 'nodes_sources'])] #[Assert\NotBlank] @@ -71,14 +71,29 @@ class Setting extends AbstractEntity #[Serializer\Groups(['setting', 'nodes_sources'])] private ?string $value = null; + /** + * Holds clear setting value after value is decoded by postLoad Doctrine event. + * + * READ ONLY: Not persisted value to hold clear value if setting is encrypted. + */ + #[SymfonySerializer\Ignore] + #[Serializer\Exclude] + private ?string $clearValue = null; + #[ORM\Column(type: 'boolean', nullable: false, options: ['default' => true])] #[SymfonySerializer\Groups(['setting'])] #[Serializer\Groups(['setting'])] private bool $visible = true; + #[ORM\Column(type: 'boolean', nullable: false, options: ['default' => false])] + #[SymfonySerializer\Groups(['setting'])] + #[Serializer\Groups(['setting'])] + private bool $encrypted = false; + #[ORM\ManyToOne( targetEntity: SettingGroup::class, cascade: ['persist', 'merge'], + fetch: 'EAGER', inversedBy: 'settings' )] #[ORM\JoinColumn(name: 'setting_group_id', referencedColumnName: 'id', onDelete: 'SET NULL')] @@ -118,7 +133,7 @@ public function getName(): string * * @return $this */ - public function setName(?string $name): self + public function setName(?string $name) { $this->name = trim(\mb_strtolower($name ?? '')); $this->name = (new UnicodeString($this->name)) @@ -158,39 +173,50 @@ public function getRawValue(): ?string } /** + * Getter for setting value OR clear value, if encrypted. + * * @return string|bool|\DateTime|int|null * @throws \Exception */ #[SymfonySerializer\Ignore] - public function getValue(): string|bool|\DateTime|int|null + public function getValue() { + if ($this->isEncrypted()) { + $value = $this->clearValue; + } else { + $value = $this->value; + } + if ($this->getType() == AbstractField::BOOLEAN_T) { - return (bool) $this->value; + return (bool) $value; } - if (null !== $this->value) { + if (null !== $value) { if ($this->getType() == AbstractField::DATETIME_T) { - return new \DateTime($this->value); + return new \DateTime($value); } if ($this->getType() == AbstractField::DOCUMENTS_T) { - return (int) $this->value; + return (int) $value; } } - return $this->value; + return $value; } /** - * @param mixed $value + * @param null|mixed $value * * @return $this */ - public function setValue(mixed $value): self + public function setValue($value) { if (null === $value) { $this->value = null; - } elseif ($value instanceof \DateTimeInterface) { - $this->value = $value->format('c'); // $value is instance of \DateTime + } elseif ( + ($this->getType() === AbstractField::DATETIME_T || $this->getType() === AbstractField::DATE_T) && + $value instanceof \DateTimeInterface + ) { + $this->value = $value->format('c'); } else { $this->value = (string) $value; } @@ -198,6 +224,26 @@ public function setValue(mixed $value): self return $this; } + /** + * @return bool + */ + public function isEncrypted(): bool + { + return $this->encrypted; + } + + /** + * @param bool $encrypted + * + * @return Setting + */ + public function setEncrypted(bool $encrypted): Setting + { + $this->encrypted = $encrypted; + + return $this; + } + /** * @return int */ @@ -211,13 +257,27 @@ public function getType(): int * * @return $this */ - public function setType(int $type): self + public function setType(int $type) { $this->type = $type; return $this; } + /** + * Holds clear setting value after value is decoded by postLoad Doctrine event. + * + * @param string|null $clearValue + * + * @return Setting + */ + public function setClearValue(?string $clearValue): Setting + { + $this->clearValue = $clearValue; + + return $this; + } + /** * @return boolean */ @@ -231,7 +291,7 @@ public function isVisible(): bool * * @return $this */ - public function setVisible(bool $visible): self + public function setVisible(bool $visible) { $this->visible = $visible; @@ -251,7 +311,7 @@ public function getSettingGroup(): ?SettingGroup * * @return $this */ - public function setSettingGroup(?SettingGroup $settingGroup): self + public function setSettingGroup(?SettingGroup $settingGroup) { $this->settingGroup = $settingGroup; @@ -271,7 +331,7 @@ public function getDefaultValues(): ?string * * @return Setting */ - public function setDefaultValues(?string $defaultValues): self + public function setDefaultValues(?string $defaultValues) { $this->defaultValues = $defaultValues; diff --git a/src/Entity/SettingGroup.php b/src/Entity/SettingGroup.php index a8d73195..297c7d67 100644 --- a/src/Entity/SettingGroup.php +++ b/src/Entity/SettingGroup.php @@ -29,7 +29,7 @@ class SettingGroup extends AbstractEntity #[Serializer\Groups(['setting', 'setting_group'])] protected bool $inMenu = false; - #[ORM\Column(type: 'string', length: 250, unique: true)] + #[ORM\Column(type: 'string', unique: true)] #[SymfonySerializer\Groups(['setting', 'setting_group'])] #[Serializer\Groups(['setting', 'setting_group'])] #[Assert\NotNull] diff --git a/src/Entity/Tag.php b/src/Entity/Tag.php index fc4f8661..f74cd0ac 100644 --- a/src/Entity/Tag.php +++ b/src/Entity/Tag.php @@ -4,14 +4,14 @@ namespace RZ\Roadiz\CoreBundle\Entity; -use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter as BaseFilter; +use ApiPlatform\Core\Serializer\Filter\PropertyFilter; use ApiPlatform\Metadata\ApiFilter; -use ApiPlatform\Metadata\ApiProperty; -use ApiPlatform\Serializer\Filter\PropertyFilter; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; +use RZ\Roadiz\Core\AbstractEntities\AbstractDateTimedPositioned; use RZ\Roadiz\Core\AbstractEntities\LeafInterface; use RZ\Roadiz\Core\AbstractEntities\LeafTrait; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; @@ -61,10 +61,6 @@ class Tag extends AbstractDateTimedPositioned implements LeafInterface #[SymfonySerializer\Groups(['tag', 'tag_color', 'color'])] #[Serializer\Groups(['tag', 'tag_color', 'color'])] #[Assert\Length(max: 7)] - #[ApiProperty( - description: 'Tag color in hexadecimal format.', - example: '#ff0000', - )] protected string $color = '#000000'; /** @@ -107,6 +103,7 @@ class Tag extends AbstractDateTimedPositioned implements LeafInterface mappedBy: 'tag', targetEntity: TagTranslation::class, cascade: ['all'], + fetch: 'EAGER', orphanRemoval: true )] #[SymfonySerializer\Groups(['translated_tag'])] @@ -114,17 +111,13 @@ class Tag extends AbstractDateTimedPositioned implements LeafInterface protected Collection $translatedTags; #[ApiFilter(BaseFilter\SearchFilter::class, strategy: "partial")] - #[ORM\Column(name: 'tag_name', type: 'string', length: 250, unique: true)] + #[ORM\Column(name: 'tag_name', type: 'string', unique: true)] #[SymfonySerializer\Ignore] #[Serializer\Groups(['tag'])] #[Serializer\Accessor(getter: "getTagName", setter: "setTagName")] #[Assert\NotNull] #[Assert\NotBlank] #[Assert\Length(max: 250)] - #[ApiProperty( - description: 'Unique tag name (slug) used to build content URL or filter queries.', - example: 'this-is-a-tag-name', - )] private string $tagName = ''; #[SymfonySerializer\Ignore] @@ -135,50 +128,24 @@ class Tag extends AbstractDateTimedPositioned implements LeafInterface #[ORM\Column(type: 'boolean', nullable: false, options: ['default' => true])] #[SymfonySerializer\Groups(['tag', 'tag_base', 'node', 'nodes_sources'])] #[Serializer\Groups(['tag', 'tag_base', 'node', 'nodes_sources'])] - #[ApiProperty( - description: 'Is this tag visible in website?', - example: 'true', - )] private bool $visible = true; - #[ORM\Column(name: 'children_order', type: 'string', length: 60, options: ['default' => 'position'])] + #[ORM\Column(name: 'children_order', type: 'string', options: ['default' => 'position'])] #[SymfonySerializer\Ignore] #[Serializer\Groups(["tag", "tag_children_order"])] #[Assert\Length(max: 60)] - #[ApiProperty( - description: 'This tag children will be sorted by a given field', - example: 'position', - schema: [ - 'type' => 'string', - 'enum' => ['position', 'tagName', 'createdAt', 'updatedAt', 'publishedAt'], - 'example' => 'position' - ], - )] private string $childrenOrder = 'position'; #[ORM\Column(name: 'children_order_direction', type: 'string', length: 4, options: ['default' => 'ASC'])] #[SymfonySerializer\Ignore] #[Serializer\Groups(["tag", "tag_children_order"])] #[Assert\Length(max: 4)] - #[ApiProperty( - description: 'This tag children will be sorted ascendant or descendant', - example: 'ASC', - schema: [ - 'type' => 'string', - 'enum' => ['ASC', 'DESC'], - 'example' => 'ASC' - ], - )] private string $childrenOrderDirection = 'ASC'; #[ApiFilter(BaseFilter\BooleanFilter::class)] #[ORM\Column(type: 'boolean', nullable: false, options: ['default' => false])] #[SymfonySerializer\Ignore] #[Serializer\Groups(["tag"])] - #[ApiProperty( - description: 'Is this tag locked to prevent deletion and renaming?', - example: 'false', - )] private bool $locked = false; /** @@ -502,17 +469,4 @@ public function setParent(?LeafInterface $parent = null): static return $this; } - - - #[ApiProperty( - description: 'Unique tag name (slug) used to build content URL or filter queries.', - example: 'this-is-a-tag-name', - )] - #[SymfonySerializer\SerializedName('slug')] - #[SymfonySerializer\Groups(['tag', 'tag_base', 'node', 'nodes_sources'])] - #[Serializer\Groups(['tag', 'tag_base', 'node', 'nodes_sources'])] - public function getSlug(): string - { - return $this->getTagName(); - } } diff --git a/src/Entity/TagTranslation.php b/src/Entity/TagTranslation.php index 36017fe1..a93b9f45 100644 --- a/src/Entity/TagTranslation.php +++ b/src/Entity/TagTranslation.php @@ -30,7 +30,7 @@ ] class TagTranslation extends AbstractEntity { - #[ORM\Column(type: 'string', length: 250)] + #[ORM\Column(type: 'string')] #[SymfonySerializer\Groups(['tag', 'node', 'nodes_sources'])] #[Serializer\Groups(['tag', 'node', 'nodes_sources'])] #[Assert\NotBlank] @@ -45,16 +45,16 @@ class TagTranslation extends AbstractEntity protected ?string $description = null; #[ORM\ManyToOne(targetEntity: Tag::class, inversedBy: 'translatedTags')] - #[ORM\JoinColumn(name: 'tag_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + #[ORM\JoinColumn(name: 'tag_id', referencedColumnName: 'id', onDelete: 'CASCADE')] #[SymfonySerializer\Ignore] #[Serializer\Exclude] - protected Tag $tag; + protected ?Tag $tag = null; #[ORM\ManyToOne(targetEntity: Translation::class, fetch: 'EXTRA_LAZY', inversedBy: 'tagTranslations')] - #[ORM\JoinColumn(name: 'translation_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + #[ORM\JoinColumn(name: 'translation_id', referencedColumnName: 'id', onDelete: 'CASCADE')] #[SymfonySerializer\Groups(['tag', 'node', 'nodes_sources'])] #[Serializer\Groups(['tag', 'node', 'nodes_sources'])] - protected TranslationInterface $translation; + protected ?TranslationInterface $translation = null; /** * @var Collection @@ -73,22 +73,33 @@ class TagTranslation extends AbstractEntity /** * Create a new TagTranslation with its origin Tag and Translation. * - * @param Tag $original - * @param TranslationInterface $translation + * @param Tag|null $original + * @param TranslationInterface|null $translation */ - public function __construct(Tag $original, TranslationInterface $translation) + public function __construct(Tag $original = null, TranslationInterface $translation = null) { $this->setTag($original); $this->setTranslation($translation); $this->tagTranslationDocuments = new ArrayCollection(); - $this->name = $original->getDirtyTagName() != '' ? $original->getDirtyTagName() : $original->getTagName(); + + if (null !== $original) { + $this->name = $original->getDirtyTagName() != '' ? $original->getDirtyTagName() : $original->getTagName(); + } } + /** + * @return string + */ public function getName(): string { return $this->name; } + /** + * @param string|null $name + * + * @return $this + */ public function setName(?string $name): TagTranslation { $this->name = $name ?? ''; @@ -96,11 +107,19 @@ public function setName(?string $name): TagTranslation return $this; } + /** + * @return string + */ public function getDescription(): ?string { return $this->description; } + /** + * @param string|null $description + * + * @return $this + */ public function setDescription(?string $description): TagTranslation { $this->description = $description; @@ -108,23 +127,48 @@ public function setDescription(?string $description): TagTranslation return $this; } - public function getTag(): Tag + /** + * Gets the value of tag. + * + * @return Tag + */ + public function getTag(): ?Tag { return $this->tag; } - public function setTag(Tag $tag): TagTranslation + /** + * Sets the value of tag. + * + * @param Tag|null $tag the tag + * + * @return self + */ + public function setTag(?Tag $tag): TagTranslation { $this->tag = $tag; + return $this; } - public function getTranslation(): TranslationInterface + /** + * Gets the value of translation. + * + * @return TranslationInterface|null + */ + public function getTranslation(): ?TranslationInterface { return $this->translation; } - public function setTranslation(TranslationInterface $translation): TagTranslation + /** + * Sets the value of translation. + * + * @param TranslationInterface|null $translation the translation + * + * @return self + */ + public function setTranslation(?TranslationInterface $translation): TagTranslation { $this->translation = $translation; @@ -142,12 +186,14 @@ public function __clone() if ($this->id) { $this->id = null; $documents = $this->getDocuments(); - $this->tagTranslationDocuments = new ArrayCollection(); - /** @var TagTranslationDocuments $document */ - foreach ($documents as $document) { - $cloneDocument = clone $document; - $this->tagTranslationDocuments->add($cloneDocument); - $cloneDocument->setTagTranslation($this); + if ($documents !== null) { + $this->tagTranslationDocuments = new ArrayCollection(); + /** @var TagTranslationDocuments $document */ + foreach ($documents as $document) { + $cloneDocument = clone $document; + $this->tagTranslationDocuments->add($cloneDocument); + $cloneDocument->setTagTranslation($this); + } } } } diff --git a/src/Entity/TagTranslationDocuments.php b/src/Entity/TagTranslationDocuments.php index d06683da..ca765fb0 100644 --- a/src/Entity/TagTranslationDocuments.php +++ b/src/Entity/TagTranslationDocuments.php @@ -28,10 +28,10 @@ class TagTranslationDocuments extends AbstractPositioned fetch: 'EAGER', inversedBy: 'tagTranslationDocuments' )] - #[ORM\JoinColumn(name: 'tag_translation_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + #[ORM\JoinColumn(name: 'tag_translation_id', referencedColumnName: 'id', onDelete: 'CASCADE')] #[SymfonySerializer\Ignore] #[Serializer\Exclude] - protected TagTranslation $tagTranslation; + protected ?TagTranslation $tagTranslation = null; #[ORM\ManyToOne( targetEntity: Document::class, @@ -39,18 +39,18 @@ class TagTranslationDocuments extends AbstractPositioned fetch: 'EAGER', inversedBy: 'tagTranslations' )] - #[ORM\JoinColumn(name: 'document_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + #[ORM\JoinColumn(name: 'document_id', referencedColumnName: 'id', onDelete: 'CASCADE')] #[SymfonySerializer\Groups(['tag'])] #[Serializer\Groups(['tag'])] - protected Document $document; + protected ?Document $document = null; /** * Create a new relation between NodeSource, a Document and a NodeTypeField. * - * @param TagTranslation $tagTranslation - * @param Document $document + * @param TagTranslation|null $tagTranslation + * @param Document|null $document */ - public function __construct(TagTranslation $tagTranslation, Document $document) + public function __construct(TagTranslation $tagTranslation = null, Document $document = null) { $this->document = $document; $this->tagTranslation = $tagTranslation; @@ -60,26 +60,44 @@ public function __clone() { if ($this->id) { $this->id = null; + $this->tagTranslation = null; } } - public function getDocument(): Document + /** + * Gets the value of document. + * + * @return Document|null + */ + public function getDocument(): ?Document { return $this->document; } - public function setDocument(Document $document): TagTranslationDocuments + /** + * Sets the value of document. + * + * @param Document|null $document the document + * + * @return self + */ + public function setDocument(?Document $document): TagTranslationDocuments { $this->document = $document; + return $this; } - public function getTagTranslation(): TagTranslation + public function getTagTranslation(): ?TagTranslation { return $this->tagTranslation; } - public function setTagTranslation(TagTranslation $tagTranslation): TagTranslationDocuments + /** + * @param TagTranslation|null $tagTranslation + * @return TagTranslationDocuments + */ + public function setTagTranslation(?TagTranslation $tagTranslation): TagTranslationDocuments { $this->tagTranslation = $tagTranslation; return $this; diff --git a/src/Entity/Theme.php b/src/Entity/Theme.php index 57d5afd6..86807262 100644 --- a/src/Entity/Theme.php +++ b/src/Entity/Theme.php @@ -87,16 +87,15 @@ public function getInformations(): array $class = $this->getClassName(); if (class_exists($class)) { - $nameCallable = [$class, 'getThemeName']; - $authorCallable = [$class, 'getThemeAuthor']; - $copyrightCallable = [$class, 'getThemeCopyright']; - $dirCallable = [$class, 'getThemeDir']; - return [ - 'name' => \is_callable($nameCallable) ? call_user_func($nameCallable) : null, - 'author' => \is_callable($authorCallable) ? call_user_func($authorCallable) : null, - 'copyright' => \is_callable($copyrightCallable) ? call_user_func($copyrightCallable) : null, - 'dir' => \is_callable($dirCallable) ? call_user_func($dirCallable) : null, - ]; + $reflector = new \ReflectionClass($class); + if ($reflector->isSubclassOf('\\RZ\\Roadiz\\CMS\\Controllers\\AppController')) { + return [ + 'name' => call_user_func([$class, 'getThemeName']), + 'author' => call_user_func([$class, 'getThemeAuthor']), + 'copyright' => call_user_func([$class, 'getThemeCopyright']), + 'dir' => call_user_func([$class, 'getThemeDir']) + ]; + } } return []; diff --git a/src/Entity/Translation.php b/src/Entity/Translation.php index 549360e8..d585fed0 100644 --- a/src/Entity/Translation.php +++ b/src/Entity/Translation.php @@ -4,10 +4,9 @@ namespace RZ\Roadiz\CoreBundle\Entity; -use ApiPlatform\Metadata\ApiProperty; -use ApiPlatform\Serializer\Filter\PropertyFilter; +use ApiPlatform\Core\Serializer\Filter\PropertyFilter; use ApiPlatform\Metadata\ApiFilter; -use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter as BaseFilter; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; @@ -544,24 +543,19 @@ class Translation extends AbstractDateTimed implements TranslationInterface protected Collection $folderTranslations; /** - * ISO 639-1 Language locale. + * Language locale * * fr or en for example * * @var string - * @see https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes * @Serializer\Groups({"translation", "document", "nodes_sources", "tag", "attribute", "folder", "log_sources"}) * @Serializer\Type("string") */ - #[ORM\Column(type: 'string', length: 10, unique: true, nullable: false)] + #[ORM\Column(type: 'string', length: 10, unique: true)] #[SymfonySerializer\Ignore] #[Assert\NotBlank] #[Assert\NotNull] #[Assert\Length(max: 10)] - #[ApiProperty( - description: 'Translation ISO 639-1 locale. See https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes', - example: 'fr', - )] private string $locale = ''; /** @@ -572,10 +566,6 @@ class Translation extends AbstractDateTimed implements TranslationInterface #[ORM\Column(name: 'override_locale', type: 'string', length: 10, unique: true, nullable: true)] #[SymfonySerializer\Ignore] #[Assert\Length(max: 10)] - #[ApiProperty( - description: 'Override standard locale with an other one (for example, `uk` instead of `en`)', - example: 'uk', - )] private ?string $overrideLocale = null; /** @@ -583,15 +573,11 @@ class Translation extends AbstractDateTimed implements TranslationInterface * @Serializer\Groups({"translation", "translation_base"}) * @Serializer\Type("string") */ - #[ORM\Column(type: 'string', length: 250, unique: true)] + #[ORM\Column(type: 'string', unique: true)] #[SymfonySerializer\Groups(['translation', 'translation_base'])] #[Assert\NotNull] #[Assert\NotBlank] #[Assert\Length(max: 250)] - #[ApiProperty( - description: 'Translation display name', - example: 'French', - )] private string $name = ''; /** @@ -601,10 +587,6 @@ class Translation extends AbstractDateTimed implements TranslationInterface */ #[ORM\Column(name: 'default_translation', type: 'boolean', nullable: false, options: ['default' => false])] #[SymfonySerializer\Groups(['translation', 'translation_base'])] - #[ApiProperty( - description: 'Is translation default one?', - example: 'true', - )] private bool $defaultTranslation = false; /** @@ -614,10 +596,6 @@ class Translation extends AbstractDateTimed implements TranslationInterface */ #[ORM\Column(type: 'boolean', nullable: false, options: ['default' => true])] #[SymfonySerializer\Groups(['translation', 'translation_base'])] - #[ApiProperty( - description: 'Is translation available publicly?', - example: 'true', - )] private bool $available = true; /** @@ -816,10 +794,6 @@ public function setOverrideLocale(?string $overrideLocale): Translation */ #[SymfonySerializer\SerializedName('locale')] #[SymfonySerializer\Groups(['translation_base'])] - #[ApiProperty( - description: 'Translation ISO 639-1 locale. See https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes', - example: 'fr', - )] public function getPreferredLocale(): string { return !empty($this->overrideLocale) ? $this->overrideLocale : $this->locale; diff --git a/src/Entity/UrlAlias.php b/src/Entity/UrlAlias.php index 9f713fef..b937ab13 100644 --- a/src/Entity/UrlAlias.php +++ b/src/Entity/UrlAlias.php @@ -22,20 +22,29 @@ ] class UrlAlias extends AbstractEntity { - #[ORM\Column(type: 'string', length: 250, unique: true)] + #[ORM\Column(type: 'string', unique: true)] #[SymfonySerializer\Groups(['url_alias'])] #[Serializer\Groups(['url_alias'])] #[Assert\NotNull] #[Assert\NotBlank] - #[Assert\Length(max: 250)] #[RoadizAssert\UniqueNodeName] private string $alias = ''; #[ORM\ManyToOne(targetEntity: NodesSources::class, inversedBy: 'urlAliases')] - #[ORM\JoinColumn(name: 'ns_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + #[ORM\JoinColumn(name: 'ns_id', referencedColumnName: 'id')] #[SymfonySerializer\Ignore] #[Serializer\Exclude] - private NodesSources $nodeSource; + private ?NodesSources $nodeSource = null; + + /** + * Create a new UrlAlias linked to a NodeSource. + * + * @param NodesSources|null $nodeSource + */ + public function __construct(?NodesSources $nodeSource = null) + { + $this->setNodeSource($nodeSource); + } /** * @return string @@ -56,12 +65,19 @@ public function setAlias(string $alias): UrlAlias return $this; } - public function getNodeSource(): NodesSources + /** + * @return NodesSources|null + */ + public function getNodeSource(): ?NodesSources { return $this->nodeSource; } - public function setNodeSource(NodesSources $nodeSource): UrlAlias + /** + * @param NodesSources|null $nodeSource + * @return $this + */ + public function setNodeSource(?NodesSources $nodeSource): UrlAlias { $this->nodeSource = $nodeSource; return $this; diff --git a/src/Entity/User.php b/src/Entity/User.php index a5d39a62..eeb57296 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -10,15 +10,16 @@ use JMS\Serializer\Annotation as Serializer; use Rollerworks\Component\PasswordStrength\Validator\Constraints\PasswordStrength; use RZ\Roadiz\Core\AbstractEntities\AbstractHuman; -use RZ\Roadiz\CoreBundle\Form\Constraint\ValidFacebookName; use RZ\Roadiz\CoreBundle\Repository\UserRepository; use RZ\Roadiz\CoreBundle\Security\User\AdvancedUserInterface; +use RZ\Roadiz\Random\SaltGenerator; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Security\Core\User\EquatableInterface; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Serializer\Annotation as SymfonySerializer; use Symfony\Component\Validator\Constraints as Assert; +use RZ\Roadiz\CoreBundle\Form\Constraint\ValidFacebookName; #[ ORM\Entity(repositoryClass: UserRepository::class), @@ -32,8 +33,6 @@ ORM\Index(columns: ["last_login"], name: "idx_users_last_login"), ORM\Index(columns: ["locked"], name: "idx_users_locked"), ORM\Index(columns: ["locale"], name: "idx_users_locale"), - ORM\Index(columns: ["created_at"], name: "idx_user_created_at"), - ORM\Index(columns: ["updated_at"], name: "idx_user_updated_at"), ORM\HasLifecycleCallbacks, UniqueEntity("email"), UniqueEntity("username") @@ -52,7 +51,7 @@ class User extends AbstractHuman implements UserInterface, AdvancedUserInterface * @Serializer\Groups({"user_personal", "human"}) * @var string|null */ - #[ORM\Column(type: 'string', length: 200, unique: true, nullable: false)] + #[ORM\Column(type: 'string', unique: true, nullable: false)] #[SymfonySerializer\Groups(['user_personal', 'human'])] #[Assert\NotNull] #[Assert\NotBlank] @@ -68,10 +67,9 @@ class User extends AbstractHuman implements UserInterface, AdvancedUserInterface #[SymfonySerializer\Ignore] protected bool $sendCreationConfirmationEmail = false; - #[ORM\Column(name: 'facebook_name', type: 'string', length: 128, unique: false, nullable: true)] + #[ORM\Column(name: 'facebook_name', type: 'string', unique: false, nullable: true)] #[SymfonySerializer\Groups(['user_social'])] #[Serializer\Groups(['user_social'])] - #[Assert\Length(max: 128)] #[ValidFacebookName] protected ?string $facebookName = null; @@ -96,9 +94,8 @@ class User extends AbstractHuman implements UserInterface, AdvancedUserInterface * @Serializer\Groups({"user_security"}) * @var string|null */ - #[ORM\Column(name: 'confirmation_token', type: 'string', length: 128, unique: true, nullable: true)] + #[ORM\Column(name: 'confirmation_token', type: 'string', unique: true, nullable: true)] #[SymfonySerializer\Groups(['user_security'])] - #[Assert\Length(max: 128)] protected ?string $confirmationToken = null; /** @@ -113,20 +110,27 @@ class User extends AbstractHuman implements UserInterface, AdvancedUserInterface * @Serializer\Groups({"user_personal", "log_user"}) * @var string */ - #[ORM\Column(type: 'string', length: 200, unique: true)] + #[ORM\Column(type: 'string', unique: true)] #[SymfonySerializer\Groups(['user_personal', 'log_user'])] #[Assert\NotNull] #[Assert\NotBlank] #[Assert\Length(max: 200)] private string $username = ''; + /** + * The salt to use for hashing. + */ + #[ORM\Column(name: 'salt', type: 'string')] + #[SymfonySerializer\Ignore] + #[Serializer\Exclude] + private string $salt = ''; + /** * Encrypted password. */ - #[ORM\Column(type: 'string', length: 128, nullable: false)] + #[ORM\Column(type: 'string', nullable: false)] #[SymfonySerializer\Ignore] #[Serializer\Exclude] - #[Assert\Length(max: 128)] private string $password = ''; /** @@ -220,7 +224,6 @@ class User extends AbstractHuman implements UserInterface, AdvancedUserInterface */ #[ORM\Column(name: 'locale', type: 'string', length: 7, nullable: true)] #[SymfonySerializer\Groups(['user'])] - #[Assert\Length(max: 7)] private ?string $locale = null; public function __construct() @@ -229,6 +232,9 @@ public function __construct() $this->groups = new ArrayCollection(); $this->sendCreationConfirmationEmail(false); $this->initAbstractDateTimed(); + + $saltGenerator = new SaltGenerator(); + $this->setSalt($saltGenerator->generateSalt()); } /** @@ -348,7 +354,17 @@ public function setPictureUrl(?string $pictureUrl): User */ public function getSalt(): ?string { - return null; + return $this->salt; + } + + /** + * @param string $salt + * @return $this + */ + public function setSalt(string $salt): User + { + $this->salt = $salt; + return $this; } /** @@ -858,8 +874,8 @@ public function __serialize(): array { return [ $this->password, + $this->salt, $this->username, - $this->getSalt(), $this->enabled, $this->id, $this->email, @@ -875,11 +891,10 @@ public function __serialize(): array public function __unserialize(array $data): void { - $salt = null; [ $this->password, + $this->salt, $this->username, - $salt, $this->enabled, $this->id, $this->email, @@ -946,6 +961,10 @@ public function isEqualTo(UserInterface $user): bool return false; } + if ($this->getSalt() !== $user->getSalt()) { + return false; + } + if ($this->getUsername() !== $user->getUsername()) { return false; } diff --git a/src/Entity/Webhook.php b/src/Entity/Webhook.php index 840cff92..22f2caaa 100644 --- a/src/Entity/Webhook.php +++ b/src/Entity/Webhook.php @@ -49,7 +49,6 @@ class Webhook extends AbstractDateTimed implements WebhookInterface */ #[ORM\Column(name: 'message_type', type: 'string', length: 255, nullable: true)] #[Serializer\Type('string')] - #[Assert\Length(max: 255)] protected ?string $messageType = null; /** diff --git a/src/EntityApi/AbstractApi.php b/src/EntityApi/AbstractApi.php index 23d662b9..6621841f 100644 --- a/src/EntityApi/AbstractApi.php +++ b/src/EntityApi/AbstractApi.php @@ -38,6 +38,7 @@ abstract public function getBy(array $criteria); * Return one entity matching criteria array. * * @param array $criteria + * * @return mixed */ abstract public function getOneBy(array $criteria); @@ -46,6 +47,7 @@ abstract public function getOneBy(array $criteria); * Count entities matching criteria array. * * @param array $criteria + * * @return int */ abstract public function countBy(array $criteria); diff --git a/src/EntityApi/NodeSourceApi.php b/src/EntityApi/NodeSourceApi.php index d3821a8b..e3dc5a8f 100644 --- a/src/EntityApi/NodeSourceApi.php +++ b/src/EntityApi/NodeSourceApi.php @@ -4,7 +4,7 @@ namespace RZ\Roadiz\CoreBundle\EntityApi; -use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Tools\Pagination\Paginator; use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\NodeType; @@ -15,16 +15,16 @@ class NodeSourceApi extends AbstractApi /** * @var class-string */ - protected string $nodeSourceClassName = NodesSources::class; + protected string $repository = NodesSources::class; /** * @param array|null $criteria - * @return class-string + * @return string */ - protected function getNodeSourceClassName(array $criteria = null): string + protected function getRepositoryName(array $criteria = null) { if (isset($criteria['node.nodeType']) && $criteria['node.nodeType'] instanceof NodeType) { - $this->nodeSourceClassName = $criteria['node.nodeType']->getSourceEntityFullQualifiedClassName(); + $this->repository = $criteria['node.nodeType']->getSourceEntityFullQualifiedClassName(); unset($criteria['node.nodeType']); } elseif ( isset($criteria['node.nodeType']) && @@ -32,22 +32,21 @@ protected function getNodeSourceClassName(array $criteria = null): string count($criteria['node.nodeType']) === 1 && $criteria['node.nodeType'][0] instanceof NodeType ) { - $this->nodeSourceClassName = $criteria['node.nodeType'][0]->getSourceEntityFullQualifiedClassName(); + $this->repository = $criteria['node.nodeType'][0]->getSourceEntityFullQualifiedClassName(); unset($criteria['node.nodeType']); } else { - $this->nodeSourceClassName = NodesSources::class; + $this->repository = NodesSources::class; } - return $this->nodeSourceClassName; + return $this->repository; } /** - * @return NodesSourcesRepository + * @return NodesSourcesRepository|EntityRepository */ - public function getRepository(): NodesSourcesRepository + public function getRepository() { - // @phpstan-ignore-next-line - return $this->managerRegistry->getRepository($this->nodeSourceClassName); + return $this->managerRegistry->getRepository($this->repository); } /** @@ -63,7 +62,7 @@ public function getBy( ?int $limit = null, ?int $offset = null ) { - $this->getNodeSourceClassName($criteria); + $this->getRepositoryName($criteria); return $this->getRepository() ->findBy( @@ -82,7 +81,8 @@ public function getBy( */ public function countBy(array $criteria) { - $this->getNodeSourceClassName($criteria); + $this->getRepositoryName($criteria); + return $this->getRepository() ->countBy( $criteria @@ -93,11 +93,11 @@ public function countBy(array $criteria) * @param array $criteria * @param array|null $order * @return null|NodesSources - * @throws NonUniqueResultException */ public function getOneBy(array $criteria, array $order = null) { - $this->getNodeSourceClassName($criteria); + $this->getRepositoryName($criteria); + return $this->getRepository() ->findOneBy( $criteria, @@ -123,13 +123,19 @@ public function searchBy( bool $onlyVisible = false, array $additionalCriteria = [] ) { - return $this->getRepository() - ->findByTextQuery( - $textQuery, - $limit, - $nodeTypes, - $onlyVisible, - $additionalCriteria - ); + $repository = $this->getRepository(); + + if ($repository instanceof NodesSourcesRepository) { + return $this->getRepository() + ->findByTextQuery( + $textQuery, + $limit, + $nodeTypes, + $onlyVisible, + $additionalCriteria + ); + } + + return []; } } diff --git a/src/EntityHandler/CustomFormFieldHandler.php b/src/EntityHandler/CustomFormFieldHandler.php index e7b8cbd5..368bb8fd 100644 --- a/src/EntityHandler/CustomFormFieldHandler.php +++ b/src/EntityHandler/CustomFormFieldHandler.php @@ -11,29 +11,38 @@ /** * Handle operations with customForms fields entities. */ -final class CustomFormFieldHandler extends AbstractHandler +class CustomFormFieldHandler extends AbstractHandler { private ?CustomFormField $customFormField = null; + private CustomFormHandler $customFormHandler; + /** + * @return CustomFormField + */ + public function getCustomFormField(): ?CustomFormField + { + return $this->customFormField; + } /** * @param CustomFormField $customFormField * @return $this */ - public function setCustomFormField(CustomFormField $customFormField): self + public function setCustomFormField(CustomFormField $customFormField) { $this->customFormField = $customFormField; return $this; } /** + * Create a new custom-form-field handler with custom-form-field to handle. + * * @param ObjectManager $objectManager * @param CustomFormHandler $customFormHandler */ - public function __construct( - ObjectManager $objectManager, - private readonly CustomFormHandler $customFormHandler - ) { + public function __construct(ObjectManager $objectManager, CustomFormHandler $customFormHandler) + { parent::__construct($objectManager); + $this->customFormHandler = $customFormHandler; } /** @@ -48,7 +57,11 @@ public function cleanPositions(bool $setPositions = true): float throw new \BadMethodCallException('CustomForm is null'); } - $this->customFormHandler->setCustomForm($this->customFormField->getCustomForm()); - return $this->customFormHandler->cleanFieldsPositions($setPositions); + if ($this->customFormField->getCustomForm() !== null) { + $this->customFormHandler->setCustomForm($this->customFormField->getCustomForm()); + return $this->customFormHandler->cleanFieldsPositions($setPositions); + } + + return 1; } } diff --git a/src/EntityHandler/CustomFormHandler.php b/src/EntityHandler/CustomFormHandler.php index 721e121a..d77dd59e 100644 --- a/src/EntityHandler/CustomFormHandler.php +++ b/src/EntityHandler/CustomFormHandler.php @@ -11,11 +11,20 @@ /** * Handle operations with node-type entities. */ -final class CustomFormHandler extends AbstractHandler +class CustomFormHandler extends AbstractHandler { protected ?CustomForm $customForm = null; - public function setCustomForm(CustomForm $customForm): self + public function getCustomForm(): ?CustomForm + { + return $this->customForm; + } + + /** + * @param CustomForm $customForm + * @return $this + */ + public function setCustomForm(CustomForm $customForm) { $this->customForm = $customForm; return $this; diff --git a/src/EntityHandler/DocumentHandler.php b/src/EntityHandler/DocumentHandler.php index 44e9d885..96b4bdbb 100644 --- a/src/EntityHandler/DocumentHandler.php +++ b/src/EntityHandler/DocumentHandler.php @@ -21,39 +21,37 @@ /** * Handle operations with documents entities. */ -final class DocumentHandler extends AbstractHandler +class DocumentHandler extends AbstractHandler { - private ?DocumentInterface $document = null; + protected ?DocumentInterface $document = null; + private FilesystemOperator $documentStorage; - public function __construct(ObjectManager $objectManager, private readonly FilesystemOperator $documentStorage) + public function __construct(ObjectManager $objectManager, FilesystemOperator $documentStorage) { parent::__construct($objectManager); + $this->documentStorage = $documentStorage; } /** * Get a Response object to force download document. * This method works for both private and public documents. * - * @param bool $asAttachment * @return StreamedResponse * @throws FilesystemException */ - public function getDownloadResponse(bool $asAttachment = true): StreamedResponse + public function getDownloadResponse(): StreamedResponse { if ($this->document->isLocal()) { $documentPath = $this->document->getMountPath(); if ($this->documentStorage->fileExists($documentPath)) { - $headers = [ - "Content-Type" => $this->documentStorage->mimeType($documentPath), - "Content-Length" => $this->documentStorage->fileSize($documentPath), - ]; - if ($asAttachment) { - $headers["Content-disposition"] = "attachment; filename=\"" . basename($this->document->getFilename()) . "\""; - } return new StreamedResponse(function () use ($documentPath) { \fpassthru($this->documentStorage->readStream($documentPath)); - }, Response::HTTP_OK, $headers); + }, Response::HTTP_OK, [ + "Content-Type" => $this->documentStorage->mimeType($documentPath), + "Content-Length" => $this->documentStorage->fileSize($documentPath), + "Content-disposition" => "attachment; filename=\"" . basename($this->document->getFilename()) . "\"", + ]); } } @@ -93,9 +91,9 @@ public function getDocument(): ?DocumentInterface /** * @param DocumentInterface $document - * @return $this + * @return DocumentHandler */ - public function setDocument(DocumentInterface $document): self + public function setDocument(DocumentInterface $document): DocumentHandler { $this->document = $document; return $this; diff --git a/src/EntityHandler/FolderHandler.php b/src/EntityHandler/FolderHandler.php index 5cb16fdb..2b01d6af 100644 --- a/src/EntityHandler/FolderHandler.php +++ b/src/EntityHandler/FolderHandler.php @@ -6,12 +6,13 @@ use Doctrine\Common\Collections\Criteria; use RZ\Roadiz\Core\Handlers\AbstractHandler; +use RZ\Roadiz\Core\AbstractEntities\LeafInterface; use RZ\Roadiz\CoreBundle\Entity\Folder; /** * Handle operations with folders entities. */ -final class FolderHandler extends AbstractHandler +class FolderHandler extends AbstractHandler { protected ?Folder $folder = null; @@ -27,7 +28,7 @@ public function getFolder(): Folder * @param Folder $folder * @return $this */ - public function setFolder(Folder $folder): self + public function setFolder(Folder $folder) { $this->folder = $folder; return $this; @@ -38,7 +39,7 @@ public function setFolder(Folder $folder): self * * @return $this */ - private function removeChildren(): self + private function removeChildren() { /** @var Folder $folder */ foreach ($this->getFolder()->getChildren() as $folder) { @@ -56,7 +57,7 @@ private function removeChildren(): self * * @return $this */ - public function removeWithChildrenAndAssociations(): self + public function removeWithChildrenAndAssociations() { $this->removeChildren(); $this->objectManager->remove($this->getFolder()); @@ -68,6 +69,49 @@ public function removeWithChildrenAndAssociations(): self return $this; } + /** + * Return every folder’s parents. + * + * @deprecated Use directly Folder::getParents method. + * @return array + */ + public function getParents(): array + { + $parentsArray = []; + $parent = $this->getFolder(); + + do { + $parent = $parent->getParent(); + if ($parent !== null) { + $parentsArray[] = $parent; + } else { + break; + } + } while ($parent !== null); + + return array_reverse($parentsArray); + } + + /** + * Get folder full path using folder names. + * + * @deprecated Use directly Folder::getFullPath method. + * @return string + */ + public function getFullPath(): string + { + $parents = $this->getParents(); + $path = []; + + foreach ($parents as $parent) { + $path[] = $parent->getFolderName(); + } + + $path[] = $this->getFolder()->getFolderName(); + + return implode('/', $path); + } + /** * Clean position for current folder siblings. * diff --git a/src/EntityHandler/GroupHandler.php b/src/EntityHandler/GroupHandler.php index 9fb78ea9..b4cfe577 100644 --- a/src/EntityHandler/GroupHandler.php +++ b/src/EntityHandler/GroupHandler.php @@ -11,7 +11,7 @@ /** * Handle operations with Group entities. */ -final class GroupHandler extends AbstractHandler +class GroupHandler extends AbstractHandler { private ?Group $group = null; diff --git a/src/EntityHandler/HandlerFactory.php b/src/EntityHandler/HandlerFactory.php index 860b941e..8975ba83 100644 --- a/src/EntityHandler/HandlerFactory.php +++ b/src/EntityHandler/HandlerFactory.php @@ -20,10 +20,16 @@ use RZ\Roadiz\CoreBundle\Entity\Tag; use RZ\Roadiz\CoreBundle\Entity\Translation; -final class HandlerFactory implements HandlerFactoryInterface +class HandlerFactory implements HandlerFactoryInterface { - public function __construct(private readonly ContainerInterface $container) + private ContainerInterface $container; + + /** + * @param ContainerInterface $container + */ + public function __construct(ContainerInterface $container) { + $this->container = $container; } /** diff --git a/src/EntityHandler/NodeHandler.php b/src/EntityHandler/NodeHandler.php index fced6244..d9a9b408 100644 --- a/src/EntityHandler/NodeHandler.php +++ b/src/EntityHandler/NodeHandler.php @@ -5,10 +5,8 @@ namespace RZ\Roadiz\CoreBundle\EntityHandler; use Doctrine\Common\Collections\Criteria; -use Doctrine\ORM\NonUniqueResultException; use Doctrine\Persistence\ObjectManager; -use RZ\Roadiz\Contracts\NodeType\NodeTypeFieldInterface; -use RZ\Roadiz\Core\Handlers\AbstractHandler; +use RZ\Roadiz\Core\AbstractEntities\LeafInterface; use RZ\Roadiz\CoreBundle\Entity\CustomForm; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\NodesCustomForms; @@ -16,28 +14,41 @@ use RZ\Roadiz\CoreBundle\Entity\NodesToNodes; use RZ\Roadiz\CoreBundle\Entity\NodeTypeField; use RZ\Roadiz\CoreBundle\Entity\Translation; +use RZ\Roadiz\CoreBundle\Repository\NodeRepository; use RZ\Roadiz\CoreBundle\Node\NodeDuplicator; use RZ\Roadiz\CoreBundle\Node\NodeNamePolicyInterface; -use RZ\Roadiz\CoreBundle\Repository\NodeRepository; use RZ\Roadiz\CoreBundle\Security\Authorization\Chroot\NodeChrootResolver; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Workflow\Registry; -use Symfony\Component\Workflow\WorkflowInterface; +use Symfony\Component\Workflow\Workflow; +use RZ\Roadiz\Core\Handlers\AbstractHandler; /** * Handle operations with nodes entities. */ -final class NodeHandler extends AbstractHandler +class NodeHandler extends AbstractHandler { + protected NodeChrootResolver $chrootResolver; + private Registry $registry; private ?Node $node = null; + private NodeNamePolicyInterface $nodeNamePolicy; + /** + * @param ObjectManager $objectManager + * @param Registry $registry + * @param NodeChrootResolver $chrootResolver + * @param NodeNamePolicyInterface $nodeNamePolicy + */ final public function __construct( ObjectManager $objectManager, - private readonly Registry $registry, - private readonly NodeChrootResolver $chrootResolver, - private readonly NodeNamePolicyInterface $nodeNamePolicy + Registry $registry, + NodeChrootResolver $chrootResolver, + NodeNamePolicyInterface $nodeNamePolicy ) { parent::__construct($objectManager); + $this->registry = $registry; + $this->chrootResolver = $chrootResolver; + $this->nodeNamePolicy = $nodeNamePolicy; } protected function createSelf(): self @@ -63,9 +74,9 @@ public function getNode(): Node /** * @param Node $node - * @return $this + * @return NodeHandler */ - public function setNode(Node $node): self + public function setNode(Node $node) { $this->node = $node; return $this; @@ -78,11 +89,11 @@ public function setNode(Node $node): self * @param bool $flush * @return $this */ - public function cleanCustomFormsFromField(NodeTypeFieldInterface $field, bool $flush = true): self + public function cleanCustomFormsFromField(NodeTypeField $field, bool $flush = true) { $nodesCustomForms = $this->objectManager ->getRepository(NodesCustomForms::class) - ->findBy(['node' => $this->getNode(), 'fieldName' => $field->getName()]); + ->findBy(['node' => $this->getNode(), 'field' => $field]); foreach ($nodesCustomForms as $ncf) { $this->objectManager->remove($ncf); @@ -106,16 +117,16 @@ public function cleanCustomFormsFromField(NodeTypeFieldInterface $field, bool $f */ public function addCustomFormForField( CustomForm $customForm, - NodeTypeFieldInterface $field, + NodeTypeField $field, bool $flush = true, ?float $position = null - ): self { + ) { $ncf = new NodesCustomForms($this->getNode(), $customForm, $field); if (null === $position) { $latestPosition = $this->objectManager ->getRepository(NodesCustomForms::class) - ->getLatestPositionForFieldName($this->getNode(), $field->getName()); + ->getLatestPosition($this->getNode(), $field); $ncf->setPosition($latestPosition + 1); } else { $ncf->setPosition($position); @@ -140,9 +151,9 @@ public function getCustomFormsFromFieldName(string $fieldName): array { return $this->objectManager ->getRepository(CustomForm::class) - ->findByNodeAndFieldName( + ->findByNodeAndField( $this->getNode(), - $fieldName + $this->getNode()->getNodeType()->getFieldByName($fieldName) ); } @@ -153,7 +164,7 @@ public function getCustomFormsFromFieldName(string $fieldName): array * @param bool $flush * @return $this */ - public function cleanNodesFromField(NodeTypeFieldInterface $field, bool $flush = true): self + public function cleanNodesFromField(NodeTypeField $field, bool $flush = true) { $this->node->clearBNodesForField($field); @@ -173,7 +184,7 @@ public function cleanNodesFromField(NodeTypeFieldInterface $field, bool $flush = * @param null|float $position * @return $this */ - public function addNodeForField(Node $node, NodeTypeFieldInterface $field, bool $flush = true, ?float $position = null): self + public function addNodeForField(Node $node, NodeTypeField $field, bool $flush = true, ?float $position = null) { $ntn = new NodesToNodes($this->getNode(), $node, $field); @@ -181,7 +192,7 @@ public function addNodeForField(Node $node, NodeTypeFieldInterface $field, bool if (null === $position) { $latestPosition = $this->objectManager ->getRepository(NodesToNodes::class) - ->getLatestPositionForFieldName($this->getNode(), $field->getName()); + ->getLatestPosition($this->getNode(), $field); $ntn->setPosition($latestPosition + 1); } else { $ntn->setPosition($position); @@ -240,7 +251,6 @@ public function getReverseNodesFromFieldName(string $fieldName): array * @param Translation $translation * * @return null|NodesSources - * @deprecated Use Node::getNodeSourcesByTranslation() instead. */ public function getNodeSourceByTranslation($translation): ?NodesSources { @@ -254,7 +264,7 @@ public function getNodeSourceByTranslation($translation): ?NodesSources * * @return $this */ - private function removeChildren(): self + private function removeChildren() { /** @var Node $node */ foreach ($this->getNode()->getChildren() as $node) { @@ -270,7 +280,7 @@ private function removeChildren(): self * * @return $this */ - public function removeAssociations(): self + public function removeAssociations() { /** @var NodesSources $ns */ foreach ($this->getNode()->getNodeSources() as $ns) { @@ -287,7 +297,7 @@ public function removeAssociations(): self * * @return $this */ - public function removeWithChildrenAndAssociations(): self + public function removeWithChildrenAndAssociations() { $this->removeChildren(); $this->removeAssociations(); @@ -297,9 +307,9 @@ public function removeWithChildrenAndAssociations(): self } /** - * @return WorkflowInterface + * @return Workflow */ - private function getWorkflow(): WorkflowInterface + private function getWorkflow(): Workflow { return $this->registry->get($this->getNode()); } @@ -311,7 +321,7 @@ private function getWorkflow(): WorkflowInterface * * @return $this */ - public function softRemoveWithChildren(): self + public function softRemoveWithChildren() { $workflow = $this->getWorkflow(); if ($workflow->can($this->getNode(), 'delete')) { @@ -335,7 +345,7 @@ public function softRemoveWithChildren(): self * * @return $this */ - public function softUnremoveWithChildren(): self + public function softUnremoveWithChildren() { $workflow = $this->getWorkflow(); if ($workflow->can($this->getNode(), 'undelete')) { @@ -359,7 +369,7 @@ public function softUnremoveWithChildren(): self * * @return $this */ - public function publishWithChildren(): self + public function publishWithChildren() { $workflow = $this->getWorkflow(); if ($workflow->can($this->getNode(), 'publish')) { @@ -382,7 +392,7 @@ public function publishWithChildren(): self * * @return $this */ - public function archiveWithChildren(): self + public function archiveWithChildren() { $workflow = $this->getWorkflow(); if ($workflow->can($this->getNode(), 'archive')) { @@ -399,6 +409,17 @@ public function archiveWithChildren(): self return $this; } + /** + * Return if is in Newsletter Node. + * + * @deprecated Just here not to break themes. + * @return bool + */ + public function isRelatedToNewsletter(): bool + { + return false; + } + /** * Return if part of Node offspring. * @@ -529,7 +550,6 @@ public function cleanRootNodesPositions(bool $setPositions = true): float * Return all node offspring id. * * @return array - * @deprecated Use NodeOffspringResolverInterface::getAllOffspringIds($chroot). */ public function getAllOffspringId(): array { @@ -541,7 +561,7 @@ public function getAllOffspringId(): array * * @return $this */ - public function makeHome(): self + public function makeHome() { $defaults = $this->getRepository() ->setDisplayingNotPublishedNodes(true) @@ -563,7 +583,7 @@ public function makeHome(): self * @return Node * @deprecated Use NodeDuplicator::duplicate() instead. */ - public function duplicate(): Node + public function duplicate() { $duplicator = new NodeDuplicator( $this->getNode(), @@ -576,17 +596,15 @@ public function duplicate(): Node /** * Get previous node from hierarchy. * - * @param array|null $criteria - * @param array|null $order + * @param array|null $criteria + * @param array|null $order * * @return Node|null - * @throws NonUniqueResultException - * @deprecated Use NodeRepository::findPreviousNode() instead. */ public function getPrevious( ?array $criteria = null, ?array $order = null - ): ?Node { + ) { if ($this->getNode()->getPosition() <= 1) { return null; } @@ -619,17 +637,15 @@ public function getPrevious( /** * Get next node from hierarchy. * - * @param array|null $criteria - * @param array|null $order + * @param array|null $criteria + * @param array|null $order * * @return Node|null - * @throws NonUniqueResultException - * @deprecated Use NodeRepository::findNextNode() instead. */ public function getNext( ?array $criteria = null, ?array $order = null - ): ?Node { + ) { if (null === $criteria) { $criteria = []; } @@ -659,7 +675,7 @@ public function getNext( /** * @return NodeRepository */ - protected function getRepository(): NodeRepository + public function getRepository(): NodeRepository { return $this->objectManager->getRepository(Node::class); } diff --git a/src/EntityHandler/NodeTypeFieldHandler.php b/src/EntityHandler/NodeTypeFieldHandler.php index 2a17f9d5..a1542bb8 100644 --- a/src/EntityHandler/NodeTypeFieldHandler.php +++ b/src/EntityHandler/NodeTypeFieldHandler.php @@ -12,8 +12,9 @@ /** * Handle operations with node-type fields entities. */ -final class NodeTypeFieldHandler extends AbstractHandler +class NodeTypeFieldHandler extends AbstractHandler { + private HandlerFactory $handlerFactory; private ?NodeTypeField $nodeTypeField = null; public function getNodeTypeField(): NodeTypeField @@ -34,9 +35,16 @@ public function setNodeTypeField(NodeTypeField $nodeTypeField): self return $this; } - public function __construct(ObjectManager $objectManager, private readonly HandlerFactory $handlerFactory) + /** + * Create a new node-type-field handler with node-type-field to handle. + * + * @param ObjectManager $objectManager + * @param HandlerFactory $handlerFactory + */ + public function __construct(ObjectManager $objectManager, HandlerFactory $handlerFactory) { parent::__construct($objectManager); + $this->handlerFactory = $handlerFactory; } /** diff --git a/src/EntityHandler/NodeTypeHandler.php b/src/EntityHandler/NodeTypeHandler.php index e391a276..8c6c7a39 100644 --- a/src/EntityHandler/NodeTypeHandler.php +++ b/src/EntityHandler/NodeTypeHandler.php @@ -8,8 +8,8 @@ use Doctrine\Persistence\ObjectManager; use JMS\Serializer\SerializationContext; use JMS\Serializer\SerializerInterface; -use Psr\Log\LoggerInterface; use RZ\Roadiz\Core\Handlers\AbstractHandler; +use RZ\Roadiz\CoreBundle\Doctrine\SchemaUpdater; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\NodeType; use RZ\Roadiz\CoreBundle\Entity\NodeTypeField; @@ -24,9 +24,17 @@ /** * Handle operations with node-type entities. */ -final class NodeTypeHandler extends AbstractHandler +class NodeTypeHandler extends AbstractHandler { private ?NodeType $nodeType = null; + private EntityGeneratorFactory $entityGeneratorFactory; + private ApiResourceGenerator $apiResourceGenerator; + private HandlerFactory $handlerFactory; + private string $generatedEntitiesDir; + private SerializerInterface $serializer; + private string $serializedNodeTypesDir; + private string $importFilesConfigPath; + private string $kernelProjectDir; /** * @return NodeType @@ -43,7 +51,7 @@ public function getNodeType(): NodeType * @param NodeType $nodeType * @return $this */ - public function setNodeType(NodeType $nodeType): self + public function setNodeType(NodeType $nodeType) { $this->nodeType = $nodeType; return $this; @@ -51,17 +59,24 @@ public function setNodeType(NodeType $nodeType): self public function __construct( ObjectManager $objectManager, - private readonly EntityGeneratorFactory $entityGeneratorFactory, - private readonly HandlerFactory $handlerFactory, - private readonly SerializerInterface $serializer, - private readonly ApiResourceGenerator $apiResourceGenerator, - private readonly LoggerInterface $logger, - private readonly string $generatedEntitiesDir, - private readonly string $serializedNodeTypesDir, - private readonly string $importFilesConfigPath, - private readonly string $kernelProjectDir + EntityGeneratorFactory $entityGeneratorFactory, + HandlerFactory $handlerFactory, + SerializerInterface $serializer, + ApiResourceGenerator $apiResourceGenerator, + string $generatedEntitiesDir, + string $serializedNodeTypesDir, + string $importFilesConfigPath, + string $kernelProjectDir ) { parent::__construct($objectManager); + $this->entityGeneratorFactory = $entityGeneratorFactory; + $this->handlerFactory = $handlerFactory; + $this->generatedEntitiesDir = $generatedEntitiesDir; + $this->serializer = $serializer; + $this->serializedNodeTypesDir = $serializedNodeTypesDir; + $this->importFilesConfigPath = $importFilesConfigPath; + $this->kernelProjectDir = $kernelProjectDir; + $this->apiResourceGenerator = $apiResourceGenerator; } public function getGeneratedEntitiesFolder(): string @@ -91,11 +106,6 @@ public function removeSourceEntityClass(): bool if ($fileSystem->exists($repositoryFile) && is_file($repositoryFile)) { $fileSystem->remove($repositoryFile); } - $this->logger->info('Entity class file and repository have been removed.', [ - 'nodeType' => $this->nodeType->getName(), - 'file' => $file, - 'repositoryFile' => $repositoryFile, - ]); return true; } @@ -244,11 +254,6 @@ public function generateSourceEntityClass(): bool \clearstatcache(true, $file); \clearstatcache(true, $repositoryFile); - $this->logger->info('Entity class file and repository have been generated.', [ - 'nodeType' => $this->nodeType->getName(), - 'file' => $file, - 'repositoryFile' => $repositoryFile, - ]); return true; } diff --git a/src/EntityHandler/NodesSourcesHandler.php b/src/EntityHandler/NodesSourcesHandler.php index 55b2eb90..860fa692 100644 --- a/src/EntityHandler/NodesSourcesHandler.php +++ b/src/EntityHandler/NodesSourcesHandler.php @@ -6,8 +6,6 @@ use Doctrine\ORM\EntityRepository; use Doctrine\Persistence\ObjectManager; -use RZ\Roadiz\Contracts\NodeType\NodeTypeFieldInterface; -use RZ\Roadiz\Core\Handlers\AbstractHandler; use RZ\Roadiz\CoreBundle\Bag\Settings; use RZ\Roadiz\CoreBundle\Entity\Document; use RZ\Roadiz\CoreBundle\Entity\Node; @@ -15,21 +13,32 @@ use RZ\Roadiz\CoreBundle\Entity\NodesSourcesDocuments; use RZ\Roadiz\CoreBundle\Entity\NodeTypeField; use RZ\Roadiz\CoreBundle\Entity\Tag; +use RZ\Roadiz\CoreBundle\Repository\NodesSourcesRepository; +use RZ\Roadiz\Core\Handlers\AbstractHandler; /** * Handle operations with node-sources entities. */ -final class NodesSourcesHandler extends AbstractHandler +class NodesSourcesHandler extends AbstractHandler { - private ?NodesSources $nodeSource = null; + protected ?NodesSources $nodeSource = null; /** * @var array|null */ - private ?array $parentsNodeSources = null; + protected ?array $parentsNodeSources = null; + protected Settings $settingsBag; - public function __construct(ObjectManager $objectManager, private readonly Settings $settingsBag) + /** + * Create a new node-source handler with node-source to handle. + * + * @param ObjectManager $objectManager + * @param Settings $settingsBag + */ + public function __construct(ObjectManager $objectManager, Settings $settingsBag) { parent::__construct($objectManager); + + $this->settingsBag = $settingsBag; } /** @@ -53,9 +62,9 @@ public function getNodeSource(): NodesSources /** * @param NodesSources $nodeSource - * @return $this + * @return NodesSourcesHandler */ - public function setNodeSource(NodesSources $nodeSource): self + public function setNodeSource(NodesSources $nodeSource) { $this->nodeSource = $nodeSource; return $this; @@ -68,7 +77,7 @@ public function setNodeSource(NodesSources $nodeSource): self * @param bool $flush * @return $this */ - public function cleanDocumentsFromField(NodeTypeFieldInterface $field, bool $flush = true): self + public function cleanDocumentsFromField(NodeTypeField $field, bool $flush = true) { $this->nodeSource->clearDocumentsByFields($field); @@ -90,17 +99,17 @@ public function cleanDocumentsFromField(NodeTypeFieldInterface $field, bool $flu */ public function addDocumentForField( Document $document, - NodeTypeFieldInterface $field, + NodeTypeField $field, bool $flush = true, ?float $position = null - ): self { + ) { $nsDoc = new NodesSourcesDocuments($this->nodeSource, $document, $field); if (!$this->nodeSource->hasNodesSourcesDocuments($nsDoc)) { if (null === $position) { $latestPosition = $this->objectManager ->getRepository(NodesSourcesDocuments::class) - ->getLatestPositionForFieldName($this->nodeSource, $field->getName()); + ->getLatestPosition($this->nodeSource, $field); $nsDoc->setPosition($latestPosition + 1); } else { @@ -121,16 +130,19 @@ public function addDocumentForField( * * @param string $fieldName Name of the node-type field * @return array - * @deprecated Use directly NodesSources::getDocumentsByFieldsWithName */ public function getDocumentsFromFieldName(string $fieldName): array { - return $this->objectManager - ->getRepository(Document::class) - ->findByNodeSourceAndFieldName( - $this->nodeSource, - $fieldName - ); + $field = $this->nodeSource->getNode()->getNodeType()->getFieldByName($fieldName); + if (null !== $field) { + return $this->objectManager + ->getRepository(Document::class) + ->findByNodeSourceAndField( + $this->nodeSource, + $field + ); + } + return []; } /** @@ -166,7 +178,7 @@ public function getParent(): ?NodesSources * * @param array|null $criteria * @return array - * @deprecated Use NodesSourcesRepository::findParents + * @throws \Doctrine\ORM\NonUniqueResultException */ public function getParents( array $criteria = null @@ -211,7 +223,6 @@ public function getParents( * @param array|null $order Non default ordering * * @return array - * @deprecated Use TreeWalker or NodesSourcesRepository::findChildren */ public function getChildren( array $criteria = null, @@ -247,7 +258,6 @@ public function getChildren( * @param array|null $order * * @return NodesSources|null - * @deprecated Use NodesSourcesRepository::findFirstChild */ public function getFirstChild( array $criteria = null, @@ -282,7 +292,6 @@ public function getFirstChild( * @param array|null $order * * @return NodesSources|null - * @deprecated Use NodesSourcesRepository::findLastChild */ public function getLastChild( array $criteria = null, @@ -318,7 +327,6 @@ public function getLastChild( * @param array|null $order * * @return NodesSources|null - * @deprecated Use NodesSourcesRepository::findFirstSibling */ public function getFirstSibling( array $criteria = null, @@ -341,7 +349,6 @@ public function getFirstSibling( * @param array|null $order * * @return NodesSources|null - * @deprecated Use NodesSourcesRepository::findLastSibling */ public function getLastSibling( array $criteria = null, @@ -364,7 +371,6 @@ public function getLastSibling( * @param array|null $order * * @return NodesSources|null - * @deprecated Use NodesSourcesRepository::findPrevious */ public function getPrevious( array $criteria = null, @@ -411,7 +417,6 @@ public function getPrevious( * @param array|null $order * * @return NodesSources|null - * @deprecated Use NodesSourcesRepository::findNext */ public function getNext( array $criteria = null, @@ -450,10 +455,9 @@ public function getNext( /** * Get node tags with current source translation. * - * @return iterable - * @deprecated Use TagRepository::findByNodesSources + * @return array */ - public function getTags(): iterable + public function getTags() { /** * @phpstan-ignore-next-line @@ -486,6 +490,7 @@ public function getSEO(): array 'description' => ($this->nodeSource->getMetaDescription() != "") ? $this->nodeSource->getMetaDescription() : $this->nodeSource->getTitle() . ', ' . $this->settingsBag->get('seo_description'), + 'keywords' => $this->nodeSource->getMetaKeywords(), ]; } @@ -496,7 +501,7 @@ public function getSEO(): array * * @return array Collection of nodes */ - public function getNodesFromFieldName(string $fieldName): array + public function getNodesFromFieldName(string $fieldName) { $field = $this->nodeSource->getNode()->getNodeType()->getFieldByName($fieldName); if (null !== $field) { @@ -518,7 +523,7 @@ public function getNodesFromFieldName(string $fieldName): array * * @return array Collection of nodes */ - public function getReverseNodesFromFieldName(string $fieldName): array + public function getReverseNodesFromFieldName(string $fieldName) { $field = $this->nodeSource->getNode()->getNodeType()->getFieldByName($fieldName); if (null !== $field) { diff --git a/src/EntityHandler/TagHandler.php b/src/EntityHandler/TagHandler.php index b995f460..b99e28c2 100644 --- a/src/EntityHandler/TagHandler.php +++ b/src/EntityHandler/TagHandler.php @@ -13,15 +13,26 @@ /** * Handle operations with tags entities. */ -final class TagHandler extends AbstractHandler +class TagHandler extends AbstractHandler { private ?Tag $tag = null; + /** + * @return Tag + */ + public function getTag(): Tag + { + if (null === $this->tag) { + throw new \BadMethodCallException('Tag is null'); + } + return $this->tag; + } + /** * @param Tag $tag * @return $this */ - public function setTag(Tag $tag): self + public function setTag(Tag $tag) { $this->tag = $tag; return $this; @@ -32,7 +43,7 @@ public function setTag(Tag $tag): self * * @return $this */ - private function removeChildren(): self + private function removeChildren() { /** @var Tag $tag */ foreach ($this->tag->getChildren() as $tag) { @@ -48,7 +59,7 @@ private function removeChildren(): self * * @return $this */ - public function removeAssociations(): self + public function removeAssociations() { foreach ($this->tag->getTranslatedTags() as $tt) { $this->objectManager->remove($tt); @@ -62,7 +73,7 @@ public function removeAssociations(): self * * @return $this */ - public function removeWithChildrenAndAssociations(): self + public function removeWithChildrenAndAssociations() { $this->removeChildren(); $this->removeAssociations(); @@ -77,6 +88,120 @@ public function removeWithChildrenAndAssociations(): self return $this; } + /** + * @return array Array of Translation + * @deprecated Do not query DB here + */ + public function getAvailableTranslations() + { + $query = $this->objectManager + ->createQuery(' + SELECT tr + FROM RZ\Roadiz\CoreBundle\Entity\Translation tr + INNER JOIN tr.tagTranslations tt + INNER JOIN tt.tag t + WHERE t.id = :tag_id') + ->setParameter('tag_id', $this->tag->getId()); + + try { + return $query->getResult(); + } catch (NoResultException $e) { + return []; + } + } + /** + * @return array Array of Translation id + * @deprecated Do not query DB here + */ + public function getAvailableTranslationsId() + { + $query = $this->objectManager + ->createQuery(' + SELECT tr.id FROM RZ\Roadiz\CoreBundle\Entity\Tag t + INNER JOIN t.translatedTags tt + INNER JOIN tt.translation tr + WHERE t.id = :tag_id') + ->setParameter('tag_id', $this->tag->getId()); + + try { + $simpleArray = []; + $complexArray = $query->getScalarResult(); + foreach ($complexArray as $subArray) { + $simpleArray[] = $subArray['id']; + } + + return $simpleArray; + } catch (NoResultException $e) { + return []; + } + } + + /** + * @return array Array of Translation + * @deprecated Do not query DB here + */ + public function getUnavailableTranslations() + { + $query = $this->objectManager + ->createQuery(' + SELECT tr FROM RZ\Roadiz\CoreBundle\Entity\Translation tr + WHERE tr.id NOT IN (:translations_id)') + ->setParameter('translations_id', $this->getAvailableTranslationsId()); + + try { + return $query->getResult(); + } catch (NoResultException $e) { + return []; + } + } + + /** + * @return array Array of Translation id + * @deprecated Do not query DB here + */ + public function getUnavailableTranslationsId() + { + /** @var Query $query */ + $query = $this->objectManager + ->createQuery(' + SELECT t.id FROM RZ\Roadiz\CoreBundle\Entity\Translation t + WHERE t.id NOT IN (:translations_id)') + ->setParameter('translations_id', $this->getAvailableTranslationsId()); + + try { + $simpleArray = []; + $complexArray = $query->getScalarResult(); + foreach ($complexArray as $subArray) { + $simpleArray[] = $subArray['id']; + } + + return $simpleArray; + } catch (NoResultException $e) { + return []; + } + } + + /** + * Return every tag’s parents. + * @deprecated Use directly Tag::getParents + * @return array + */ + public function getParents() + { + return $this->tag->getParents(); + } + + /** + * Get tag full path using tag names. + * + * @deprecated Use directly Tag::getFullPath + * @return string + */ + public function getFullPath(): string + { + return $this->tag->getFullPath(); + } + /** * Clean position for current tag siblings. * diff --git a/src/EntityHandler/TranslationHandler.php b/src/EntityHandler/TranslationHandler.php index 6f3f30ea..7a9d9673 100644 --- a/src/EntityHandler/TranslationHandler.php +++ b/src/EntityHandler/TranslationHandler.php @@ -14,16 +14,27 @@ /** * Handle operations with translations entities. */ -final class TranslationHandler extends AbstractHandler +class TranslationHandler extends AbstractHandler { private ?TranslationInterface $translation = null; + /** + * @return TranslationInterface + */ + public function getTranslation(): TranslationInterface + { + if (null === $this->translation) { + throw new \BadMethodCallException('Translation is null'); + } + return $this->translation; + } + /** * @param TranslationInterface $translation * * @return $this */ - public function setTranslation(TranslationInterface $translation): self + public function setTranslation(TranslationInterface $translation) { $this->translation = $translation; return $this; @@ -34,7 +45,7 @@ public function setTranslation(TranslationInterface $translation): self * * @return $this */ - public function makeDefault(): self + public function makeDefault() { $defaults = $this->objectManager ->getRepository(Translation::class) diff --git a/src/Event/NodesSources/NodesSourcesPathGeneratingEvent.php b/src/Event/NodesSources/NodesSourcesPathGeneratingEvent.php index 2efc28d3..b2cd984b 100644 --- a/src/Event/NodesSources/NodesSourcesPathGeneratingEvent.php +++ b/src/Event/NodesSources/NodesSourcesPathGeneratingEvent.php @@ -11,14 +11,46 @@ final class NodesSourcesPathGeneratingEvent extends Event { - private ?string $path; + /** + * @var bool + */ + protected $forceLocaleWithUrlAlias; + /** + * @var Theme|null + */ + private $theme; + /** + * @var NodesSources|null + */ + private $nodeSource; + /** + * @var array|null + */ + private $parameters; + /** + * @var RequestContext|null + */ + private $requestContext; + /** + * @var bool + */ + private $forceLocale = false; + /** + * @var string|null + */ + private $path; /** * @var bool Tells Node Router to prepend request context information to path or not. */ - private bool $isComplete = false; - protected bool $containsScheme = false; + private $isComplete = false; + /** + * @var bool + */ + protected $containsScheme = false; /** + * NodesSourcesPathGeneratingEvent constructor. + * * @param Theme|null $theme * @param NodesSources|null $nodeSource * @param RequestContext|null $requestContext @@ -27,13 +59,19 @@ final class NodesSourcesPathGeneratingEvent extends Event * @param bool $forceLocaleWithUrlAlias */ public function __construct( - private readonly ?Theme $theme, - private ?NodesSources $nodeSource, - private readonly ?RequestContext $requestContext, - private array $parameters = [], - private readonly bool $forceLocale = false, - private bool $forceLocaleWithUrlAlias = false + ?Theme $theme, + ?NodesSources $nodeSource, + ?RequestContext $requestContext, + array $parameters = [], + bool $forceLocale = false, + bool $forceLocaleWithUrlAlias = false ) { + $this->theme = $theme; + $this->nodeSource = $nodeSource; + $this->requestContext = $requestContext; + $this->forceLocale = $forceLocale; + $this->parameters = $parameters; + $this->forceLocaleWithUrlAlias = $forceLocaleWithUrlAlias; } /** diff --git a/src/Event/Redirection/PostCreatedRedirectionEvent.php b/src/Event/Redirection/PostCreatedRedirectionEvent.php deleted file mode 100644 index 13014c4f..00000000 --- a/src/Event/Redirection/PostCreatedRedirectionEvent.php +++ /dev/null @@ -1,9 +0,0 @@ -redirection = $redirection; - } - - /** - * @return Redirection|null - */ - public function getRedirection(): ?Redirection - { - return $this->redirection; - } - - /** - * @param Redirection|null $redirection - * @return RedirectionEvent - */ - public function setRedirection(?Redirection $redirection): RedirectionEvent - { - $this->redirection = $redirection; - return $this; - } -} diff --git a/src/EventSubscriber/AssetsCacheEventSubscriber.php b/src/EventSubscriber/AssetsCacheEventSubscriber.php index 664344d0..1e729545 100644 --- a/src/EventSubscriber/AssetsCacheEventSubscriber.php +++ b/src/EventSubscriber/AssetsCacheEventSubscriber.php @@ -11,16 +11,23 @@ final class AssetsCacheEventSubscriber implements EventSubscriberInterface { - public function __construct( - private readonly AssetsFileClearer $assetsClearer, - private readonly LoggerInterface $logger - ) { + private AssetsFileClearer $assetsClearer; + private LoggerInterface $logger; + + public function __construct(AssetsFileClearer $assetsClearer, LoggerInterface $logger) + { + $this->assetsClearer = $assetsClearer; + $this->logger = $logger; } + /** + * @inheritDoc + */ public static function getSubscribedEvents(): array { return [ - CachePurgeAssetsRequestEvent::class => ['onPurgeAssetsRequest', 0] + CachePurgeAssetsRequestEvent::class => ['onPurgeAssetsRequest', 0], + '\RZ\Roadiz\Core\Events\Cache\CachePurgeAssetsRequestEvent' => ['onPurgeAssetsRequest', 0], ]; } diff --git a/src/EventSubscriber/AttributeValueIndexingSubscriber.php b/src/EventSubscriber/AttributeValueIndexingSubscriber.php new file mode 100644 index 00000000..bd4a3e6b --- /dev/null +++ b/src/EventSubscriber/AttributeValueIndexingSubscriber.php @@ -0,0 +1,90 @@ + 'onNodeSourceIndexing', + ]; + } + + public function onNodeSourceIndexing(NodesSourcesIndexingEvent $event): void + { + if ($event->getNodeSource()->getNode()->getAttributeValues()->count() === 0) { + return; + } + + $associations = $event->getAssociations(); + $attributeValues = $event->getNodeSource() + ->getNode() + ->getAttributesValuesForTranslation($event->getNodeSource()->getTranslation()); + + /** @var AttributeValueInterface $attributeValue */ + foreach ($attributeValues as $attributeValue) { + if ($attributeValue->getAttribute()->isSearchable()) { + $data = $attributeValue->getAttributeValueTranslation( + $event->getNodeSource()->getTranslation() + )->getValue(); + if (null === $data) { + $data = $attributeValue->getAttributeValueTranslations()->first()->getValue(); + } + if (null !== $data) { + switch ($attributeValue->getType()) { + case AttributeInterface::DATETIME_T: + case AttributeInterface::DATE_T: + if ($data instanceof \DateTime) { + $fieldName = $attributeValue->getAttribute()->getCode() . '_dt'; + $associations[$fieldName] = $data->format('Y-m-d\TH:i:s'); + } + break; + case AttributeInterface::STRING_T: + $fieldName = $attributeValue->getAttribute()->getCode(); + /* + * Use locale to create field name + * with right language + */ + if ( + in_array( + $event->getNodeSource()->getTranslation()->getLocale(), + AbstractSolarium::$availableLocalizedTextFields + ) + ) { + $lang = $event->getNodeSource()->getTranslation()->getLocale(); + $fieldName .= '_txt_' . $lang; + } else { + $lang = null; + $fieldName .= '_t'; + } + /* + * Strip Markdown syntax + */ + $content = $event->getSolariumDocument()->cleanTextContent($data); + $associations[$fieldName] = $content; + $associations['collection_txt'][] = $content; + if (null !== $lang) { + // Compile all text content into a single localized text field. + $associations['collection_txt_' . $lang] = implode(PHP_EOL, $associations['collection_txt']); + } + break; + } + } + } + } + + $event->setAssociations($associations); + } +} diff --git a/src/EventSubscriber/AutomaticWebhookSubscriber.php b/src/EventSubscriber/AutomaticWebhookSubscriber.php index fb29b2fc..1e6ba43f 100644 --- a/src/EventSubscriber/AutomaticWebhookSubscriber.php +++ b/src/EventSubscriber/AutomaticWebhookSubscriber.php @@ -25,25 +25,45 @@ final class AutomaticWebhookSubscriber implements EventSubscriberInterface { + private WebhookDispatcher $webhookDispatcher; + private HandlerFactoryInterface $handlerFactory; + private ManagerRegistry $managerRegistry; + + /** + * @param WebhookDispatcher $webhookDispatcher + * @param ManagerRegistry $managerRegistry + * @param HandlerFactoryInterface $handlerFactory + */ public function __construct( - private readonly WebhookDispatcher $webhookDispatcher, - private readonly ManagerRegistry $managerRegistry, - private readonly HandlerFactoryInterface $handlerFactory + WebhookDispatcher $webhookDispatcher, + ManagerRegistry $managerRegistry, + HandlerFactoryInterface $handlerFactory ) { + $this->webhookDispatcher = $webhookDispatcher; + $this->handlerFactory = $handlerFactory; + $this->managerRegistry = $managerRegistry; } public static function getSubscribedEvents(): array { return [ - 'workflow.node.completed' => 'onAutomaticWebhook', + 'workflow.node.completed' => ['onAutomaticWebhook'], NodeVisibilityChangedEvent::class => 'onAutomaticWebhook', + '\RZ\Roadiz\Core\Events\Node\NodeVisibilityChangedEvent' => 'onAutomaticWebhook', NodesSourcesPreUpdatedEvent::class => 'onAutomaticWebhook', + '\RZ\Roadiz\Core\Events\NodesSources\NodesSourcesPreUpdatedEvent' => 'onAutomaticWebhook', NodesSourcesDeletedEvent::class => 'onAutomaticWebhook', + '\RZ\Roadiz\Core\Events\NodesSources\NodesSourcesDeletedEvent' => 'onAutomaticWebhook', NodeUpdatedEvent::class => 'onAutomaticWebhook', + '\RZ\Roadiz\Core\Events\Node\NodeUpdatedEvent' => 'onAutomaticWebhook', NodeDeletedEvent::class => 'onAutomaticWebhook', + '\RZ\Roadiz\Core\Events\Node\NodeDeletedEvent' => 'onAutomaticWebhook', NodeTaggedEvent::class => 'onAutomaticWebhook', + '\RZ\Roadiz\Core\Events\Node\NodeTaggedEvent' => 'onAutomaticWebhook', TagUpdatedEvent::class => 'onAutomaticWebhook', + '\RZ\Roadiz\Core\Events\Tag\TagUpdatedEvent' => 'onAutomaticWebhook', DocumentTranslationUpdatedEvent::class => 'onAutomaticWebhook', + '\RZ\Roadiz\Core\Events\DocumentTranslationUpdatedEvent' => 'onAutomaticWebhook', DocumentUpdatedEvent::class => 'onAutomaticWebhook', ]; } @@ -95,6 +115,8 @@ private function isEventSubjectInRootNode(mixed $event, ?Node $rootNode): bool */ return true; } + /** @var Node|null $subject */ + $subject = null; switch (true) { case $event instanceof Event: diff --git a/src/EventSubscriber/CachedNodeOffspringEventSubscriber.php b/src/EventSubscriber/CachedNodeOffspringEventSubscriber.php deleted file mode 100644 index b06e2058..00000000 --- a/src/EventSubscriber/CachedNodeOffspringEventSubscriber.php +++ /dev/null @@ -1,33 +0,0 @@ - 'invalidateNodeOffspringCache', - NodeDuplicatedEvent::class => 'invalidateNodeOffspringCache', - NodeUpdatedEvent::class => 'invalidateNodeOffspringCache', - ]; - } - - public function invalidateNodeOffspringCache(FilterNodeEvent $event): void - { - $this->cachedNodeOffspringResolver->purgeOffspringCache($event->getNode()); - } -} diff --git a/src/EventSubscriber/CloudflareCacheEventSubscriber.php b/src/EventSubscriber/CloudflareCacheEventSubscriber.php index 48fd5f2e..04b513bd 100644 --- a/src/EventSubscriber/CloudflareCacheEventSubscriber.php +++ b/src/EventSubscriber/CloudflareCacheEventSubscriber.php @@ -24,12 +24,27 @@ final class CloudflareCacheEventSubscriber implements EventSubscriberInterface { + private LoggerInterface $logger; + private MessageBusInterface $bus; + private UrlGeneratorInterface $urlGenerator; + private ReverseProxyCacheLocator $reverseProxyCacheLocator; + + /** + * @param MessageBusInterface $bus + * @param ReverseProxyCacheLocator $reverseProxyCacheLocator + * @param UrlGeneratorInterface $urlGenerator + * @param LoggerInterface $logger + */ public function __construct( - private readonly MessageBusInterface $bus, - private readonly ReverseProxyCacheLocator $reverseProxyCacheLocator, - private readonly UrlGeneratorInterface $urlGenerator, - private readonly LoggerInterface $logger + MessageBusInterface $bus, + ReverseProxyCacheLocator $reverseProxyCacheLocator, + UrlGeneratorInterface $urlGenerator, + LoggerInterface $logger ) { + $this->logger = $logger; + $this->bus = $bus; + $this->reverseProxyCacheLocator = $reverseProxyCacheLocator; + $this->urlGenerator = $urlGenerator; } /** * @inheritDoc @@ -38,15 +53,25 @@ public static function getSubscribedEvents(): array { return [ CachePurgeRequestEvent::class => ['onBanRequest', 3], + '\RZ\Roadiz\Core\Events\Cache\CachePurgeRequestEvent' => ['onBanRequest', 3], NodesSourcesUpdatedEvent::class => ['onPurgeRequest', 3], + '\RZ\Roadiz\Core\Events\NodesSources\NodesSourcesUpdatedEvent' => ['onPurgeRequest', 3], ]; } + /** + * @return bool + */ protected function supportConfig(): bool { return null !== $this->reverseProxyCacheLocator->getCloudflareProxyCache(); } + /** + * @param CachePurgeRequestEvent $event + * @throws \GuzzleHttp\Exception\GuzzleException + * @return void + */ public function onBanRequest(CachePurgeRequestEvent $event): void { if (!$this->supportConfig()) { @@ -84,6 +109,11 @@ public function onBanRequest(CachePurgeRequestEvent $event): void } } + /** + * @param NodesSourcesUpdatedEvent $event + * + * @throws \GuzzleHttp\Exception\GuzzleException + */ public function onPurgeRequest(NodesSourcesUpdatedEvent $event): void { if (!$this->supportConfig()) { @@ -124,7 +154,6 @@ private function getCloudflareCacheProxy(): CloudflareProxyCache /** * @param array $body * @return Request - * @throws \JsonException */ protected function createRequest(array $body): Request { @@ -140,18 +169,16 @@ protected function createRequest(array $body): Request $this->getCloudflareCacheProxy()->getVersion(), $this->getCloudflareCacheProxy()->getZone() ); - $body = \json_encode($body, JSON_THROW_ON_ERROR); return new Request( 'POST', $uri, $headers, - $body + \json_encode($body) ); } /** * @return Request - * @throws \JsonException */ protected function createBanRequest(): Request { @@ -162,8 +189,8 @@ protected function createBanRequest(): Request /** * @param string[] $uris + * * @return Request - * @throws \JsonException */ protected function createPurgeRequest(array $uris = []): Request { diff --git a/src/EventSubscriber/LocaleSubscriber.php b/src/EventSubscriber/LocaleSubscriber.php index 8583550f..cfdd022b 100644 --- a/src/EventSubscriber/LocaleSubscriber.php +++ b/src/EventSubscriber/LocaleSubscriber.php @@ -7,20 +7,19 @@ use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; use RZ\Roadiz\CoreBundle\Entity\Translation; -use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; -use RZ\Roadiz\CoreBundle\Repository\TranslationRepository; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\Routing\RequestContextAwareInterface; final class LocaleSubscriber implements EventSubscriberInterface { - public function __construct( - private readonly PreviewResolverInterface $previewResolver, - private readonly ManagerRegistry $managerRegistry, - private readonly RequestContextAwareInterface $router - ) { + private ManagerRegistry $managerRegistry; + private RequestContextAwareInterface $router; + + public function __construct(ManagerRegistry $managerRegistry, RequestContextAwareInterface $router) + { + $this->managerRegistry = $managerRegistry; + $this->router = $router; } /** @@ -30,44 +29,13 @@ public static function getSubscribedEvents(): array { return [ // must be registered just after Symfony\Component\HttpKernel\EventListener\LocaleListener - RequestEvent::class => ['onKernelRequest', 16], + RequestEvent::class => [['onKernelRequest', 16]], ]; } - private function getRepository(): TranslationRepository - { - return $this->managerRegistry->getRepository(Translation::class); - } - private function getDefaultTranslation(): ?TranslationInterface { - return $this->getRepository()->findDefault(); - } - - private function supportsLocale(?string $locale): bool - { - if (null === $locale || $locale === '') { - return false; - } - - if ($this->previewResolver->isPreview()) { - $locales = $this->getRepository()->getAllLocales(); - } else { - $locales = $this->getRepository()->getAvailableLocales(); - } - return \in_array( - $locale, - $locales, - true - ); - } - - private function getTranslationByLocale(string $locale): ?TranslationInterface - { - if ($this->previewResolver->isPreview()) { - return $this->getRepository()->findOneByLocaleOrOverrideLocale($locale); - } - return $this->getRepository()->findOneAvailableByLocaleOrOverrideLocale($locale); + return $this->managerRegistry->getRepository(Translation::class)->findDefault(); } public function onKernelRequest(RequestEvent $event): void @@ -75,53 +43,27 @@ public function onKernelRequest(RequestEvent $event): void $request = $event->getRequest(); $locale = $request->query->get('_locale') ?? $request->attributes->get('_locale'); - /* - * Set default locale - */ - if ($this->supportsLocale($locale)) { - $this->setTranslation($request, $this->getTranslationByLocale($locale)); - return; - } - - $statelessRoutes = [ - 'api_genid', - 'api_doc', - 'api_entrypoint', - 'api_graphql_entrypoint', - 'api_jsonld_context', - 'healthCheckAction', - 'interventionRequestProcess', - ]; - if ( - !\in_array($request->attributes->getString('_route'), $statelessRoutes, true) && - !$request->attributes->getBoolean('_stateless') && - $request->hasPreviousSession() - ) { - $sessionLocale = $request->getSession()->get('_locale', null); - if ($this->supportsLocale($sessionLocale)) { - $this->setTranslation($request, $this->getTranslationByLocale($sessionLocale)); - return; + if ($request->hasPreviousSession()) { + $locale = $request->getSession()->get('_locale', null); + if (null !== $locale) { + $this->setLocale($event, $locale); } } - if (null !== $translation = $this->getDefaultTranslation()) { - $this->setTranslation($request, $translation); - return; + /* + * Set default locale + */ + if (null !== $locale && $locale !== '') { + $this->setLocale($event, $locale); + } elseif (null !== $translation = $this->getDefaultTranslation()) { + $shortLocale = $translation->getLocale(); + $this->setLocale($event, $shortLocale); } } - private function setTranslation(Request $request, ?TranslationInterface $translation): void + private function setLocale(RequestEvent $event, string $locale): void { - if (null === $translation) { - return; - } - $locale = $translation->getPreferredLocale(); - /* - * Set current translation globally for controllers, utils, etc - */ - $request->attributes->set('_translation', $translation); - $request->attributes->set('_locale', $locale); - $request->setLocale($locale); + $event->getRequest()->setLocale($locale); \Locale::setDefault($locale); $this->router->getContext()->setParameter('_locale', $locale); } diff --git a/src/EventSubscriber/LoggableSubscriber.php b/src/EventSubscriber/LoggableSubscriber.php index 8bec9f86..9dc9a7ed 100644 --- a/src/EventSubscriber/LoggableSubscriber.php +++ b/src/EventSubscriber/LoggableSubscriber.php @@ -5,18 +5,27 @@ namespace RZ\Roadiz\CoreBundle\EventSubscriber; use Gedmo\Loggable\LoggableListener; -use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; final class LoggableSubscriber implements EventSubscriberInterface { + private LoggableListener $loggableListener; + private ?TokenStorageInterface $tokenStorage; + private ?AuthorizationCheckerInterface $authorizationChecker; + public function __construct( - private readonly LoggableListener $loggableListener, - private readonly Security $security, + LoggableListener $loggableListener, + TokenStorageInterface $tokenStorage = null, + AuthorizationCheckerInterface $authorizationChecker = null ) { + $this->loggableListener = $loggableListener; + $this->tokenStorage = $tokenStorage; + $this->authorizationChecker = $authorizationChecker; } public function onKernelRequest(RequestEvent $event): void @@ -25,12 +34,14 @@ public function onKernelRequest(RequestEvent $event): void return; } - if (null === $user = $this->security->getUser()) { + if (null === $this->tokenStorage || null === $this->authorizationChecker) { return; } - if ($this->security->isGranted('IS_AUTHENTICATED_REMEMBERED')) { - $this->loggableListener->setUsername($user); + $token = $this->tokenStorage->getToken(); + + if (null !== $token && $this->authorizationChecker->isGranted('IS_AUTHENTICATED_REMEMBERED')) { + $this->loggableListener->setUsername($token); } } diff --git a/src/EventSubscriber/NodeDuplicationSubscriber.php b/src/EventSubscriber/NodeDuplicationSubscriber.php index 6990f179..7370ee3d 100644 --- a/src/EventSubscriber/NodeDuplicationSubscriber.php +++ b/src/EventSubscriber/NodeDuplicationSubscriber.php @@ -10,18 +10,26 @@ use RZ\Roadiz\CoreBundle\EntityHandler\NodeHandler; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -final class NodeDuplicationSubscriber implements EventSubscriberInterface +class NodeDuplicationSubscriber implements EventSubscriberInterface { - public function __construct( - private readonly ManagerRegistry $managerRegistry, - private readonly HandlerFactoryInterface $handlerFactory - ) { + protected HandlerFactoryInterface $handlerFactory; + private ManagerRegistry $managerRegistry; + + /** + * @param ManagerRegistry $managerRegistry + * @param HandlerFactoryInterface $handlerFactory + */ + public function __construct(ManagerRegistry $managerRegistry, HandlerFactoryInterface $handlerFactory) + { + $this->handlerFactory = $handlerFactory; + $this->managerRegistry = $managerRegistry; } public static function getSubscribedEvents(): array { return [ NodeDuplicatedEvent::class => 'cleanPosition', + '\RZ\Roadiz\Core\Events\Node\NodeDuplicatedEvent' => 'cleanPosition', ]; } diff --git a/src/EventSubscriber/NodeNameSubscriber.php b/src/EventSubscriber/NodeNameSubscriber.php index 91659409..17517016 100644 --- a/src/EventSubscriber/NodeNameSubscriber.php +++ b/src/EventSubscriber/NodeNameSubscriber.php @@ -43,6 +43,7 @@ public static function getSubscribedEvents(): array { return [ NodesSourcesPreUpdatedEvent::class => ['onBeforeUpdate', 0], + '\RZ\Roadiz\Core\Events\NodesSources\NodesSourcesPreUpdatedEvent' => ['onBeforeUpdate', 0], ]; } diff --git a/src/EventSubscriber/NodeRedirectionSubscriber.php b/src/EventSubscriber/NodeRedirectionSubscriber.php index 2b3e00e8..31158fbb 100644 --- a/src/EventSubscriber/NodeRedirectionSubscriber.php +++ b/src/EventSubscriber/NodeRedirectionSubscriber.php @@ -10,23 +10,37 @@ use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\KernelInterface; /** * Subscribe to Node, NodesSources and UrlAlias event to clear ns url cache. */ class NodeRedirectionSubscriber implements EventSubscriberInterface { + protected NodeMover $nodeMover; + protected KernelInterface $kernel; + protected PreviewResolverInterface $previewResolver; + + /** + * @param NodeMover $nodeMover + * @param KernelInterface $kernel + * @param PreviewResolverInterface $previewResolver + */ public function __construct( - protected readonly NodeMover $nodeMover, - protected readonly string $kernelEnvironment, - protected readonly PreviewResolverInterface $previewResolver + NodeMover $nodeMover, + KernelInterface $kernel, + PreviewResolverInterface $previewResolver ) { + $this->nodeMover = $nodeMover; + $this->kernel = $kernel; + $this->previewResolver = $previewResolver; } public static function getSubscribedEvents(): array { return [ NodePathChangedEvent::class => 'redirectOldPaths', + '\RZ\Roadiz\Core\Events\Node\NodePathChangedEvent' => 'redirectOldPaths' ]; } @@ -43,7 +57,7 @@ public function redirectOldPaths( EventDispatcherInterface $dispatcher ): void { if ( - $this->kernelEnvironment === 'prod' && + $this->kernel->getEnvironment() === 'prod' && !$this->previewResolver->isPreview() && null !== $event->getNode() && $event->getNode()->isPublished() && diff --git a/src/EventSubscriber/NodeSourcePathSubscriber.php b/src/EventSubscriber/NodeSourcePathSubscriber.php index 0165266b..82dbaccf 100644 --- a/src/EventSubscriber/NodeSourcePathSubscriber.php +++ b/src/EventSubscriber/NodeSourcePathSubscriber.php @@ -9,11 +9,16 @@ use RZ\Roadiz\CoreBundle\Routing\NodesSourcesUrlGenerator; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -final class NodeSourcePathSubscriber implements EventSubscriberInterface +class NodeSourcePathSubscriber implements EventSubscriberInterface { - public function __construct( - private readonly NodesSourcesPathAggregator $pathAggregator - ) { + protected NodesSourcesPathAggregator $pathAggregator; + + /** + * @param NodesSourcesPathAggregator $pathAggregator + */ + public function __construct(NodesSourcesPathAggregator $pathAggregator) + { + $this->pathAggregator = $pathAggregator; } /** @@ -22,7 +27,8 @@ public function __construct( public static function getSubscribedEvents(): array { return [ - NodesSourcesPathGeneratingEvent::class => ['onNodesSourcesPath', -100], + NodesSourcesPathGeneratingEvent::class => [['onNodesSourcesPath', -100]], + '\RZ\Roadiz\Core\Events\NodesSources\NodesSourcesPathGeneratingEvent' => [['onNodesSourcesPath', -100]], ]; } @@ -33,6 +39,7 @@ public function onNodesSourcesPath(NodesSourcesPathGeneratingEvent $event): void { $urlGenerator = new NodesSourcesUrlGenerator( $this->pathAggregator, + null, $event->getNodeSource(), $event->isForceLocale(), $event->isForceLocaleWithUrlAlias() diff --git a/src/EventSubscriber/NodesSourcesAddHeadersSubscriber.php b/src/EventSubscriber/NodesSourcesAddHeadersSubscriber.php deleted file mode 100644 index 63808c9f..00000000 --- a/src/EventSubscriber/NodesSourcesAddHeadersSubscriber.php +++ /dev/null @@ -1,86 +0,0 @@ - ['onKernelResponse', 0] - ]; - } - - public function onKernelResponse(ResponseEvent $event): void - { - $request = $event->getRequest(); - $response = $event->getResponse(); - - if (!$request->isMethodCacheable()) { - return; - } - if (!$response->getContent() || !$response->isSuccessful()) { - return; - } - - $attributes = RequestAttributesExtractor::extractAttributes($request); - if (\count($attributes) < 1) { - return; - } - - if ($this->previewResolver->isPreview()) { - return; - } - - if ($this->security->isGranted('IS_AUTHENTICATED')) { - return; - } - - $resourceCacheHeaders = $attributes['cache_headers'] ?? []; - $data = $request->attributes->get('data'); - - // Work with WebResponse item instead of WebResponse itself - if ($data instanceof WebResponseInterface) { - $data = $data->getItem(); - } - - // if the public-property is defined and not yet set; apply it to the response - $public = $resourceCacheHeaders['public'] ?? null; - if (null !== $public && !$response->headers->hasCacheControlDirective('public')) { - $public ? $response->setPublic() : $response->setPrivate(); - } - - if (!$data instanceof NodesSources) { - return; - } - - if ($data->getNode()->getTtl() <= 0) { - return; - } - - if (null !== ($maxAge = $resourceCacheHeaders['max_age'] ?? $data->getNode()->getTtl()) && !$response->headers->hasCacheControlDirective('max-age')) { - $response->setMaxAge($maxAge * 60); - } - // Cache-Control "s-maxage" is only relevant is resource is not marked as "private" - if (false !== $public && null !== ($sharedMaxAge = $resourceCacheHeaders['shared_max_age'] ?? $data->getNode()->getTtl()) && !$response->headers->hasCacheControlDirective('s-maxage')) { - $response->setSharedMaxAge($sharedMaxAge * 60); - } - } -} diff --git a/src/EventSubscriber/NodesSourcesLinkHeaderEventSubscriber.php b/src/EventSubscriber/NodesSourcesLinkHeaderEventSubscriber.php index 3c0b5c91..a852c6eb 100644 --- a/src/EventSubscriber/NodesSourcesLinkHeaderEventSubscriber.php +++ b/src/EventSubscriber/NodesSourcesLinkHeaderEventSubscriber.php @@ -6,7 +6,6 @@ use Doctrine\Persistence\ManagerRegistry; use Psr\Link\EvolvableLinkProviderInterface; -use RZ\Roadiz\CoreBundle\Api\Model\WebResponseInterface; use RZ\Roadiz\CoreBundle\Entity\NodesSources; use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -15,12 +14,17 @@ use Symfony\Component\WebLink\GenericLinkProvider; use Symfony\Component\WebLink\Link; -final class NodesSourcesLinkHeaderEventSubscriber implements EventSubscriberInterface +class NodesSourcesLinkHeaderEventSubscriber implements EventSubscriberInterface { + private ManagerRegistry $managerRegistry; + private UrlGeneratorInterface $urlGenerator; + public function __construct( - private readonly ManagerRegistry $managerRegistry, - private readonly UrlGeneratorInterface $urlGenerator + ManagerRegistry $managerRegistry, + UrlGeneratorInterface $urlGenerator ) { + $this->managerRegistry = $managerRegistry; + $this->urlGenerator = $urlGenerator; } /** @@ -36,40 +40,33 @@ public static function getSubscribedEvents(): array public function onKernelView(ViewEvent $event): void { $request = $event->getRequest(); - $resources = $request->attributes->get('data'); + $resources = $request->attributes->get('data', null); $linkProvider = $request->attributes->get('_links', new GenericLinkProvider()); - // Work with WebResponse item instead of WebResponse itself - if ($resources instanceof WebResponseInterface) { - $resources = $resources->getItem(); - } - - if (!$resources instanceof NodesSources || !$linkProvider instanceof EvolvableLinkProviderInterface) { - return; - } - - /* - * Preview and authentication is handled at repository level. - */ - /** @var NodesSources[] $allSources */ - $allSources = $this->managerRegistry - ->getRepository(get_class($resources)) - ->findByNode($resources->getNode()); + if ($resources instanceof NodesSources && $linkProvider instanceof EvolvableLinkProviderInterface) { + /* + * Preview and authentication is handled at repository level. + */ + /** @var NodesSources[] $allSources */ + $allSources = $this->managerRegistry + ->getRepository(get_class($resources)) + ->findByNode($resources->getNode()); - foreach ($allSources as $singleSource) { - $linkProvider = $linkProvider->withLink( - (new Link( - 'alternate', - $this->urlGenerator->generate(RouteObjectInterface::OBJECT_BASED_ROUTE_NAME, [ - RouteObjectInterface::ROUTE_OBJECT => $singleSource - ]) - )) - ->withAttribute('hreflang', $singleSource->getTranslation()->getLocale()) - // Must encode translation name in base64 because headers are ASCII only - ->withAttribute('title', \base64_encode($singleSource->getTranslation()->getName())) - ->withAttribute('type', 'text/html') - ); + foreach ($allSources as $singleSource) { + $linkProvider = $linkProvider->withLink( + (new Link( + 'alternate', + $this->urlGenerator->generate(RouteObjectInterface::OBJECT_BASED_ROUTE_NAME, [ + RouteObjectInterface::ROUTE_OBJECT => $singleSource + ]) + )) + ->withAttribute('hreflang', $singleSource->getTranslation()->getLocale()) + // Must encode translation name in base64 because headers are ASCII only + ->withAttribute('title', \base64_encode($singleSource->getTranslation()->getName())) + ->withAttribute('type', 'text/html') + ); + } + $request->attributes->set('_links', $linkProvider); } - $request->attributes->set('_links', $linkProvider); } } diff --git a/src/EventSubscriber/NodesSourcesUniversalSubscriber.php b/src/EventSubscriber/NodesSourcesUniversalSubscriber.php index b16cb180..d13e80d6 100644 --- a/src/EventSubscriber/NodesSourcesUniversalSubscriber.php +++ b/src/EventSubscriber/NodesSourcesUniversalSubscriber.php @@ -33,6 +33,7 @@ public static function getSubscribedEvents(): array { return [ NodesSourcesUpdatedEvent::class => 'duplicateUniversalContents', + '\RZ\Roadiz\Core\Events\NodesSources\NodesSourcesUpdatedEvent' => 'duplicateUniversalContents', ]; } diff --git a/src/EventSubscriber/NodesSourcesUrlsCacheEventSubscriber.php b/src/EventSubscriber/NodesSourcesUrlsCacheEventSubscriber.php index 19a51cbd..771eea51 100644 --- a/src/EventSubscriber/NodesSourcesUrlsCacheEventSubscriber.php +++ b/src/EventSubscriber/NodesSourcesUrlsCacheEventSubscriber.php @@ -37,17 +37,28 @@ public static function getSubscribedEvents(): array { return [ NodesSourcesCreatedEvent::class => 'onPurgeRequest', + '\RZ\Roadiz\Core\Events\NodesSources\NodesSourcesCreatedEvent' => 'onPurgeRequest', NodesSourcesDeletedEvent::class => 'onPurgeRequest', + '\RZ\Roadiz\Core\Events\NodesSources\NodesSourcesDeletedEvent' => 'onPurgeRequest', TranslationUpdatedEvent::class => 'onPurgeRequest', + '\RZ\Roadiz\Core\Events\Translation\TranslationUpdatedEvent' => 'onPurgeRequest', TranslationDeletedEvent::class => 'onPurgeRequest', + '\RZ\Roadiz\Core\Events\Translation\TranslationDeletedEvent' => 'onPurgeRequest', NodeDeletedEvent::class => 'onPurgeRequest', + '\RZ\Roadiz\Core\Events\Node\NodeDeletedEvent' => 'onPurgeRequest', NodeUndeletedEvent::class => 'onPurgeRequest', + '\RZ\Roadiz\Core\Events\Node\NodeUndeletedEvent' => 'onPurgeRequest', NodeUpdatedEvent::class => 'onPurgeRequest', + '\RZ\Roadiz\Core\Events\Node\NodeUpdatedEvent' => 'onPurgeRequest', UrlAliasCreatedEvent::class => 'onPurgeRequest', + '\RZ\Roadiz\Core\Events\UrlAlias\UrlAliasCreatedEvent' => 'onPurgeRequest', UrlAliasUpdatedEvent::class => 'onPurgeRequest', + '\RZ\Roadiz\Core\Events\UrlAlias\UrlAliasUpdatedEvent' => 'onPurgeRequest', UrlAliasDeletedEvent::class => 'onPurgeRequest', + '\RZ\Roadiz\Core\Events\UrlAlias\UrlAliasDeletedEvent' => 'onPurgeRequest', 'workflow.node.completed' => 'onPurgeRequest', CachePurgeRequestEvent::class => ['onPurgeRequest', 3], + '\RZ\Roadiz\Core\Events\Cache\CachePurgeRequestEvent' => ['onPurgeRequest', 3], ]; } diff --git a/src/EventSubscriber/OPCacheEventSubscriber.php b/src/EventSubscriber/OPCacheEventSubscriber.php index fb080645..6ab73e53 100644 --- a/src/EventSubscriber/OPCacheEventSubscriber.php +++ b/src/EventSubscriber/OPCacheEventSubscriber.php @@ -17,6 +17,7 @@ public static function getSubscribedEvents(): array { return [ CachePurgeRequestEvent::class => ['onPurgeRequest', 3], + '\RZ\Roadiz\Core\Events\Cache\CachePurgeRequestEvent' => ['onPurgeRequest', 3], ]; } diff --git a/src/EventSubscriber/RealmNodeInheritanceSubscriber.php b/src/EventSubscriber/RealmNodeInheritanceSubscriber.php index 22ffbd6f..b18da0fa 100644 --- a/src/EventSubscriber/RealmNodeInheritanceSubscriber.php +++ b/src/EventSubscriber/RealmNodeInheritanceSubscriber.php @@ -59,7 +59,7 @@ public function onNodeJoinedRealm(AbstractRealmNodeEvent $event): void */ $this->bus->dispatch(new Envelope(new ApplyRealmNodeInheritanceMessage( $event->getRealmNode()->getNode()->getId(), - $event->getRealmNode()->getRealm()->getId() + $event->getRealmNode()->getRealm()?->getId() ))); } @@ -70,7 +70,7 @@ public function onNodeLeftRealm(AbstractRealmNodeEvent $event): void */ $this->bus->dispatch(new Envelope(new CleanRealmNodeInheritanceMessage( $event->getRealmNode()->getNode()->getId(), - $event->getRealmNode()->getRealm()->getId() + $event->getRealmNode()->getRealm()?->getId() ))); } } diff --git a/src/EventSubscriber/RedirectionCacheSubscriber.php b/src/EventSubscriber/RedirectionCacheSubscriber.php deleted file mode 100644 index 49dece5c..00000000 --- a/src/EventSubscriber/RedirectionCacheSubscriber.php +++ /dev/null @@ -1,40 +0,0 @@ -cacheAdapter = $cacheAdapter; - } - - /** - * @inheritDoc - */ - public static function getSubscribedEvents(): array - { - return [ - PostCreatedRedirectionEvent::class => 'clearCache', - PostDeletedRedirectionEvent::class => 'clearCache', - PostUpdatedRedirectionEvent::class => 'clearCache', - ]; - } - - public function clearCache(RedirectionEvent $event): void - { - $this->cacheAdapter->deleteItem(RedirectionPathResolver::CACHE_KEY); - } -} diff --git a/src/EventSubscriber/ReverseProxyCacheEventSubscriber.php b/src/EventSubscriber/ReverseProxyCacheEventSubscriber.php index d59055fc..eec69e0c 100644 --- a/src/EventSubscriber/ReverseProxyCacheEventSubscriber.php +++ b/src/EventSubscriber/ReverseProxyCacheEventSubscriber.php @@ -46,7 +46,9 @@ public static function getSubscribedEvents(): array { return [ CachePurgeRequestEvent::class => ['onBanRequest', 3], + '\RZ\Roadiz\Core\Events\Cache\CachePurgeRequestEvent' => ['onBanRequest', 3], NodesSourcesUpdatedEvent::class => ['onPurgeRequest', 3], + '\RZ\Roadiz\Core\Events\NodesSources\NodesSourcesUpdatedEvent' => ['onPurgeRequest', 3], 'workflow.node.completed' => ['onNodeWorkflowCompleted', 3], ]; } @@ -113,16 +115,9 @@ protected function createBanRequests(): array { $requests = []; foreach ($this->reverseProxyCacheLocator->getFrontends() as $frontend) { - // Add protocol if host does not start with it - if (!\str_starts_with($frontend->getHost(), 'http')) { - // Use HTTP to be able to call Varnish from a Docker network - $uri = 'http://' . $frontend->getHost(); - } else { - $uri = $frontend->getHost(); - } $requests[$frontend->getName()] = new Request( 'BAN', - $uri, + 'http://' . $frontend->getHost(), [ 'Host' => $frontend->getDomainName() ] diff --git a/src/EventSubscriber/RoleSubscriber.php b/src/EventSubscriber/RoleSubscriber.php index b3547449..d04eb2ab 100644 --- a/src/EventSubscriber/RoleSubscriber.php +++ b/src/EventSubscriber/RoleSubscriber.php @@ -37,8 +37,11 @@ public static function getSubscribedEvents(): array { return [ PreCreatedRoleEvent::class => 'onRoleChanged', + '\RZ\Roadiz\Core\Events\Role\PreCreatedRoleEvent' => 'onRoleChanged', PreUpdatedRoleEvent::class => 'onRoleChanged', + '\RZ\Roadiz\Core\Events\Role\PreUpdatedRoleEvent' => 'onRoleChanged', PreDeletedRoleEvent::class => 'onRoleChanged', + '\RZ\Roadiz\Core\Events\Role\PreDeletedRoleEvent' => 'onRoleChanged', ]; } diff --git a/src/EventSubscriber/SignatureSubscriber.php b/src/EventSubscriber/SignatureSubscriber.php index 68bcb97c..518e73b1 100644 --- a/src/EventSubscriber/SignatureSubscriber.php +++ b/src/EventSubscriber/SignatureSubscriber.php @@ -10,11 +10,15 @@ final class SignatureSubscriber implements EventSubscriberInterface { - public function __construct( - private readonly string $cmsVersion, - private readonly bool $hideRoadizVersion, - private readonly bool $debug = false - ) { + private string $version; + private bool $debug; + private bool $hideRoadizVersion; + + public function __construct(string $cmsVersion, bool $hideRoadizVersion, bool $debug = false) + { + $this->version = $cmsVersion; + $this->debug = $debug; + $this->hideRoadizVersion = $hideRoadizVersion; } /** * Filters the Response. @@ -30,8 +34,8 @@ public function onKernelResponse(ResponseEvent $event): void $response = $event->getResponse(); $response->headers->add(['X-Powered-By' => 'Roadiz CMS']); - if ($this->debug && $this->cmsVersion) { - $response->headers->add(['X-Version' => $this->cmsVersion]); + if ($this->debug && $this->version) { + $response->headers->add(['X-Version' => $this->version]); } } diff --git a/src/EventSubscriber/TagTimestampSubscriber.php b/src/EventSubscriber/TagTimestampSubscriber.php index 86b30b43..ca59a717 100644 --- a/src/EventSubscriber/TagTimestampSubscriber.php +++ b/src/EventSubscriber/TagTimestampSubscriber.php @@ -22,6 +22,9 @@ public static function getSubscribedEvents(): array public function onTagUpdatedEvent(TagUpdatedEvent $event): void { - $event->getTag()->setUpdatedAt(new \DateTime()); + $tag = $event->getTag(); + if ($tag instanceof AbstractDateTimed) { + $tag->setUpdatedAt(new \DateTime()); + } } } diff --git a/src/EventSubscriber/TranslationSubscriber.php b/src/EventSubscriber/TranslationSubscriber.php index d9b85f68..e7f61509 100644 --- a/src/EventSubscriber/TranslationSubscriber.php +++ b/src/EventSubscriber/TranslationSubscriber.php @@ -18,18 +18,24 @@ /** * Subscribe to Translation event to clear result cache. */ -final class TranslationSubscriber implements EventSubscriberInterface +class TranslationSubscriber implements EventSubscriberInterface { - public function __construct(private readonly ManagerRegistry $managerRegistry) + protected ManagerRegistry $managerRegistry; + + public function __construct(ManagerRegistry $managerRegistry) { + $this->managerRegistry = $managerRegistry; } public static function getSubscribedEvents(): array { return [ TranslationCreatedEvent::class => 'purgeCache', + '\RZ\Roadiz\Core\Events\Translation\TranslationCreatedEvent' => 'purgeCache', TranslationUpdatedEvent::class => 'purgeCache', + '\RZ\Roadiz\Core\Events\Translation\TranslationUpdatedEvent' => 'purgeCache', TranslationDeletedEvent::class => 'purgeCache', + '\RZ\Roadiz\Core\Events\Translation\TranslationDeletedEvent' => 'purgeCache', ]; } diff --git a/src/EventSubscriber/UserLocaleSubscriber.php b/src/EventSubscriber/UserLocaleSubscriber.php index 5b865a5c..fc75e89f 100644 --- a/src/EventSubscriber/UserLocaleSubscriber.php +++ b/src/EventSubscriber/UserLocaleSubscriber.php @@ -15,10 +15,15 @@ final class UserLocaleSubscriber implements EventSubscriberInterface { + private RequestStack $requestStack; + private TokenStorageInterface $tokenStorage; + public function __construct( - private readonly RequestStack $requestStack, - private readonly TokenStorageInterface $tokenStorage + RequestStack $requestStack, + TokenStorageInterface $tokenStorage ) { + $this->requestStack = $requestStack; + $this->tokenStorage = $tokenStorage; } /** @@ -29,7 +34,8 @@ public static function getSubscribedEvents(): array // must be registered after the default Locale listener return [ SecurityEvents::INTERACTIVE_LOGIN => 'onInteractiveLogin', - UserUpdatedEvent::class => 'onUserUpdated', + UserUpdatedEvent::class => [['onUserUpdated']], + '\RZ\Roadiz\Core\Events\User\UserUpdatedEvent' => [['onUserUpdated']], ]; } @@ -38,10 +44,6 @@ public static function getSubscribedEvents(): array */ public function onInteractiveLogin(InteractiveLoginEvent $event): void { - if ($this->requestStack->getMainRequest()?->attributes->getBoolean('_stateless')) { - return; - } - $user = $event->getAuthenticationToken()->getUser(); if ( @@ -57,15 +59,12 @@ public function onInteractiveLogin(InteractiveLoginEvent $event): void */ public function onUserUpdated(FilterUserEvent $event): void { - if ($this->requestStack->getMainRequest()?->attributes->getBoolean('_stateless')) { - return; - } $user = $event->getUser(); if ( null !== $this->tokenStorage->getToken() && $this->tokenStorage->getToken()->getUser() instanceof User && - $this->tokenStorage->getToken()->getUserIdentifier() === $user->getUserIdentifier() + $this->tokenStorage->getToken()->getUsername() === $user->getUsername() ) { if (null === $user->getLocale()) { $this->requestStack->getSession()->remove('_locale'); diff --git a/src/Exception/EmptySaltException.php b/src/Exception/EmptySaltException.php new file mode 100644 index 00000000..8ebc655b --- /dev/null +++ b/src/Exception/EmptySaltException.php @@ -0,0 +1,12 @@ +foreground_colors['black'] = '0;30'; + $this->foreground_colors['dark_gray'] = '1;30'; + $this->foreground_colors['blue'] = '0;34'; + $this->foreground_colors['light_blue'] = '1;34'; + $this->foreground_colors['green'] = '0;32'; + $this->foreground_colors['light_green'] = '1;32'; + $this->foreground_colors['cyan'] = '0;36'; + $this->foreground_colors['light_cyan'] = '1;36'; + $this->foreground_colors['red'] = '0;31'; + $this->foreground_colors['light_red'] = '1;31'; + $this->foreground_colors['purple'] = '0;35'; + $this->foreground_colors['light_purple'] = '1;35'; + $this->foreground_colors['brown'] = '0;33'; + $this->foreground_colors['yellow'] = '1;33'; + $this->foreground_colors['light_gray'] = '0;37'; + $this->foreground_colors['white'] = '1;37'; + + $this->background_colors['black'] = '40'; + $this->background_colors['red'] = '41'; + $this->background_colors['green'] = '42'; + $this->background_colors['yellow'] = '43'; + $this->background_colors['blue'] = '44'; + $this->background_colors['magenta'] = '45'; + $this->background_colors['cyan'] = '46'; + $this->background_colors['light_gray'] = '47'; + } + + /** + * @param \Exception|\TypeError $exception + * @return int + */ + public function getHttpStatusCode($exception): int + { + if ($exception instanceof HttpExceptionInterface) { + return $exception->getStatusCode(); + } elseif ($exception instanceof ResourceNotFoundException) { + return Response::HTTP_NOT_FOUND; + } elseif ($exception instanceof MaintenanceModeException) { + return Response::HTTP_SERVICE_UNAVAILABLE; + } elseif ($exception instanceof AccessDeniedException || $exception instanceof AccessDeniedHttpException) { + return Response::HTTP_FORBIDDEN; + } + + return Response::HTTP_INTERNAL_SERVER_ERROR; + } + + /** + * @param \Exception|\TypeError $e + * @return string + */ + public function getHumanExceptionTitle($e): string + { + if ($e instanceof MaintenanceModeException) { + return "Website is under maintenance."; + } + + if ($e instanceof NoConfigurationFoundException) { + return "No configuration file has been found. Did you run composer install before using Roadiz?"; + } + + if ($e instanceof InvalidConfigurationException) { + return "Roadiz configuration is not valid."; + } + + if ($e instanceof ResourceNotFoundException || $e instanceof NotFoundHttpException) { + return "Resource not found."; + } + + if ($e instanceof ConnectionException || $e instanceof \Doctrine\DBAL\ConnectionException) { + return "Your database is not reachable. Did you run install before using Roadiz?"; + } + + if ($e instanceof TableNotFoundException) { + return "Your database is not synchronised to Roadiz data schema. Did you run install before using Roadiz?"; + } + + if ($e instanceof AccessDeniedException || $e instanceof AccessDeniedHttpException) { + return "Oups! Wrong way, you are not supposed to be here."; + } + + return "A problem occurred on our website. We are working on this to be back soon."; + } + + /** + * @param \Exception|\TypeError $e + * @return string + */ + public function getJsonError($e): string + { + if ($e instanceof NoConfigurationFoundException) { + return "no_configuration_file"; + } + + if ($e instanceof InvalidConfigurationException) { + return "invalid_configuration"; + } + + if ($e instanceof ResourceNotFoundException || $e instanceof NotFoundHttpException) { + return "not_found"; + } + + if ($e instanceof ConnectionException || $e instanceof \Doctrine\DBAL\ConnectionException) { + return "database_not_reachable"; + } + + if ($e instanceof TableNotFoundException) { + return "database_not_uptodate"; + } + + if ($e instanceof AccessDeniedException || $e instanceof AccessDeniedHttpException) { + return "access_denied"; + } + + return "general_error"; + } + + + /** + * @param \Exception|\TypeError $e + * @param Request $request + * @param bool $debug + * @return JsonResponse|Response + */ + public function getResponse($e, Request $request, bool $debug = false): Response + { + /* + * Log error before displaying a fallback page. + */ + $class = get_class($e); + + $humanMessage = $this->getHumanExceptionTitle($e); + + if (php_sapi_name() === 'cli') { + return new Response( + implode(PHP_EOL, [ + $this->getColoredString('[' . $class . ']', 'white', 'red'), + $this->getColoredString($e->getMessage(), 'red', null), + ]) . PHP_EOL, + $this->getHttpStatusCode($e), + [ + 'content-type' => 'text/plain', + ] + ); + } elseif ($this->isFormatJson($request)) { + $data = [ + 'error' => $this->getJsonError($e), + 'error_message' => $e->getMessage(), + 'message' => $e->getMessage(), + 'exception' => $class, + 'humanMessage' => $humanMessage, + 'status' => $this->getHttpStatusCode($e), + ]; + if ($debug) { + $data['error_trace'] = $e->getTrace(); + } + return new JsonResponse($data, $this->getHttpStatusCode($e)); + } else { + $html = file_get_contents(dirname(__DIR__) . '/../templates/emerg.html'); + $html = str_replace('{{ http_code }}', (string) $this->getHttpStatusCode($e), $html); + $html = str_replace('{{ human_message }}', $humanMessage, $html); + + if ($e instanceof MaintenanceModeException) { + $html = str_replace('{{ smiley }}', '🏗', $html); + } elseif ($this->getHttpStatusCode($e) === Response::HTTP_FORBIDDEN) { + $html = str_replace('{{ smiley }}', '🤔', $html); + } elseif ($this->getHttpStatusCode($e) === Response::HTTP_NOT_FOUND) { + $html = str_replace('{{ smiley }}', '🧐', $html); + } else { + $html = str_replace('{{ smiley }}', '🤕', $html); + } + + if ($debug) { + $html = str_replace('{{ message }}', $e->getMessage(), $html); + $trace = preg_replace('#([^\n]+)#', '

$1

', $e->getTraceAsString()); + $trace = $this->addTwigSource($e, $trace); + $html = str_replace('{{ details }}', $trace, $html); + $html = str_replace('{{ exception }}', $class, $html); + } else { + $html = str_replace('{{ message }}', '', $html); + $html = str_replace('{{ details }}', '', $html); + $html = str_replace('{{ exception }}', '', $html); + } + + return new Response( + $html, + $this->getHttpStatusCode($e), + [ + 'content-type' => 'text/html', + 'X-Error-Reason' => $e->getMessage(), + ] + ); + } + } + + /** + * @param \Exception|\TypeError $e + * @param string $trace + * + * @return string + */ + protected function addTwigSource($e, string $trace): string + { + if ($e instanceof SyntaxError && null !== $e->getSourceContext()) { + return '' . PHP_EOL . + '' . PHP_EOL . + '
Template' . $e->getSourceContext()->getName() . '
Line number' . $e->getTemplateLine() . '
Path' . $e->getSourceContext()->getPath() . '
' . PHP_EOL . + $trace; + } elseif ($e instanceof Error && null !== $e->getSourceContext()) { + return '' . PHP_EOL . + '
Template' . $e->getSourceContext()->getName() . '
' . $e->getSourceContext()->getPath() . '
' . PHP_EOL . + $trace; + } + return $trace; + } + + /** + * @param Request $request + * @return bool + */ + public function isFormatJson(Request $request): bool + { + if ( + $request->attributes->has('_format') && + ( + $request->attributes->get('_format') == 'json' || + $request->attributes->get('_format') == 'ld+json' + ) + ) { + return true; + } + + if ( + $request->headers->get('Content-Type') && + ( + 0 === \mb_strpos($request->headers->get('Content-Type'), 'application/json') || + 0 === \mb_strpos($request->headers->get('Content-Type'), 'application/ld+json') + ) + ) { + return true; + } + + if ( + in_array('application/json', $request->getAcceptableContentTypes()) || + in_array('application/ld+json', $request->getAcceptableContentTypes()) + ) { + return true; + } + + return false; + } + + /** + * @param string $string + * @param string|null $foreground_color + * @param string|null $background_color + * @return string + */ + public function getColoredString(string $string, $foreground_color = null, $background_color = null): string + { + $colored_string = ""; + + // Check if given foreground color found + if (isset($this->foreground_colors[$foreground_color])) { + $colored_string .= "\033[" . $this->foreground_colors[$foreground_color] . "m"; + } + // Check if given background color found + if (isset($this->background_colors[$background_color])) { + $colored_string .= "\033[" . $this->background_colors[$background_color] . "m"; + } + + // Add string and end coloring + $colored_string .= $string . "\033[0m"; + + return $colored_string; + } + + /** + * @return array Returns all foreground color names + */ + public function getForegroundColors(): array + { + return array_keys($this->foreground_colors); + } + + /** + * @return array Returns all background color names + */ + public function getBackgroundColors(): array + { + return array_keys($this->background_colors); + } +} diff --git a/src/Exception/TooManyLoginAttemptsException.php b/src/Exception/TooManyLoginAttemptsException.php new file mode 100644 index 00000000..a15276b0 --- /dev/null +++ b/src/Exception/TooManyLoginAttemptsException.php @@ -0,0 +1,18 @@ + 'ASC'] ); foreach ($attributes as $attribute) { - $label = $attribute->getLabelOrCode($options['translation']); - if ( - null !== $attribute->getGroup() && - null !== $groupName = $attribute->getGroup()->getName() - ) { - if (!isset($choices[$groupName]) || !is_array($choices[$groupName])) { - $choices[$groupName] = []; + if (null !== $attribute->getGroup()) { + if (!isset($choices[$attribute->getGroup()->getName()])) { + $choices[$attribute->getGroup()->getName()] = []; } - $choices[$groupName][$label] = $attribute->getId(); + $choices[$attribute->getGroup()->getName()][$attribute->getLabelOrCode($options['translation'])] = $attribute->getId(); } else { - $choices[$label] = $attribute->getId(); + $choices[$attribute->getLabelOrCode($options['translation'])] = $attribute->getId(); } } return $choices; diff --git a/src/Form/AttributeType.php b/src/Form/AttributeType.php index cd461657..f4400d74 100644 --- a/src/Form/AttributeType.php +++ b/src/Form/AttributeType.php @@ -10,7 +10,6 @@ use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\CollectionType; -use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -60,18 +59,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, 'help' => 'attributes.form_help.searchable' ]) - ->add('weight', NumberType::class, [ - 'label' => 'attributes.weight', - 'required' => false, - 'scale' => 1, - 'help' => 'attributes.form_help.weight' - ]) - ->add('defaultRealm', RealmChoiceType::class, [ - 'label' => 'attributes.defaultRealm', - 'help' => 'attributes.defaultRealm.help', - 'placeholder' => 'attributes.defaultRealm.placeholder', - 'required' => false, - ]) ->add('attributeTranslations', CollectionType::class, [ 'label' => 'attributes.form.attributeTranslations', 'allow_add' => true, diff --git a/src/Form/AttributeValueRealmType.php b/src/Form/AttributeValueRealmType.php deleted file mode 100644 index 540fb051..00000000 --- a/src/Form/AttributeValueRealmType.php +++ /dev/null @@ -1,30 +0,0 @@ -add('realm', RealmChoiceType::class, [ - 'label' => false, - 'placeholder' => 'attributeValue.realm.placeholder', - 'required' => false, - ]); - } - - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults([ - 'label' => false, - 'data_class' => AttributeValueInterface::class, - ]); - } -} diff --git a/src/Form/AttributeValueTranslationType.php b/src/Form/AttributeValueTranslationType.php index 0e3b2e8b..d6e5ad42 100644 --- a/src/Form/AttributeValueTranslationType.php +++ b/src/Form/AttributeValueTranslationType.php @@ -13,12 +13,10 @@ use Symfony\Component\Form\Extension\Core\Type\DateTimeType; use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\EmailType; -use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\Extension\Core\Type\IntegerType; use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Constraints\Length; @@ -105,9 +103,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void break; } } - $builder->add('attributeValue', AttributeValueRealmType::class, [ - 'label' => false, - ]); } /** @@ -139,15 +134,6 @@ protected function getOptions(AttributeValueTranslationInterface $attributeValue ], $options ?: []); } - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults([ - 'label' => false, - 'data_class' => AttributeValueTranslationInterface::class, - ]); - } - - /** * @inheritDoc */ diff --git a/src/Form/Constraint/NodeTypeFieldValidator.php b/src/Form/Constraint/NodeTypeFieldValidator.php index 696252df..9ba3b5c0 100644 --- a/src/Form/Constraint/NodeTypeFieldValidator.php +++ b/src/Form/Constraint/NodeTypeFieldValidator.php @@ -4,7 +4,6 @@ namespace RZ\Roadiz\CoreBundle\Form\Constraint; -use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; use RZ\Roadiz\CoreBundle\Configuration\CollectionFieldConfiguration; use RZ\Roadiz\CoreBundle\Configuration\JoinNodeTypeFieldConfiguration; @@ -20,46 +19,23 @@ class NodeTypeFieldValidator extends ConstraintValidator { - public function __construct( - private readonly ManagerRegistry $registry, - ) { - } - public function validate(mixed $value, Constraint $constraint): void { - if (!$value instanceof NodeTypeFieldEntity) { - $this->context->buildViolation('Value is not a valid NodeTypeField.')->addViolation(); - return; - } - - $existingNodeTypeFieldsByName = $this->registry->getRepository(NodeTypeFieldEntity::class)->findBy([ - 'name' => $value->getName(), - ]); - foreach ($existingNodeTypeFieldsByName as $item) { - if ($item->getId() === $value->getId()) { - continue; + if ($value instanceof NodeTypeFieldEntity) { + if ($value->isMarkdown()) { + $this->validateMarkdownOptions($value); } - if ($item->getDoctrineType() !== $value->getDoctrineType()) { - $this->context->buildViolation('field_with_same_name_already_exists_but_with_different_doctrine_type') - ->setParameter('%name%', $item->getName()) - ->setParameter('%nodeTypeName%', $item->getNodeTypeName()) - ->setParameter('%type%', $item->getDoctrineType()) - ->atPath('name') - ->addViolation(); + if ($value->isManyToMany() || $value->isManyToOne()) { + $this->validateJoinTypes($value, $constraint); } - } - - if ($value->isMarkdown()) { - $this->validateMarkdownOptions($value); - } - if ($value->isManyToMany() || $value->isManyToOne()) { - $this->validateJoinTypes($value, $constraint); - } - if ($value->isMultiProvider() || $value->isSingleProvider()) { - $this->validateProviderTypes($value, $constraint); - } - if ($value->isCollection()) { - $this->validateCollectionTypes($value, $constraint); + if ($value->isMultiProvider() || $value->isSingleProvider()) { + $this->validateProviderTypes($value, $constraint); + } + if ($value->isCollection()) { + $this->validateCollectionTypes($value, $constraint); + } + } else { + $this->context->buildViolation('Value is not a valid NodeTypeField.')->addViolation(); } } diff --git a/src/Form/Constraint/UniqueEntity.php b/src/Form/Constraint/UniqueEntity.php new file mode 100644 index 00000000..f5bfd400 --- /dev/null +++ b/src/Form/Constraint/UniqueEntity.php @@ -0,0 +1,42 @@ + + * @see https://github.com/symfony/doctrine-bridge/blob/master/Validator/Constraints/UniqueEntity.php + * @deprecated Use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity + */ +class UniqueEntity extends Constraint +{ + public const NOT_UNIQUE_ERROR = '23bd9dbf-6b9b-41cd-a99e-4844bcf3077f'; + + public string $message = 'value.is.already.used'; + /** + * @var class-string|null + */ + public ?string $entityClass = null; + public string $repositoryMethod = 'findBy'; + public ?string $errorPath = null; + public array $fields = []; + public bool $ignoreNull = true; + + public function getRequiredOptions(): array + { + return ['fields']; + } + + public function getDefaultOption(): string + { + return 'fields'; + } +} diff --git a/src/Form/Constraint/UniqueEntityValidator.php b/src/Form/Constraint/UniqueEntityValidator.php new file mode 100644 index 00000000..701af696 --- /dev/null +++ b/src/Form/Constraint/UniqueEntityValidator.php @@ -0,0 +1,171 @@ + + * @package RZ\Roadiz\CoreBundle\Form\Constraint + * @see https://github.com/symfony/doctrine-bridge/blob/master/Validator/Constraints/UniqueEntityValidator.php + * @deprecated Use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntityValidator + */ +class UniqueEntityValidator extends ConstraintValidator +{ + protected ManagerRegistry $managerRegistry; + + /** + * @param ManagerRegistry $managerRegistry + */ + public function __construct(ManagerRegistry $managerRegistry) + { + $this->managerRegistry = $managerRegistry; + } + + /** + * @param mixed $value + * @param UniqueEntity $constraint + * + * @throws \Exception + */ + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof UniqueEntity) { + throw new UnexpectedTypeException($constraint, __NAMESPACE__ . '\UniqueEntity'); + } + + $fields = $constraint->fields; + if (0 === count($fields)) { + throw new ConstraintDefinitionException('At least one field has to be specified.'); + } + + $class = $this->managerRegistry + ->getManagerForClass(get_class($value)) + ->getClassMetadata(get_class($value)); + + $criteria = []; + $hasNullValue = false; + foreach ($fields as $fieldName) { + if (!$class instanceof ClassMetadataInfo) { + throw new ConstraintDefinitionException(sprintf('The class "%s" is not mapped by Doctrine, so it cannot be validated for uniqueness.', get_class($value))); + } + if (!$class->hasField($fieldName) && !$class->hasAssociation($fieldName)) { + throw new ConstraintDefinitionException(sprintf('The field "%s" is not mapped by Doctrine, so it cannot be validated for uniqueness.', $fieldName)); + } + $fieldValue = $class->getReflectionProperty($fieldName)->getValue($value); + + if (null === $fieldValue) { + $hasNullValue = true; + } + if ($constraint->ignoreNull && null === $fieldValue) { + continue; + } + $criteria[$fieldName] = $fieldValue; + if (null !== $criteria[$fieldName] && $class->hasAssociation($fieldName)) { + /* Ensure the Proxy is initialized before using reflection to + * read its identifiers. This is necessary because the wrapped + * getter methods in the Proxy are being bypassed. + */ + $this->managerRegistry + ->getManagerForClass(get_class($value)) + ->initializeObject($criteria[$fieldName]); + } + } + // validation doesn't fail if one of the fields is null and if null values should be ignored + if ($hasNullValue && $constraint->ignoreNull) { + return; + } + // skip validation if there are no criteria (this can happen when the + // "ignoreNull" option is enabled and fields to be checked are null + if (empty($criteria)) { + return; + } + if (null !== $constraint->entityClass) { + /* Retrieve repository from given entity name. + * We ensure the retrieved repository can handle the entity + * by checking the entity is the same, or subclass of the supported entity. + */ + $repository = $this->managerRegistry->getRepository($constraint->entityClass); + $supportedClass = $repository->getClassName(); + if (!$value instanceof $supportedClass) { + throw new ConstraintDefinitionException(sprintf('The "%s" entity repository does not support the "%s" entity. The entity should be an instance of or extend "%s".', $constraint->entityClass, $class->getName(), $supportedClass)); + } + } else { + $repository = $this->managerRegistry->getRepository(get_class($value)); + } + $result = $repository->{$constraint->repositoryMethod}($criteria); + if ($result instanceof \IteratorAggregate) { + $result = $result->getIterator(); + } + /* If the result is a MongoCursor, it must be advanced to the first + * element. Rewinding should have no ill effect if $result is another + * iterator implementation. + */ + if ($result instanceof \Iterator) { + $result->rewind(); + } elseif (is_array($result)) { + reset($result); + } + /* If no entity matched the query criteria or a single entity matched, + * which is the same as the entity being validated, the criteria is + * unique. + */ + if (0 === count($result) || (1 === count($result) && $value === ($result instanceof \Iterator ? $result->current() : current($result)))) { + return; + } + + $errorPath = null !== $constraint->errorPath ? $constraint->errorPath : $fields[0]; + $invalidValue = $criteria[$errorPath] ?? $criteria[$fields[0]]; + + $this->context->buildViolation($constraint->message) + ->atPath($errorPath) + ->setParameter('{{ value }}', $this->formatWithIdentifiers($class, $invalidValue)) + ->setInvalidValue($invalidValue) + ->setCode(UniqueEntity::NOT_UNIQUE_ERROR) + ->addViolation(); + } + + private function formatWithIdentifiers(ClassMetadata $class, mixed $value): string + { + if (!is_object($value) || $value instanceof \DateTimeInterface) { + return $this->formatValue($value, self::PRETTY_DATE); + } + if ($class->getName() !== $idClass = get_class($value)) { + // non unique value might be a composite PK that consists of other entity objects + if ($this->managerRegistry->getManagerForClass($idClass)->getMetadataFactory()->hasMetadataFor($idClass)) { + $identifiers = $this->managerRegistry + ->getManagerForClass($idClass) + ->getClassMetadata($idClass) + ->getIdentifierValues($value); + } else { + // this case might happen if the non unique column has a custom doctrine type and its value is an object + // in which case we cannot get any identifiers for it + $identifiers = []; + } + } else { + $identifiers = $class->getIdentifierValues($value); + } + if (!$identifiers) { + return sprintf('object("%s")', $idClass); + } + array_walk($identifiers, function (&$id, $field) { + if (!is_object($id) || $id instanceof \DateTimeInterface) { + $idAsString = $this->formatValue($id, self::PRETTY_DATE); + } else { + $idAsString = sprintf('object("%s")', get_class($id)); + } + $id = sprintf('%s => %s', $field, $idAsString); + }); + return sprintf('object("%s") identified by (%s)', $idClass, implode(', ', $identifiers)); + } +} diff --git a/src/Form/CustomFormsType.php b/src/Form/CustomFormsType.php index 5cd0711c..ad40496f 100644 --- a/src/Form/CustomFormsType.php +++ b/src/Form/CustomFormsType.php @@ -105,12 +105,11 @@ protected function getFieldsByGroups(array $options): array /** @var CustomFormField $field */ foreach ($fields as $field) { - $groupName = $field->getGroupName(); - if (\is_string($groupName) && $groupName !== '') { - if (!isset($fieldsArray[$groupName]) || !\is_array($fieldsArray[$groupName])) { - $fieldsArray[$groupName] = []; + if ($field->getGroupName() != '') { + if (!isset($fieldsArray[$field->getGroupName()])) { + $fieldsArray[$field->getGroupName()] = []; } - $fieldsArray[$groupName][] = $field; + $fieldsArray[$field->getGroupName()][] = $field; } else { $fieldsArray[] = $field; } diff --git a/src/Form/DataListTextType.php b/src/Form/DataListTextType.php deleted file mode 100644 index 4cf6b095..00000000 --- a/src/Form/DataListTextType.php +++ /dev/null @@ -1,43 +0,0 @@ -setRequired('listName'); - $resolver->setAllowedTypes('listName', 'string'); - $resolver->setRequired('list'); - $resolver->setAllowedTypes('list', 'array'); - } - - public function buildView(FormView $view, FormInterface $form, array $options): void - { - parent::buildView($view, $form, $options); - - $view->vars['listName'] = $options['listName']; - $view->vars['list'] = $options['list']; - } - - - public function getBlockPrefix(): string - { - return 'data_list_text'; - } - - public function getParent(): string - { - return TextType::class; - } -} diff --git a/src/Form/DataTransformer/ExplorerProviderItemTransformer.php b/src/Form/DataTransformer/ExplorerProviderItemTransformer.php index f94a7863..6d899917 100644 --- a/src/Form/DataTransformer/ExplorerProviderItemTransformer.php +++ b/src/Form/DataTransformer/ExplorerProviderItemTransformer.php @@ -70,7 +70,7 @@ public function reverseTransform(mixed $value): mixed $items = []; } elseif ($value instanceof ExplorerItemInterface) { $items = [$value]; - } elseif (\is_string($value) || \is_int($value)) { + } elseif (is_scalar($value)) { $items = $this->explorerProvider->getItemsById([$value]); } elseif (\is_array($value) && is_scalar(reset($value))) { $items = $this->explorerProvider->getItemsById($value); diff --git a/src/Form/DataTransformer/JoinDataTransformer.php b/src/Form/DataTransformer/JoinDataTransformer.php index 5259300a..72825f6b 100644 --- a/src/Form/DataTransformer/JoinDataTransformer.php +++ b/src/Form/DataTransformer/JoinDataTransformer.php @@ -24,7 +24,7 @@ class JoinDataTransformer implements DataTransformerInterface /** * @param NodeTypeField $nodeTypeField * @param ManagerRegistry $managerRegistry - * @param class-string $entityClassname + * @param string $entityClassname */ public function __construct( NodeTypeField $nodeTypeField, @@ -74,7 +74,6 @@ public function transform(mixed $value): array public function reverseTransform(mixed $value): mixed { if ($this->nodeTypeField->isManyToMany()) { - /** @var PersistableInterface[] $unorderedEntities */ $unorderedEntities = $this->managerRegistry->getRepository($this->entityClassname)->findBy([ 'id' => $value, ]); diff --git a/src/Form/RoleEntityType.php b/src/Form/RoleEntityType.php index 44d53e57..6e0a4cf9 100644 --- a/src/Form/RoleEntityType.php +++ b/src/Form/RoleEntityType.php @@ -10,7 +10,7 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Security; final class RoleEntityType extends AbstractType { diff --git a/src/Form/SettingType.php b/src/Form/SettingType.php index 56392a67..cd5b2613 100644 --- a/src/Form/SettingType.php +++ b/src/Form/SettingType.php @@ -48,6 +48,10 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'label' => 'visible', 'required' => false, ]) + ->add('encrypted', CheckboxType::class, [ + 'label' => 'encrypted', + 'required' => false, + ]) ->add('type', ChoiceType::class, [ 'label' => 'type', 'required' => true, diff --git a/src/Form/ThemesType.php b/src/Form/ThemesType.php index e47a6590..de4becbb 100644 --- a/src/Form/ThemesType.php +++ b/src/Form/ThemesType.php @@ -28,9 +28,7 @@ public function configureOptions(OptionsResolver $resolver): void $value = []; foreach ($options['themes_config'] as $themeConfig) { $class = $themeConfig['classname']; - /** @var callable $callable */ - $callable = [$class, 'getThemeName']; - $value[call_user_func($callable)] = $class; + $value[call_user_func([$class, 'getThemeName'])] = $class; } return $value; }); diff --git a/src/Importer/NodeTypesImporter.php b/src/Importer/NodeTypesImporter.php index 0a8c4077..da1830a7 100644 --- a/src/Importer/NodeTypesImporter.php +++ b/src/Importer/NodeTypesImporter.php @@ -6,16 +6,29 @@ use JMS\Serializer\DeserializationContext; use JMS\Serializer\SerializerInterface; +use RZ\Roadiz\Core\Handlers\HandlerFactoryInterface; use RZ\Roadiz\CoreBundle\Entity\NodeType; +use RZ\Roadiz\CoreBundle\EntityHandler\NodeTypeHandler; +use RZ\Roadiz\CoreBundle\Message\UpdateNodeTypeSchemaMessage; use RZ\Roadiz\CoreBundle\Serializer\ObjectConstructor\TypedObjectConstructorInterface; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\MessageBusInterface; class NodeTypesImporter implements EntityImporterInterface { - public function __construct( - protected SerializerInterface $serializer - ) { + protected SerializerInterface $serializer; + protected HandlerFactoryInterface $handlerFactory; + + + public function __construct(SerializerInterface $serializer, HandlerFactoryInterface $handlerFactory) + { + $this->serializer = $serializer; + $this->handlerFactory = $handlerFactory; } + /** + * @inheritDoc + */ public function supports(string $entityClass): bool { return $entityClass === NodeType::class; @@ -26,7 +39,7 @@ public function supports(string $entityClass): bool */ public function import(string $serializedData): bool { - $this->serializer->deserialize( + $nodeType = $this->serializer->deserialize( $serializedData, NodeType::class, 'json', @@ -35,6 +48,10 @@ public function import(string $serializedData): bool ->setAttribute(TypedObjectConstructorInterface::FLUSH_NEW_OBJECTS, true) ); + /** @var NodeTypeHandler $nodeTypeHandler */ + $nodeTypeHandler = $this->handlerFactory->getHandler($nodeType); + $nodeTypeHandler->updateSchema(); + return true; } } diff --git a/src/ListManager/AbstractEntityListManager.php b/src/ListManager/AbstractEntityListManager.php index 128c964c..6e54c0e3 100644 --- a/src/ListManager/AbstractEntityListManager.php +++ b/src/ListManager/AbstractEntityListManager.php @@ -13,7 +13,6 @@ abstract class AbstractEntityListManager implements EntityListManagerInterface protected ?array $queryArray = null; protected ?int $currentPage = null; protected ?int $itemPerPage = null; - protected ?string $searchPattern = null; protected bool $displayNotPublishedNodes; protected bool $displayAllNodesStatuses; protected bool $allowRequestSorting = true; @@ -137,7 +136,6 @@ public function getAssignation(): array 'pageCount' => $this->getPageCount(), 'itemPerPage' => $this->getItemPerPage(), 'itemCount' => $this->getItemCount(), - 'search' => $this->searchPattern, 'nextPageQuery' => null, 'previousPageQuery' => null, ]; @@ -216,72 +214,6 @@ public function getPageCount(): int return (int) ceil($this->getItemCount() / $this->getItemPerPage()); } - protected function handleRequestQuery(bool $disabled): void - { - if ($disabled || null === $this->request) { - /* - * Disable pagination and paginator - */ - $this->disablePagination(); - return; - } - - $field = $this->request->query->get('field'); - $ordering = $this->request->query->get('ordering'); - $search = $this->request->query->get('search'); - $itemPerPage = $this->request->query->get('item_per_page') ?? - $this->request->query->get('itemPerPage') ?? - $this->request->query->get('itemsPerPage'); - $page = $this->request->query->get('page'); - - if ( - $this->allowRequestSorting && - \is_string($field) && - $field !== "" && - \is_string($ordering) && - \in_array(strtolower($ordering), ['asc', 'desc']) - ) { - $this->handleOrderingParam($field, $ordering); - $this->queryArray['field'] = $field; - $this->queryArray['ordering'] = $ordering; - } - - if ( - $this->allowRequestSearching && - \is_string($search) && - $search !== "" - ) { - $this->handleSearchParam($search); - $this->queryArray['search'] = $search; - } - - if ( - \is_numeric($itemPerPage) && - ((int) $itemPerPage) > 0 - ) { - $this->setItemPerPage((int) $itemPerPage); - } - - if ( - \is_numeric($page) && - ((int) $page) > 1 - ) { - $this->setPage((int) $page); - } else { - $this->setPage(1); - } - } - - protected function handleSearchParam(string $search): void - { - $this->searchPattern = $search; - } - - protected function handleOrderingParam(string $field, string $ordering): void - { - // Do nothing on abstract - } - protected function validateOrderingFieldName(string $field): void { // check if field is a valid name without any SQL injection diff --git a/src/ListManager/EntityListManager.php b/src/ListManager/EntityListManager.php index f46adaa8..7822db82 100644 --- a/src/ListManager/EntityListManager.php +++ b/src/ListManager/EntityListManager.php @@ -4,7 +4,6 @@ namespace RZ\Roadiz\CoreBundle\ListManager; -use Doctrine\ORM\Tools\Pagination\Paginator as DoctrinePaginator; use Doctrine\Persistence\ObjectManager; use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; @@ -14,6 +13,7 @@ use RZ\Roadiz\CoreBundle\Repository\NodeRepository; use RZ\Roadiz\CoreBundle\Repository\StatusAwareRepository; use Symfony\Component\HttpFoundation\Request; +use Doctrine\ORM\Tools\Pagination\Paginator as DoctrinePaginator; /** * Perform basic filtering and search over entity listings. @@ -28,6 +28,7 @@ class EntityListManager extends AbstractEntityListManager protected ?Paginator $paginator = null; protected ?array $orderingArray = null; protected ?array $filteringArray = null; + protected ?string $searchPattern = null; protected ?array $assignation = null; protected ?TranslationInterface $translation = null; @@ -118,30 +119,59 @@ public function handle(bool $disabled = false) unset($this->filteringArray["chroot"]); // remove placeholder } - $this->handleRequestQuery($disabled); + if (false === $disabled && null !== $this->request) { + if ( + $this->allowRequestSorting && + $this->request->query->get('field') && + $this->request->query->get('ordering') + ) { + $this->validateOrderingFieldName($this->request->query->get('field')); + $this->orderingArray = [ + $this->request->query->get('field') => $this->request->query->get('ordering') + ]; + $this->queryArray['field'] = $this->request->query->get('field'); + $this->queryArray['ordering'] = $this->request->query->get('ordering'); + } + + if ($this->allowRequestSearching && $this->request->query->get('search') != "") { + $this->searchPattern = $this->request->query->get('search'); + $this->queryArray['search'] = $this->request->query->get('search'); + } + + if ( + $this->request->query->has('item_per_page') && + $this->request->query->get('item_per_page') > 0 + ) { + $this->setItemPerPage((int) $this->request->query->get('item_per_page')); + } + + if ( + $this->request->query->has('page') && + $this->request->query->get('page') > 1 + ) { + $this->setPage((int) $this->request->query->get('page')); + } else { + $this->setPage(1); + } + } else { + /* + * Disable pagination and paginator + */ + $this->disablePagination(); + } + $this->createPaginator(); if ( $this->allowRequestSearching && false === $disabled && - null !== $this->request + null !== $this->request && + $this->request->query->get('search') != "" ) { - $search = $this->request->query->get('search'); - if (\is_string($search) && $search !== "") { - $this->paginator->setSearchPattern($search); - } + $this->paginator->setSearchPattern($this->request->query->get('search')); } } - protected function handleOrderingParam(string $field, string $ordering): void - { - $this->validateOrderingFieldName($field); - $this->orderingArray = [ - $field => $ordering - ]; - } - - protected function createPaginator(): void { if ( @@ -183,6 +213,16 @@ protected function createPaginator(): void $this->paginator->setDisplayingAllNodesStatuses($this->isDisplayingAllNodesStatuses()); } + /** + * @return array + */ + public function getAssignation(): array + { + return array_merge(parent::getAssignation(), [ + 'search' => $this->searchPattern, + ]); + } + /** * @return int */ diff --git a/src/ListManager/NodePaginator.php b/src/ListManager/NodePaginator.php index 3f8813fa..0b3816d3 100644 --- a/src/ListManager/NodePaginator.php +++ b/src/ListManager/NodePaginator.php @@ -39,11 +39,11 @@ public function setTranslation(TranslationInterface $translation = null) * Return entities filtered for current page. * * @param array $order - * @param int $page + * @param integer $page * - * @return array|\Doctrine\ORM\Tools\Pagination\Paginator + * @return array */ - public function findByAtPage(array $order = [], int $page = 1) + public function findByAtPage(array $order = [], $page = 1) { if (null !== $this->searchPattern) { return $this->searchByAtPage($order, $page); diff --git a/src/ListManager/NodesSourcesPaginator.php b/src/ListManager/NodesSourcesPaginator.php index c6c746ff..288d0611 100644 --- a/src/ListManager/NodesSourcesPaginator.php +++ b/src/ListManager/NodesSourcesPaginator.php @@ -33,7 +33,7 @@ public function getTotalCount(): int * @param array $order * @param integer $page * - * @return array|\Doctrine\ORM\Tools\Pagination\Paginator + * @return array */ public function findByAtPage(array $order = [], int $page = 1) { diff --git a/src/ListManager/QueryBuilderListManager.php b/src/ListManager/QueryBuilderListManager.php index 98f94230..d220f474 100644 --- a/src/ListManager/QueryBuilderListManager.php +++ b/src/ListManager/QueryBuilderListManager.php @@ -14,10 +14,6 @@ class QueryBuilderListManager extends AbstractEntityListManager protected ?Paginator $paginator = null; protected string $identifier; protected bool $debug = false; - /** - * @var null|callable - */ - protected $searchingCallable = null; /** * @param Request|null $request @@ -37,40 +33,57 @@ public function __construct( $this->debug = $debug; } - /** - * @param callable|null $searchingCallable - * @return QueryBuilderListManager - */ - public function setSearchingCallable(?callable $searchingCallable): QueryBuilderListManager - { - $this->searchingCallable = $searchingCallable; - return $this; - } - /** * @param string $search */ protected function handleSearchParam(string $search): void { - parent::handleSearchParam($search); - - if (\is_callable($this->searchingCallable)) { - \call_user_func($this->searchingCallable, $this->queryBuilder, $search); - } + // Implement your custom logic } public function handle(bool $disabled = false) { - $this->handleRequestQuery($disabled); - } - - protected function handleOrderingParam(string $field, string $ordering): void - { - $this->validateOrderingFieldName($field); - $this->queryBuilder->addOrderBy( - sprintf('%s.%s', $this->identifier, $field), - $ordering - ); + if (false === $disabled && null !== $this->request) { + if ( + $this->allowRequestSorting && + $this->request->query->get('field') && + $this->request->query->get('ordering') + ) { + $this->validateOrderingFieldName($this->request->query->get('field')); + $this->queryBuilder->addOrderBy( + sprintf('%s.%s', $this->identifier, $this->request->query->get('field')), + $this->request->query->get('ordering') + ); + $this->queryArray['field'] = $this->request->query->get('field'); + $this->queryArray['ordering'] = $this->request->query->get('ordering'); + } + + if ($this->allowRequestSearching && $this->request->query->get('search') != "") { + $this->handleSearchParam($this->request->query->get('search')); + $this->queryArray['search'] = $this->request->query->get('search'); + } + + if ( + $this->request->query->has('item_per_page') && + $this->request->query->get('item_per_page') > 0 + ) { + $this->setItemPerPage((int) $this->request->query->get('item_per_page')); + } + + if ( + $this->request->query->has('page') && + $this->request->query->get('page') > 1 + ) { + $this->setPage((int) $this->request->query->get('page')); + } else { + $this->setPage(1); + } + } else { + /* + * Disable pagination and paginator + */ + $this->disablePagination(); + } } /** diff --git a/src/ListManager/SessionListFilters.php b/src/ListManager/SessionListFilters.php deleted file mode 100644 index 4e8dc5f7..00000000 --- a/src/ListManager/SessionListFilters.php +++ /dev/null @@ -1,53 +0,0 @@ -hasSession() && - $request->getSession()->has($this->sessionIdentifier) && - $request->getSession()->get($this->sessionIdentifier) > 0 && - (!$request->query->has('item_per_page') || - $request->query->get('item_per_page') < 1) - ) { - /* - * Item count is in session - */ - $request->query->set('item_per_page', intval($request->getSession()->get($this->sessionIdentifier))); - $listManager->setItemPerPage(intval($request->getSession()->get($this->sessionIdentifier))); - } elseif ( - $request->query->has('item_per_page') && - $request->query->get('item_per_page') > 0 - ) { - /* - * Item count is in query, save it in session - */ - $request->getSession()->set($this->sessionIdentifier, intval($request->query->get('item_per_page'))); - $listManager->setItemPerPage(intval($request->query->get('item_per_page'))); - } else { - $listManager->setItemPerPage($this->defaultItemsParPage); - } - } -} diff --git a/src/Logger/DoctrineHandler.php b/src/Logger/DoctrineHandler.php index bcc9e42c..330dea02 100644 --- a/src/Logger/DoctrineHandler.php +++ b/src/Logger/DoctrineHandler.php @@ -7,14 +7,9 @@ use Doctrine\Persistence\ManagerRegistry; use Monolog\Handler\AbstractProcessingHandler; use Monolog\Logger; -use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; -use RZ\Roadiz\CoreBundle\Entity\Node; +use RZ\Roadiz\CoreBundle\Entity\Log; use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\User; -use RZ\Roadiz\CoreBundle\Logger\Entity\Log; -use RZ\Roadiz\Documents\Models\DocumentInterface; -use RZ\Roadiz\Documents\UrlGenerators\DocumentUrlGeneratorInterface; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\User\UserInterface; @@ -24,98 +19,40 @@ */ final class DoctrineHandler extends AbstractProcessingHandler { - private ManagerRegistry $managerRegistry; - private TokenStorageInterface $tokenStorage; - private RequestStack $requestStack; - private DocumentUrlGeneratorInterface $documentUrlGenerator; + protected ManagerRegistry $managerRegistry; + protected TokenStorageInterface $tokenStorage; + protected RequestStack $requestStack; public function __construct( ManagerRegistry $managerRegistry, TokenStorageInterface $tokenStorage, RequestStack $requestStack, - DocumentUrlGeneratorInterface $documentUrlGenerator, $level = Logger::INFO, $bubble = true ) { - parent::__construct($level, $bubble); $this->tokenStorage = $tokenStorage; $this->requestStack = $requestStack; $this->managerRegistry = $managerRegistry; - $this->documentUrlGenerator = $documentUrlGenerator; - } - protected function getThumbnailSourcePath(?DocumentInterface $thumbnail): ?string - { - if (null === $thumbnail || $thumbnail->isPrivate()) { - return null; - } - return $this->documentUrlGenerator - ->setDocument($thumbnail) - ->setOptions([ - "fit" => "150x150", - "quality" => 70, - ]) - ->getUrl(); + parent::__construct($level, $bubble); } - protected function populateForNode(Node $value, Log $log, array &$data): void + /** + * @return TokenStorageInterface + */ + public function getTokenStorage(): TokenStorageInterface { - $log->setEntityClass(Node::class); - $log->setEntityId($value->getId()); - $data = array_merge( - $data, - [ - 'node_id' => $value->getId(), - 'entity_title' => $value->getNodeName(), - ] - ); - $nodeSource = $value->getNodeSources()->first() ?: null; - if (null !== $nodeSource) { - $data = array_merge( - $data, - [ - 'node_source_id' => $nodeSource->getId(), - 'translation_id' => $nodeSource->getTranslation()->getId(), - 'entity_title' => $nodeSource->getTitle() ?? $value->getNodeName(), - ] - ); - } - - $thumbnailSrc = $this->getThumbnailSourcePath($nodeSource?->getOneDisplayableDocument()); - if (null !== $thumbnailSrc) { - $data = array_merge( - $data, - [ - 'entity_thumbnail_src' => $thumbnailSrc, - ] - ); - } + return $this->tokenStorage; } - - protected function populateForNodesSources(NodesSources $value, Log $log, array &$data): void + /** + * @param TokenStorageInterface $tokenStorage + * + * @return $this + */ + public function setTokenStorage(TokenStorageInterface $tokenStorage): DoctrineHandler { - $log->setEntityClass(NodesSources::class); - $log->setEntityId($value->getId()); - $data = array_merge( - $data, - [ - 'node_source_id' => $value->getId(), - 'node_id' => $value->getNode()->getId(), - 'translation_id' => $value->getTranslation()->getId(), - 'entity_title' => $value->getTitle(), - ] - ); - - $thumbnail = $value->getOneDisplayableDocument(); - $thumbnailSrc = $this->getThumbnailSourcePath($thumbnail); - if (null !== $thumbnailSrc) { - $data = array_merge( - $data, - [ - 'entity_thumbnail_src' => $thumbnailSrc, - ] - ); - } + $this->tokenStorage = $tokenStorage; + return $this; } /** @@ -136,89 +73,44 @@ public function write(array $record): void $log->setChannel((string) $record['channel']); $data = $record['extra']; - $context = $record['context']; - - if (\is_array($context)) { - foreach ($context as $key => $value) { - if ($value instanceof Node) { - $this->populateForNode($value, $log, $data); - } elseif ($value instanceof NodesSources) { - $this->populateForNodesSources($value, $log, $data); - } elseif ($key === 'entity' && $value instanceof PersistableInterface) { - $log->setEntityClass(get_class($value)); - $log->setEntityId($value->getId()); - - $texteable = ['getTitle', 'getName', '__toString']; - foreach ($texteable as $method) { - if (method_exists($value, $method)) { - $data = array_merge( - $data, - [ - 'entity_title' => $value->{$method}() - ] - ); - break; - } - } - } - if ($value instanceof \Exception) { - $data = array_merge( - $data, - [ - 'exception_class' => get_class($value), - 'message' => $value->getMessage() - ] - ); - } - if ($value instanceof Request) { - $data = array_merge( - $data, - [ - 'uri' => $value->getUri(), - 'schemeHost' => $value->getSchemeAndHttpHost(), - ] - ); - } - if ($key === 'request' && \is_array($value)) { - $data = array_merge( - $data, - $value - ); - } - if (\is_string($value) && !empty($value) && !\is_numeric($key)) { - $data = array_merge( - $data, - [$key => $value] - ); - } - if (\is_string($value) && !empty($value) && \in_array($key, ['user', 'username'])) { - $log->setUsername($value); - } - } + if (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Exception) { + $data = array_merge( + $data, + [ + get_class($record['context']['exception']) => $record['context']['exception']->getMessage() + ] + ); + } + if (isset($record['context']['request'])) { + $data = array_merge( + $data, + $record['context']['request'] + ); } + if (isset($record['context']['username'])) { + $data = array_merge( + $data, + ['username' => $record['context']['username']] + ); + } + $log->setAdditionalData($data); /* * Use available securityAuthorizationChecker to provide a valid user */ - if (null !== $token = $this->tokenStorage->getToken()) { + if ( + null !== $this->getTokenStorage() && + null !== $token = $this->getTokenStorage()->getToken() + ) { $user = $token->getUser(); if ($user instanceof UserInterface) { if ($user instanceof User) { $log->setUser($user); - $data = array_merge( - $data, - [ - 'user_email' => $user->getEmail(), - 'user_public_name' => $user->getPublicName(), - 'user_picture_url' => $user->getPictureUrl(), - 'user_id' => $user->getId() - ] - ); } else { - $log->setUsername($user->getUserIdentifier()); + $log->setUsername($user->getUsername()); } } else { - $log->setUsername($token->getUserIdentifier()); + $log->setUsername($token->getUsername()); } } @@ -229,7 +121,15 @@ public function write(array $record): void $log->setClientIp($this->requestStack->getMainRequest()->getClientIp()); } - $log->setAdditionalData($data); + /* + * Add a related node-source entity + */ + if ( + isset($record['context']['source']) && + $record['context']['source'] instanceof NodesSources + ) { + $log->setNodeSource($record['context']['source']); + } $manager->persist($log); $manager->flush(); diff --git a/src/Mailer/ContactFormManager.php b/src/Mailer/ContactFormManager.php index 8fa3df41..1c340f40 100644 --- a/src/Mailer/ContactFormManager.php +++ b/src/Mailer/ContactFormManager.php @@ -56,6 +56,10 @@ class ContactFormManager extends EmailManager 'image/gif', ]; protected int $maxFileSize = 5242880; // 5MB + protected FormFactoryInterface $formFactory; + protected FormErrorSerializerInterface $formErrorSerializer; + protected ?string $recaptchaPrivateKey; + protected ?string $recaptchaPublicKey; /* * DO NOT DIRECTLY USE THIS CONSTRUCTOR @@ -63,18 +67,20 @@ class ContactFormManager extends EmailManager */ public function __construct( RequestStack $requestStack, + FormFactoryInterface $formFactory, TranslatorInterface $translator, Environment $templating, MailerInterface $mailer, Settings $settingsBag, DocumentUrlGeneratorInterface $documentUrlGenerator, - protected readonly FormFactoryInterface $formFactory, - protected readonly FormErrorSerializerInterface $formErrorSerializer, - protected readonly ?string $recaptchaPrivateKey, - protected readonly ?string $recaptchaPublicKey + FormErrorSerializerInterface $formErrorSerializer, + ?string $recaptchaPrivateKey, + ?string $recaptchaPublicKey ) { parent::__construct($requestStack, $translator, $templating, $mailer, $settingsBag, $documentUrlGenerator); + $this->formFactory = $formFactory; + $this->formErrorSerializer = $formErrorSerializer; $this->options = [ 'attr' => [ 'id' => 'contactForm', @@ -95,6 +101,8 @@ public function __construct( 'new.contact.form.%site%', ['%site%' => $this->settingsBag->get('site_name')] )); + $this->recaptchaPrivateKey = $recaptchaPrivateKey; + $this->recaptchaPublicKey = $recaptchaPublicKey; } /** @@ -122,7 +130,7 @@ public function setFormName(string $formName): ContactFormManager * * @return $this */ - public function disableCsrfProtection(): self + public function disableCsrfProtection() { $this->options['csrf_protection'] = false; return $this; @@ -145,7 +153,7 @@ public function getForm(): FormInterface * @see https://symfony.com/doc/4.4/reference/constraints/Email.html#strict * @return $this */ - public function setEmailStrictMode(bool $emailStrictMode = true): self + public function setEmailStrictMode(bool $emailStrictMode = true) { $this->emailStrictMode = $emailStrictMode; @@ -163,9 +171,9 @@ public function isEmailStrictMode(): bool * Adds email, name and message fields with their constraints. * * @param bool $useHoneypot - * @return $this + * @return ContactFormManager $this */ - public function withDefaultFields(bool $useHoneypot = true): self + public function withDefaultFields(bool $useHoneypot = true) { $this->getFormBuilder()->add('email', EmailType::class, [ 'label' => 'your.email', @@ -176,7 +184,7 @@ public function withDefaultFields(bool $useHoneypot = true): self 'message' => 'email.not.valid', 'mode' => $this->isEmailStrictMode() ? Email::VALIDATION_MODE_STRICT : - Email::VALIDATION_MODE_HTML5 + Email::VALIDATION_MODE_LOOSE ]), ], ]) @@ -209,7 +217,7 @@ public function withDefaultFields(bool $useHoneypot = true): self * @param string $honeypotName * @return $this */ - public function withHoneypot(string $honeypotName = 'eml'): self + public function withHoneypot(string $honeypotName = 'eml') { $this->getFormBuilder()->add($honeypotName, HoneypotType::class); return $this; @@ -221,7 +229,7 @@ public function withHoneypot(string $honeypotName = 'eml'): self * @param string $consentDescription * @return $this */ - public function withUserConsent(string $consentDescription = 'contact_form.user_consent'): self + public function withUserConsent(string $consentDescription = 'contact_form.user_consent') { $this->getFormBuilder()->add('consent', CheckboxType::class, [ 'label' => $consentDescription, @@ -268,7 +276,7 @@ public function getFormBuilder(): FormBuilderInterface public function withGoogleRecaptcha( string $name = 'recaptcha', string $validatorFieldName = Recaptcha::FORM_NAME - ): self { + ) { if ( !empty($this->recaptchaPublicKey) && !empty($this->recaptchaPrivateKey) @@ -626,7 +634,7 @@ public function getRedirectUrl(): ?string * * @return self */ - public function setRedirectUrl(?string $redirectUrl): self + public function setRedirectUrl(?string $redirectUrl) { $this->redirectUrl = $redirectUrl; @@ -638,7 +646,7 @@ public function setRedirectUrl(?string $redirectUrl): self * * @return int */ - public function getMaxFileSize(): int + public function getMaxFileSize() { return $this->maxFileSize; } @@ -650,7 +658,7 @@ public function getMaxFileSize(): int * * @return self */ - public function setMaxFileSize($maxFileSize): self + public function setMaxFileSize($maxFileSize) { $this->maxFileSize = (int) $maxFileSize; @@ -662,7 +670,7 @@ public function setMaxFileSize($maxFileSize): self * * @return array */ - public function getAllowedMimeTypes(): array + public function getAllowedMimeTypes() { return $this->allowedMimeTypes; } @@ -674,7 +682,7 @@ public function getAllowedMimeTypes(): array * * @return self */ - public function setAllowedMimeTypes(array $allowedMimeTypes): self + public function setAllowedMimeTypes(array $allowedMimeTypes) { $this->allowedMimeTypes = $allowedMimeTypes; @@ -684,7 +692,7 @@ public function setAllowedMimeTypes(array $allowedMimeTypes): self /** * @return array */ - public function getOptions(): array + public function getOptions() { return $this->options; } @@ -692,9 +700,9 @@ public function getOptions(): array /** * @param array $options * - * @return $this + * @return ContactFormManager */ - public function setOptions(array $options): self + public function setOptions($options) { $this->options = $options; diff --git a/src/Mailer/EmailManager.php b/src/Mailer/EmailManager.php index 73890767..346ebf60 100644 --- a/src/Mailer/EmailManager.php +++ b/src/Mailer/EmailManager.php @@ -33,30 +33,43 @@ class EmailManager protected ?Address $origin = null; protected string $successMessage = 'email.successfully.sent'; protected string $failMessage = 'email.has.errors'; + protected TranslatorInterface $translator; + protected Environment $templating; + protected MailerInterface $mailer; protected ?string $emailTemplate = null; protected ?string $emailPlainTextTemplate = null; protected string $emailStylesheet; + protected RequestStack $requestStack; protected array $assignation; protected ?Email $message; + protected ?Settings $settingsBag; + protected ?DocumentUrlGeneratorInterface $documentUrlGenerator; /** @var File[] */ protected array $files = []; /** @var array */ protected array $resources = []; public function __construct( - protected readonly RequestStack $requestStack, - protected readonly TranslatorInterface $translator, - protected readonly Environment $templating, - protected readonly MailerInterface $mailer, - protected readonly Settings $settingsBag, - protected readonly DocumentUrlGeneratorInterface $documentUrlGenerator + RequestStack $requestStack, + TranslatorInterface $translator, + Environment $templating, + MailerInterface $mailer, + ?Settings $settingsBag = null, + ?DocumentUrlGeneratorInterface $documentUrlGenerator = null ) { + $this->requestStack = $requestStack; + $this->translator = $translator; + $this->mailer = $mailer; + $this->templating = $templating; $this->assignation = []; $this->message = null; + /* * Sets a default CSS for emails. */ $this->emailStylesheet = dirname(__DIR__) . '/../css/transactionalStyles.css'; + $this->settingsBag = $settingsBag; + $this->documentUrlGenerator = $documentUrlGenerator; } /** @@ -80,13 +93,9 @@ public function renderHtmlEmailBodyWithCss(): string { if (null !== $this->getEmailStylesheet()) { $htmldoc = new InlineStyle($this->renderHtmlEmailBody()); - $css = file_get_contents( + $htmldoc->applyStylesheet(file_get_contents( $this->getEmailStylesheet() - ); - if (false === $css) { - throw new \RuntimeException('Unable to read email stylesheet file.'); - } - $htmldoc->applyStylesheet($css); + )); return $htmldoc->getHTML(); } @@ -265,7 +274,7 @@ public function getReceiverEmail(): ?string * @return $this * @throws \Exception */ - public function setReceiver(mixed $receiver): static + public function setReceiver($receiver): static { if ($receiver instanceof Address) { $this->receiver = [$receiver]; @@ -320,7 +329,7 @@ public function getSenderEmail(): ?string * @return $this * @throws \Exception */ - public function setSender(mixed $sender): static + public function setSender($sender): static { if ($sender instanceof Address) { $this->sender = [$sender]; @@ -388,6 +397,16 @@ public function getTranslator(): TranslatorInterface return $this->translator; } + /** + * @param TranslatorInterface $translator + * @return $this + */ + public function setTranslator(TranslatorInterface $translator): static + { + $this->translator = $translator; + return $this; + } + /** * @return Environment */ @@ -396,6 +415,16 @@ public function getTemplating(): Environment return $this->templating; } + /** + * @param Environment $templating + * @return $this + */ + public function setTemplating(Environment $templating): static + { + $this->templating = $templating; + return $this; + } + /** * @return MailerInterface */ @@ -404,6 +433,16 @@ public function getMailer(): MailerInterface return $this->mailer; } + /** + * @param MailerInterface $mailer + * @return $this + */ + public function setMailer(MailerInterface $mailer): static + { + $this->mailer = $mailer; + return $this; + } + /** * @return string|null */ diff --git a/src/Message/ApplyRealmNodeInheritanceMessage.php b/src/Message/ApplyRealmNodeInheritanceMessage.php index 7943f8a8..7159e176 100644 --- a/src/Message/ApplyRealmNodeInheritanceMessage.php +++ b/src/Message/ApplyRealmNodeInheritanceMessage.php @@ -6,27 +6,27 @@ final class ApplyRealmNodeInheritanceMessage implements AsyncMessage { - private int|string|null $nodeId; - private int|string|null $realmId; + private int $nodeId; + private ?int $realmId; - public function __construct(int|string|null $nodeId, int|string|null $realmId) + public function __construct(int $nodeId, ?int $realmId) { $this->nodeId = $nodeId; $this->realmId = $realmId; } /** - * @return int|string|null + * @return int */ - public function getNodeId(): int|string|null + public function getNodeId(): int { return $this->nodeId; } /** - * @return int|string|null + * @return int|null */ - public function getRealmId(): int|string|null + public function getRealmId(): ?int { return $this->realmId; } diff --git a/src/Message/CleanRealmNodeInheritanceMessage.php b/src/Message/CleanRealmNodeInheritanceMessage.php index cf1d1201..05579c30 100644 --- a/src/Message/CleanRealmNodeInheritanceMessage.php +++ b/src/Message/CleanRealmNodeInheritanceMessage.php @@ -6,27 +6,27 @@ final class CleanRealmNodeInheritanceMessage implements AsyncMessage { - private int|string|null $nodeId; - private int|string|null $realmId; + private int $nodeId; + private ?int $realmId; - public function __construct(int|string|null $nodeId, int|string|null $realmId) + public function __construct(int $nodeId, ?int $realmId) { $this->nodeId = $nodeId; $this->realmId = $realmId; } /** - * @return int|string|null + * @return int */ - public function getNodeId(): int|string|null + public function getNodeId(): int { return $this->nodeId; } /** - * @return int|string|null + * @return int|null */ - public function getRealmId(): int|string|null + public function getRealmId(): ?int { return $this->realmId; } diff --git a/src/Message/DeleteNodeTypeMessage.php b/src/Message/DeleteNodeTypeMessage.php index 0f3caca1..a5f3c682 100644 --- a/src/Message/DeleteNodeTypeMessage.php +++ b/src/Message/DeleteNodeTypeMessage.php @@ -6,20 +6,20 @@ final class DeleteNodeTypeMessage implements AsyncMessage { - private int|string|null $nodeTypeId; + private int $nodeTypeId; /** - * @param int|string|null $nodeTypeId + * @param int $nodeTypeId */ - public function __construct(int|string|null $nodeTypeId) + public function __construct(int $nodeTypeId) { $this->nodeTypeId = $nodeTypeId; } /** - * @return int|string|null + * @return int */ - public function getNodeTypeId(): int|string|null + public function getNodeTypeId(): int { return $this->nodeTypeId; } diff --git a/src/Message/Handler/ApplyRealmNodeInheritanceMessageHandler.php b/src/Message/Handler/ApplyRealmNodeInheritanceMessageHandler.php index bae2c618..785eb73d 100644 --- a/src/Message/Handler/ApplyRealmNodeInheritanceMessageHandler.php +++ b/src/Message/Handler/ApplyRealmNodeInheritanceMessageHandler.php @@ -5,21 +5,25 @@ namespace RZ\Roadiz\CoreBundle\Message\Handler; use Doctrine\Persistence\ManagerRegistry; +use RZ\Roadiz\Core\Handlers\HandlerFactoryInterface; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\Realm; use RZ\Roadiz\CoreBundle\Entity\RealmNode; +use RZ\Roadiz\CoreBundle\EntityHandler\NodeHandler; use RZ\Roadiz\CoreBundle\Message\ApplyRealmNodeInheritanceMessage; use RZ\Roadiz\CoreBundle\Model\RealmInterface; -use RZ\Roadiz\CoreBundle\Node\NodeOffspringResolverInterface; use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; final class ApplyRealmNodeInheritanceMessageHandler implements MessageHandlerInterface { - public function __construct( - private readonly ManagerRegistry $managerRegistry, - private readonly NodeOffspringResolverInterface $nodeOffspringResolver - ) { + private ManagerRegistry $managerRegistry; + private HandlerFactoryInterface $handlerFactory; + + public function __construct(ManagerRegistry $managerRegistry, HandlerFactoryInterface $handlerFactory) + { + $this->managerRegistry = $managerRegistry; + $this->handlerFactory = $handlerFactory; } public function __invoke(ApplyRealmNodeInheritanceMessage $message): void @@ -49,12 +53,15 @@ public function __invoke(ApplyRealmNodeInheritanceMessage $message): void return; } - $nodeRepository = $this->managerRegistry->getRepository(Node::class); - $childrenIds = $this->nodeOffspringResolver->getAllOffspringIds($node); + /** @var NodeHandler $nodeHandler */ + $nodeHandler = $this->handlerFactory->getHandler($node); + $childrenIds = $nodeHandler->getAllOffspringId(); foreach ($childrenIds as $childId) { /** @var Node|null $child */ - $child = $nodeRepository->find($childId); + $child = $this->managerRegistry + ->getRepository(Node::class) + ->find($childId); if (null === $child) { continue; } diff --git a/src/Message/Handler/CleanRealmNodeInheritanceMessageHandler.php b/src/Message/Handler/CleanRealmNodeInheritanceMessageHandler.php index 166d52e7..b9d01fd6 100644 --- a/src/Message/Handler/CleanRealmNodeInheritanceMessageHandler.php +++ b/src/Message/Handler/CleanRealmNodeInheritanceMessageHandler.php @@ -5,20 +5,24 @@ namespace RZ\Roadiz\CoreBundle\Message\Handler; use Doctrine\Persistence\ManagerRegistry; +use RZ\Roadiz\Core\Handlers\HandlerFactoryInterface; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\Realm; use RZ\Roadiz\CoreBundle\Entity\RealmNode; +use RZ\Roadiz\CoreBundle\EntityHandler\NodeHandler; use RZ\Roadiz\CoreBundle\Message\CleanRealmNodeInheritanceMessage; -use RZ\Roadiz\CoreBundle\Node\NodeOffspringResolverInterface; use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; final class CleanRealmNodeInheritanceMessageHandler implements MessageHandlerInterface { - public function __construct( - private readonly ManagerRegistry $managerRegistry, - private readonly NodeOffspringResolverInterface $nodeOffspringResolver - ) { + private ManagerRegistry $managerRegistry; + private HandlerFactoryInterface $handlerFactory; + + public function __construct(ManagerRegistry $managerRegistry, HandlerFactoryInterface $handlerFactory) + { + $this->managerRegistry = $managerRegistry; + $this->handlerFactory = $handlerFactory; } public function __invoke(CleanRealmNodeInheritanceMessage $message): void @@ -36,7 +40,9 @@ public function __invoke(CleanRealmNodeInheritanceMessage $message): void throw new UnrecoverableMessageHandlingException('Realm does not exist'); } - $childrenIds = $this->nodeOffspringResolver->getAllOffspringIds($node); + /** @var NodeHandler $nodeHandler */ + $nodeHandler = $this->handlerFactory->getHandler($node); + $childrenIds = $nodeHandler->getAllOffspringId(); $realmNodes = $this->managerRegistry ->getRepository(RealmNode::class) diff --git a/src/Message/Handler/DeleteNodeTypeMessageHandler.php b/src/Message/Handler/DeleteNodeTypeMessageHandler.php index 9a9c4723..6159b918 100644 --- a/src/Message/Handler/DeleteNodeTypeMessageHandler.php +++ b/src/Message/Handler/DeleteNodeTypeMessageHandler.php @@ -19,11 +19,15 @@ final class DeleteNodeTypeMessageHandler implements MessageHandlerInterface { - public function __construct( - private readonly ManagerRegistry $managerRegistry, - private readonly HandlerFactoryInterface $handlerFactory, - private readonly MessageBusInterface $messageBus - ) { + private ManagerRegistry $managerRegistry; + private HandlerFactoryInterface $handlerFactory; + private MessageBusInterface $messageBus; + + public function __construct(ManagerRegistry $managerRegistry, HandlerFactoryInterface $handlerFactory, MessageBusInterface $messageBus) + { + $this->managerRegistry = $managerRegistry; + $this->handlerFactory = $handlerFactory; + $this->messageBus = $messageBus; } /** diff --git a/src/Message/Handler/HttpRequestMessageHandler.php b/src/Message/Handler/HttpRequestMessageHandler.php index 35f275e5..b7848ed5 100644 --- a/src/Message/Handler/HttpRequestMessageHandler.php +++ b/src/Message/Handler/HttpRequestMessageHandler.php @@ -7,15 +7,24 @@ use GuzzleHttp\Client; use GuzzleHttp\Exception\GuzzleException; use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; use RZ\Roadiz\CoreBundle\Message\HttpRequestMessage; use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; final class HttpRequestMessageHandler implements MessageHandlerInterface { - public function __construct( - private readonly LoggerInterface $logger - ) { + private LoggerInterface $logger; + private ?Client $client; + + /** + * @param Client|null $client + * @param LoggerInterface|null $logger + */ + public function __construct(Client $client = null, ?LoggerInterface $logger = null) + { + $this->logger = $logger ?? new NullLogger(); + $this->client = $client ?? new Client(); } public function __invoke(HttpRequestMessage $message): void @@ -26,8 +35,7 @@ public function __invoke(HttpRequestMessage $message): void $message->getRequest()->getMethod(), $message->getRequest()->getUri() )); - $client = new Client(); - $client->send($message->getRequest(), $message->getOptions()); + $this->client->send($message->getRequest(), $message->getOptions()); } catch (GuzzleException $exception) { throw new UnrecoverableMessageHandlingException($exception->getMessage(), 0, $exception); } diff --git a/src/Message/Handler/PurgeReverseProxyCacheMessageHandler.php b/src/Message/Handler/PurgeReverseProxyCacheMessageHandler.php index 13da36f7..6082040d 100644 --- a/src/Message/Handler/PurgeReverseProxyCacheMessageHandler.php +++ b/src/Message/Handler/PurgeReverseProxyCacheMessageHandler.php @@ -20,12 +20,27 @@ final class PurgeReverseProxyCacheMessageHandler implements MessageHandlerInterface { + private UrlGeneratorInterface $urlGenerator; + private ReverseProxyCacheLocator $reverseProxyCacheLocator; + private MessageBusInterface $bus; + private ManagerRegistry $managerRegistry; + + /** + * @param MessageBusInterface $bus + * @param UrlGeneratorInterface $urlGenerator + * @param ReverseProxyCacheLocator $reverseProxyCacheLocator + * @param ManagerRegistry $managerRegistry + */ public function __construct( - private readonly MessageBusInterface $bus, - private readonly UrlGeneratorInterface $urlGenerator, - private readonly ReverseProxyCacheLocator $reverseProxyCacheLocator, - private readonly ManagerRegistry $managerRegistry + MessageBusInterface $bus, + UrlGeneratorInterface $urlGenerator, + ReverseProxyCacheLocator $reverseProxyCacheLocator, + ManagerRegistry $managerRegistry ) { + $this->urlGenerator = $urlGenerator; + $this->reverseProxyCacheLocator = $reverseProxyCacheLocator; + $this->managerRegistry = $managerRegistry; + $this->bus = $bus; } public function __invoke(PurgeReverseProxyCacheMessage $message): void diff --git a/src/Message/Handler/SearchRealmNodeInheritanceMessageHandler.php b/src/Message/Handler/SearchRealmNodeInheritanceMessageHandler.php index 277bf6a5..145bc5e8 100644 --- a/src/Message/Handler/SearchRealmNodeInheritanceMessageHandler.php +++ b/src/Message/Handler/SearchRealmNodeInheritanceMessageHandler.php @@ -21,12 +21,21 @@ final class SearchRealmNodeInheritanceMessageHandler implements MessageHandlerInterface { + private ManagerRegistry $managerRegistry; + private HandlerFactoryInterface $handlerFactory; + private MessageBusInterface $bus; + private LoggerInterface $logger; + public function __construct( - private readonly ManagerRegistry $managerRegistry, - private readonly HandlerFactoryInterface $handlerFactory, - private readonly MessageBusInterface $bus, - private readonly LoggerInterface $logger + ManagerRegistry $managerRegistry, + HandlerFactoryInterface $handlerFactory, + MessageBusInterface $bus, + LoggerInterface $logger ) { + $this->managerRegistry = $managerRegistry; + $this->handlerFactory = $handlerFactory; + $this->bus = $bus; + $this->logger = $logger; } public function __invoke(SearchRealmNodeInheritanceMessage $message): void @@ -56,7 +65,7 @@ private function clearAnyExistingRealmNodes(Node $node): void $this->logger->info('Clean existing RealmNode information'); $this->bus->dispatch(new Envelope(new CleanRealmNodeInheritanceMessage( $autoRealmNode->getNode()->getId(), - $autoRealmNode->getRealm()->getId() + null !== $autoRealmNode->getRealm() ? $autoRealmNode->getRealm()->getId() : null ))); } } @@ -81,7 +90,7 @@ private function applyRootRealmNodes(Node $node): void $this->logger->info('Apply new root RealmNode information'); $this->bus->dispatch(new Envelope(new ApplyRealmNodeInheritanceMessage( $rootRealmNode->getNode()->getId(), - $rootRealmNode->getRealm()->getId() + null !== $rootRealmNode->getRealm() ? $rootRealmNode->getRealm()->getId() : null ))); } } diff --git a/src/Message/Handler/UpdateDoctrineSchemaMessageHandler.php b/src/Message/Handler/UpdateDoctrineSchemaMessageHandler.php index e9fc413d..e9c384f5 100644 --- a/src/Message/Handler/UpdateDoctrineSchemaMessageHandler.php +++ b/src/Message/Handler/UpdateDoctrineSchemaMessageHandler.php @@ -10,8 +10,11 @@ final class UpdateDoctrineSchemaMessageHandler implements MessageHandlerInterface { - public function __construct(private readonly SchemaUpdater $schemaUpdater) + private SchemaUpdater $schemaUpdater; + + public function __construct(SchemaUpdater $schemaUpdater) { + $this->schemaUpdater = $schemaUpdater; } /** diff --git a/src/Message/Handler/UpdateNodeTypeSchemaMessageHandler.php b/src/Message/Handler/UpdateNodeTypeSchemaMessageHandler.php index a14b07d1..6a0185ec 100644 --- a/src/Message/Handler/UpdateNodeTypeSchemaMessageHandler.php +++ b/src/Message/Handler/UpdateNodeTypeSchemaMessageHandler.php @@ -17,11 +17,15 @@ final class UpdateNodeTypeSchemaMessageHandler implements MessageHandlerInterface { - public function __construct( - private readonly ManagerRegistry $managerRegistry, - private readonly HandlerFactoryInterface $handlerFactory, - private readonly MessageBusInterface $messageBus - ) { + private ManagerRegistry $managerRegistry; + private HandlerFactoryInterface $handlerFactory; + private MessageBusInterface $messageBus; + + public function __construct(ManagerRegistry $managerRegistry, HandlerFactoryInterface $handlerFactory, MessageBusInterface $messageBus) + { + $this->managerRegistry = $managerRegistry; + $this->handlerFactory = $handlerFactory; + $this->messageBus = $messageBus; } public function __invoke(UpdateNodeTypeSchemaMessage $message): void diff --git a/src/Message/PurgeReverseProxyCacheMessage.php b/src/Message/PurgeReverseProxyCacheMessage.php index be0c54c0..58e79384 100644 --- a/src/Message/PurgeReverseProxyCacheMessage.php +++ b/src/Message/PurgeReverseProxyCacheMessage.php @@ -6,20 +6,20 @@ final class PurgeReverseProxyCacheMessage implements AsyncMessage { - private int|string|null $nodeSourceId; + private int $nodeSourceId; /** - * @param int|string|null $nodeSourceId + * @param int $nodeSourceId */ - public function __construct(int|string|null $nodeSourceId) + public function __construct(int $nodeSourceId) { $this->nodeSourceId = $nodeSourceId; } /** - * @return int|string|null + * @return int */ - public function getNodeSourceId(): int|string|null + public function getNodeSourceId(): int { return $this->nodeSourceId; } diff --git a/src/Message/SearchRealmNodeInheritanceMessage.php b/src/Message/SearchRealmNodeInheritanceMessage.php index c9ec45ff..e3732026 100644 --- a/src/Message/SearchRealmNodeInheritanceMessage.php +++ b/src/Message/SearchRealmNodeInheritanceMessage.php @@ -6,17 +6,17 @@ final class SearchRealmNodeInheritanceMessage implements AsyncMessage { - private int|string|null $nodeId; + private int $nodeId; - public function __construct(int|string|null $nodeId) + public function __construct(int $nodeId) { $this->nodeId = $nodeId; } /** - * @return int|string|null + * @return int */ - public function getNodeId(): int|string|null + public function getNodeId(): int { return $this->nodeId; } diff --git a/src/Message/UpdateNodeTypeSchemaMessage.php b/src/Message/UpdateNodeTypeSchemaMessage.php index f4a309a5..830828b5 100644 --- a/src/Message/UpdateNodeTypeSchemaMessage.php +++ b/src/Message/UpdateNodeTypeSchemaMessage.php @@ -9,20 +9,20 @@ */ final class UpdateNodeTypeSchemaMessage { - private int|string|null $nodeTypeId; + private int $nodeTypeId; /** - * @param int|string|null $nodeTypeId + * @param int $nodeTypeId */ - public function __construct(int|string|null $nodeTypeId) + public function __construct(int $nodeTypeId) { $this->nodeTypeId = $nodeTypeId; } /** - * @return int|string|null + * @return int */ - public function getNodeTypeId(): int|string|null + public function getNodeTypeId(): int { return $this->nodeTypeId; } diff --git a/src/Model/AttributeGroupTrait.php b/src/Model/AttributeGroupTrait.php index 398d03d5..6348ad92 100644 --- a/src/Model/AttributeGroupTrait.php +++ b/src/Model/AttributeGroupTrait.php @@ -17,11 +17,10 @@ trait AttributeGroupTrait { #[ - ORM\Column(name: "canonical_name", type: "string", length: 255, unique: true, nullable: false), + ORM\Column(name: "canonical_name", type: "string", unique: true, nullable: false), Serializer\Groups(["attribute_group", "attribute", "node", "nodes_sources"]), Serializer\Type("string"), Assert\NotNull(), - Assert\Length(max: 255), Assert\NotBlank() ] protected string $canonicalName = ''; @@ -32,7 +31,7 @@ trait AttributeGroupTrait #[ ORM\OneToMany(mappedBy: "group", targetEntity: AttributeInterface::class), Serializer\Groups(["attribute_group"]), - Serializer\Type("ArrayCollection") + Serializer\Type("ArrayCollection") ] protected Collection $attributes; @@ -40,14 +39,9 @@ trait AttributeGroupTrait * @var Collection */ #[ - ORM\OneToMany( - mappedBy: "attributeGroup", - targetEntity: AttributeGroupTranslationInterface::class, - cascade: ["all"], - orphanRemoval: true - ), + ORM\OneToMany(mappedBy: "attributeGroup", targetEntity: AttributeGroupTranslationInterface::class, cascade: ["all"]), Serializer\Groups(["attribute_group", "attribute", "node", "nodes_sources"]), - Serializer\Type("ArrayCollection"), + Serializer\Type("ArrayCollection"), Serializer\Accessor(getter: "getAttributeGroupTranslations", setter: "setAttributeGroupTranslations") ] protected Collection $attributeGroupTranslations; diff --git a/src/Model/AttributeGroupTranslationTrait.php b/src/Model/AttributeGroupTranslationTrait.php index 34ec8324..241fdb31 100644 --- a/src/Model/AttributeGroupTranslationTrait.php +++ b/src/Model/AttributeGroupTranslationTrait.php @@ -7,33 +7,31 @@ use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; -use Symfony\Component\Validator\Constraints as Assert; trait AttributeGroupTranslationTrait { #[ ORM\ManyToOne(targetEntity: "RZ\Roadiz\Core\AbstractEntities\TranslationInterface"), - ORM\JoinColumn(name: "translation_id", referencedColumnName: "id", nullable: false, onDelete: "CASCADE"), + ORM\JoinColumn(name: "translation_id", onDelete: "CASCADE"), Serializer\Groups(["attribute_group", "attribute", "node", "nodes_sources"]), Serializer\Type("RZ\Roadiz\Core\AbstractEntities\TranslationInterface"), Serializer\Accessor(getter: "getTranslation", setter: "setTranslation") ] - protected TranslationInterface $translation; + protected ?TranslationInterface $translation = null; #[ - ORM\Column(type: "string", length: 255, unique: false, nullable: false), + ORM\Column(type: "string", unique: false, nullable: false), Serializer\Groups(["attribute_group", "attribute", "node", "nodes_sources"]), - Serializer\Type("string"), - Assert\Length(max: 255) + Serializer\Type("string") ] protected string $name = ''; #[ ORM\ManyToOne(targetEntity: AttributeGroupInterface::class, cascade: ["persist"], inversedBy: "attributeGroupTranslations"), - ORM\JoinColumn(name: "attribute_group_id", referencedColumnName: "id", nullable: false, onDelete: "CASCADE"), + ORM\JoinColumn(name: "attribute_group_id", referencedColumnName: "id", nullable: true, onDelete: "CASCADE"), Serializer\Exclude ] - protected AttributeGroupInterface $attributeGroup; + protected ?AttributeGroupInterface $attributeGroup = null; /** * @return string @@ -45,6 +43,7 @@ public function getName(): string /** * @param string $value + * * @return self */ public function setName(string $value) @@ -55,7 +54,8 @@ public function setName(string $value) /** * @param TranslationInterface $translation - * @return self + * + * @return mixed */ public function setTranslation(TranslationInterface $translation) { @@ -63,7 +63,10 @@ public function setTranslation(TranslationInterface $translation) return $this; } - public function getTranslation(): TranslationInterface + /** + * @return TranslationInterface|null + */ + public function getTranslation(): ?TranslationInterface { return $this->translation; } @@ -78,7 +81,8 @@ public function getAttributeGroup(): AttributeGroupInterface /** * @param AttributeGroupInterface $attributeGroup - * @return self + * + * @return mixed */ public function setAttributeGroup(AttributeGroupInterface $attributeGroup) { diff --git a/src/Model/AttributeInterface.php b/src/Model/AttributeInterface.php index 1da81a62..f22e2ec2 100644 --- a/src/Model/AttributeInterface.php +++ b/src/Model/AttributeInterface.php @@ -129,8 +129,6 @@ public function getType(): int; */ public function getColor(): ?string; - public function getWeight(): int; - /** * @param string|null $color */ @@ -217,7 +215,4 @@ public function isEnum(): bool; * @return bool */ public function isCountry(): bool; - - public function getDefaultRealm(): ?RealmInterface; - public function setDefaultRealm(?RealmInterface $defaultRealm): self; } diff --git a/src/Model/AttributeTrait.php b/src/Model/AttributeTrait.php index bb52d5e3..64cc7174 100644 --- a/src/Model/AttributeTrait.php +++ b/src/Model/AttributeTrait.php @@ -6,8 +6,6 @@ use Doctrine\Common\Collections\Collection; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; -use RZ\Roadiz\CoreBundle\Entity\AttributeGroup; -use RZ\Roadiz\CoreBundle\Entity\AttributeTranslation; use RZ\Roadiz\Utils\StringHandler; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; @@ -17,13 +15,12 @@ trait AttributeTrait { #[ - ORM\Column(type: "string", length: 255, unique: true, nullable: false), + ORM\Column(type: "string", unique: true, nullable: false), Serializer\Groups(["attribute", "node", "nodes_sources"]), SymfonySerializer\Groups(["attribute", "node", "nodes_sources"]), Serializer\Type("string"), Assert\NotNull(), - Assert\NotBlank(), - Assert\Length(max: 255) + Assert\NotBlank() ] protected string $code = ''; @@ -47,8 +44,7 @@ trait AttributeTrait ORM\Column(type: "string", length: 7, unique: false, nullable: true), Serializer\Groups(["attribute", "node", "nodes_sources"]), SymfonySerializer\Groups(["attribute", "node", "nodes_sources"]), - Serializer\Type("string"), - Assert\Length(max: 7) + Serializer\Type("string") ] protected ?string $color = null; @@ -62,12 +58,12 @@ trait AttributeTrait ORM\JoinColumn(name: "group_id", onDelete: "SET NULL"), Serializer\Groups(["attribute", "node", "nodes_sources"]), SymfonySerializer\Groups(["attribute", "node", "nodes_sources"]), - Serializer\Type(AttributeGroup::class) + Serializer\Type("RZ\Roadiz\CoreBundle\Model\AttributeGroupInterface") ] protected ?AttributeGroupInterface $group = null; /** - * @var Collection + * @var Collection */ #[ ORM\OneToMany( @@ -79,7 +75,7 @@ trait AttributeTrait ), Serializer\Groups(["attribute", "node", "nodes_sources"]), SymfonySerializer\Groups(["attribute", "node", "nodes_sources"]), - Serializer\Type("ArrayCollection<" . AttributeTranslation::class . ">"), + Serializer\Type("ArrayCollection"), Serializer\Accessor(getter: "getAttributeTranslations", setter: "setAttributeTranslations") ] protected Collection $attributeTranslations; @@ -231,9 +227,9 @@ public function getOptions(TranslationInterface $translation): ?array function (AttributeTranslationInterface $attributeTranslation) use ($translation) { return $attributeTranslation->getTranslation() === $translation; } - )->first(); - if (false !== $attributeTranslation) { - return $attributeTranslation->getOptions(); + ); + if ($attributeTranslation->count() > 0) { + return $attributeTranslation->first()->getOptions(); } return null; @@ -348,26 +344,4 @@ public function isCountry(): bool { return $this->getType() === AttributeInterface::COUNTRY_T; } - - public function getTypeLabel(): string - { - return match ($this->getType()) { - AttributeInterface::DATETIME_T => 'attributes.form.type.datetime', - AttributeInterface::BOOLEAN_T => 'attributes.form.type.boolean', - AttributeInterface::INTEGER_T => 'attributes.form.type.integer', - AttributeInterface::DECIMAL_T => 'attributes.form.type.decimal', - AttributeInterface::PERCENT_T => 'attributes.form.type.percent', - AttributeInterface::EMAIL_T => 'attributes.form.type.email', - AttributeInterface::COLOUR_T => 'attributes.form.type.colour', - AttributeInterface::ENUM_T => 'attributes.form.type.enum', - AttributeInterface::DATE_T => 'attributes.form.type.date', - AttributeInterface::COUNTRY_T => 'attributes.form.type.country', - default => 'attributes.form.type.string', - }; - } - - public function getAttributeValues(): Collection - { - return $this->attributeValues; - } } diff --git a/src/Model/AttributeTranslationTrait.php b/src/Model/AttributeTranslationTrait.php index 9926436f..0c2cd860 100644 --- a/src/Model/AttributeTranslationTrait.php +++ b/src/Model/AttributeTranslationTrait.php @@ -4,27 +4,25 @@ namespace RZ\Roadiz\CoreBundle\Model; -use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; +use Doctrine\ORM\Mapping as ORM; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; -use Symfony\Component\Validator\Constraints as Assert; trait AttributeTranslationTrait { #[ ORM\ManyToOne(targetEntity: TranslationInterface::class), - ORM\JoinColumn(name: "translation_id", referencedColumnName: "id", nullable: false, onDelete: "CASCADE"), + ORM\JoinColumn(onDelete: "CASCADE"), Serializer\Groups(["attribute", "node", "nodes_sources"]), Serializer\Type("RZ\Roadiz\Core\AbstractEntities\TranslationInterface"), Serializer\Accessor(getter: "getTranslation", setter: "setTranslation") ] - protected TranslationInterface $translation; + protected ?TranslationInterface $translation = null; #[ - ORM\Column(type: "string", length: 250, unique: false, nullable: false), + ORM\Column(type: "string", unique: false, nullable: false), Serializer\Groups(["attribute", "node", "nodes_sources"]), - Serializer\Type("string"), - Assert\Length(max: 250) + Serializer\Type("string") ] protected string $label = ''; @@ -40,10 +38,10 @@ trait AttributeTranslationTrait #[ ORM\ManyToOne(targetEntity: AttributeInterface::class, cascade: ["persist"], inversedBy: "attributeTranslations"), - ORM\JoinColumn(name: "attribute_id", referencedColumnName: "id", nullable: false, onDelete: "CASCADE"), + ORM\JoinColumn(referencedColumnName: "id", onDelete: "CASCADE"), Serializer\Exclude ] - protected AttributeInterface $attribute; + protected ?AttributeInterface $attribute = null; /** * @return string|null @@ -66,6 +64,7 @@ public function setLabel(?string $label) /** * @param TranslationInterface $translation + * * @return $this */ public function setTranslation(TranslationInterface $translation) @@ -74,7 +73,10 @@ public function setTranslation(TranslationInterface $translation) return $this; } - public function getTranslation(): TranslationInterface + /** + * @return TranslationInterface|null + */ + public function getTranslation(): ?TranslationInterface { return $this->translation; } @@ -89,6 +91,7 @@ public function getAttribute(): AttributeInterface /** * @param AttributeInterface $attribute + * * @return $this */ public function setAttribute(AttributeInterface $attribute) diff --git a/src/Model/AttributeValueInterface.php b/src/Model/AttributeValueInterface.php index aab86a8c..8bb43e55 100644 --- a/src/Model/AttributeValueInterface.php +++ b/src/Model/AttributeValueInterface.php @@ -11,11 +11,8 @@ interface AttributeValueInterface extends PositionedInterface, PersistableInterface { - public function getRealm(): ?RealmInterface; - public function setRealm(?RealmInterface $realm): self; - /** - * @return AttributeInterface|null + * @return AttributeInterface */ public function getAttribute(): ?AttributeInterface; diff --git a/src/Model/AttributeValueTrait.php b/src/Model/AttributeValueTrait.php index ef154cfc..fd6b0cbe 100644 --- a/src/Model/AttributeValueTrait.php +++ b/src/Model/AttributeValueTrait.php @@ -9,19 +9,18 @@ use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; -use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter as BaseFilter; trait AttributeValueTrait { #[ ORM\ManyToOne(targetEntity: AttributeInterface::class, fetch: "EAGER", inversedBy: "attributeValues"), - ORM\JoinColumn(name: "attribute_id", referencedColumnName: "id", nullable: false, onDelete: "CASCADE"), + ORM\JoinColumn(name: "attribute_id", referencedColumnName: "id", onDelete: "CASCADE"), Serializer\Groups(["attribute", "node", "nodes_sources"]), Serializer\Type("RZ\Roadiz\CoreBundle\Entity\Attribute"), ApiFilter(BaseFilter\SearchFilter::class, properties: [ "attribute.id" => "exact", "attribute.code" => "exact", - "attribute.color" => "exact", "attribute.type" => "exact", "attribute.group" => "exact", "attribute.group.canonicalName" => "exact", @@ -29,16 +28,9 @@ trait AttributeValueTrait ApiFilter(BaseFilter\BooleanFilter::class, properties: [ "attribute.visible", "attribute.searchable" - ]), - ApiFilter(BaseFilter\ExistsFilter::class, properties: [ - "attribute.color", - "attribute.group" - ]), - ApiFilter(BaseFilter\OrderFilter::class, properties: [ - "attribute.weight" => "DESC", ]) ] - protected AttributeInterface $attribute; + protected ?AttributeInterface $attribute = null; /** * @var Collection @@ -53,21 +45,12 @@ trait AttributeValueTrait ), Serializer\Groups(["attribute", "node", "nodes_sources"]), Serializer\Type("ArrayCollection"), - Serializer\Accessor(getter: "getAttributeValueTranslations", setter: "setAttributeValueTranslations"), - ApiFilter(BaseFilter\SearchFilter::class, properties: [ - "attributeValueTranslations.value" => "partial", - ]), - ApiFilter(BaseFilter\RangeFilter::class, properties: [ - "attributeValueTranslations.value", - ]), - ApiFilter(BaseFilter\ExistsFilter::class, properties: [ - "attributeValueTranslations.value", - ]), + Serializer\Accessor(getter: "getAttributeValueTranslations", setter: "setAttributeValueTranslations") ] protected Collection $attributeValueTranslations; /** - * @return AttributeInterface|null + * @return AttributeInterface */ public function getAttribute(): ?AttributeInterface { @@ -76,7 +59,8 @@ public function getAttribute(): ?AttributeInterface /** * @param AttributeInterface $attribute - * @return self + * + * @return mixed */ public function setAttribute(AttributeInterface $attribute) { @@ -103,7 +87,7 @@ public function getAttributeValueTranslations(): Collection /** * @param Collection $attributeValueTranslations * - * @return static + * @return mixed */ public function setAttributeValueTranslations(Collection $attributeValueTranslations) { @@ -112,13 +96,13 @@ public function setAttributeValueTranslations(Collection $attributeValueTranslat foreach ($this->attributeValueTranslations as $attributeValueTranslation) { $attributeValueTranslation->setAttributeValue($this); } - return $this; + return true; } /** * @param TranslationInterface $translation * - * @return AttributeValueTranslationInterface|null + * @return AttributeValueTranslationInterface */ public function getAttributeValueTranslation(TranslationInterface $translation): ?AttributeValueTranslationInterface { @@ -131,16 +115,4 @@ public function getAttributeValueTranslation(TranslationInterface $translation): }) ->first() ?: null; } - - /** - * @return AttributeValueTranslationInterface|null - */ - public function getAttributeValueDefaultTranslation(): ?AttributeValueTranslationInterface - { - return $this->getAttributeValueTranslations() - ->filter(function (AttributeValueTranslationInterface $attributeValueTranslation) { - return $attributeValueTranslation->getTranslation()?->isDefaultTranslation() ?? false; - }) - ->first() ?: null; - } } diff --git a/src/Model/AttributeValueTranslationTrait.php b/src/Model/AttributeValueTranslationTrait.php index 57cb8ae2..94020752 100644 --- a/src/Model/AttributeValueTranslationTrait.php +++ b/src/Model/AttributeValueTranslationTrait.php @@ -7,58 +7,62 @@ use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; -use Symfony\Component\Validator\Constraints as Assert; trait AttributeValueTranslationTrait { #[ ORM\ManyToOne(targetEntity: TranslationInterface::class), - ORM\JoinColumn(name: "translation_id", referencedColumnName: "id", nullable: false, onDelete: "CASCADE"), + ORM\JoinColumn(name: "translation_id", referencedColumnName: "id", onDelete: "CASCADE"), Serializer\Groups(["attribute", "node", "nodes_sources"]), Serializer\Type("RZ\Roadiz\Core\AbstractEntities\TranslationInterface"), Serializer\Accessor(getter: "getTranslation", setter: "setTranslation") ] - protected TranslationInterface $translation; + protected ?TranslationInterface $translation = null; #[ ORM\Column(type: "string", length: 255, unique: false, nullable: true), Serializer\Groups(["attribute", "node", "nodes_sources"]), - Serializer\Type("string"), - Assert\Length(max: 255) + Serializer\Type("string") ] protected ?string $value = null; #[ ORM\ManyToOne(targetEntity: AttributeValueInterface::class, cascade: ["persist"], inversedBy: "attributeValueTranslations"), - ORM\JoinColumn(name: "attribute_value", referencedColumnName: "id", nullable: false, onDelete: "CASCADE"), + ORM\JoinColumn(name: "attribute_value", referencedColumnName: "id", onDelete: "CASCADE"), Serializer\Exclude ] - protected AttributeValueInterface $attributeValue; + protected ?AttributeValueInterface $attributeValue = null; /** - * @return bool|\DateTime|float|int|string|null + * @return mixed|null * @throws \Exception */ - public function getValue(): bool|\DateTime|float|int|string|null + public function getValue() { if (null === $this->value) { return null; } - return match ($this->getAttributeValue()->getType()) { - AttributeInterface::DECIMAL_T => (float) $this->value, - AttributeInterface::INTEGER_T => (int) $this->value, - AttributeInterface::BOOLEAN_T => (bool) $this->value, - AttributeInterface::DATETIME_T, AttributeInterface::DATE_T => $this->value ? new \DateTime($this->value) : null, - default => $this->value, - }; + switch ($this->getAttributeValue()->getType()) { + case AttributeInterface::DECIMAL_T: + return (float) $this->value; + case AttributeInterface::INTEGER_T: + return (int) $this->value; + case AttributeInterface::BOOLEAN_T: + return (bool) $this->value; + case AttributeInterface::DATETIME_T: + case AttributeInterface::DATE_T: + return $this->value ? new \DateTime($this->value) : null; + default: + return $this->value; + } } /** * @param mixed|null $value * - * @return self + * @return static */ - public function setValue(mixed $value) + public function setValue($value) { if (null === $value) { $this->value = null; @@ -72,7 +76,7 @@ public function setValue(mixed $value) return $this; case AttributeInterface::DATETIME_T: case AttributeInterface::DATE_T: - if ($value instanceof \DateTimeInterface) { + if ($value instanceof \DateTime) { $this->value = $value->format('Y-m-d H:i:s'); } else { $this->value = (string) $value; @@ -86,7 +90,8 @@ public function setValue(mixed $value) /** * @param TranslationInterface $translation - * @return self + * + * @return static */ public function setTranslation(TranslationInterface $translation) { @@ -94,7 +99,10 @@ public function setTranslation(TranslationInterface $translation) return $this; } - public function getTranslation(): TranslationInterface + /** + * @return TranslationInterface|null + */ + public function getTranslation(): ?TranslationInterface { return $this->translation; } @@ -109,7 +117,8 @@ public function getAttributeValue(): AttributeValueInterface /** * @param AttributeValueInterface $attributeValue - * @return self + * + * @return static */ public function setAttributeValue(AttributeValueInterface $attributeValue) { @@ -117,7 +126,10 @@ public function setAttributeValue(AttributeValueInterface $attributeValue) return $this; } - public function getAttribute(): AttributeInterface + /** + * @return AttributeInterface + */ + public function getAttribute(): ?AttributeInterface { return $this->getAttributeValue()->getAttribute(); } diff --git a/src/Model/RealmInterface.php b/src/Model/RealmInterface.php index 6518eca8..e637d108 100644 --- a/src/Model/RealmInterface.php +++ b/src/Model/RealmInterface.php @@ -5,9 +5,8 @@ namespace RZ\Roadiz\CoreBundle\Model; use Doctrine\Common\Collections\Collection; -use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; -interface RealmInterface extends PersistableInterface +interface RealmInterface { public const TYPE_PLAIN_PASSWORD = 'plain_password'; public const TYPE_ROLE = 'bearer_role'; diff --git a/src/Node/CachedNodeOffspringResolver.php b/src/Node/CachedNodeOffspringResolver.php deleted file mode 100644 index 52571ccd..00000000 --- a/src/Node/CachedNodeOffspringResolver.php +++ /dev/null @@ -1,68 +0,0 @@ -cache->getItem(self::CACHE_PREFIX . $ancestor->getId()); - if (!$cacheItem->isHit()) { - $nodeRepository = $this->managerRegistry->getRepository(Node::class); - $offspringIds = $nodeRepository->findAllOffspringIdByNode($ancestor); - $cacheItem->set($offspringIds); - $cacheItem->expiresAfter(300); - if ($cacheItem instanceof ItemInterface && $this->cache instanceof TagAwareCacheInterface) { - $cacheItem->tag(array_map(function (int $nodeId) { - return self::CACHE_TAG_PREFIX . $nodeId; - }, $offspringIds)); - } - $this->cache->save($cacheItem); - } else { - $offspringIds = $cacheItem->get(); - } - return $offspringIds; - } - - /** - * @throws InvalidArgumentException - */ - public function purgeOffspringCache(Node $node): void - { - $this->cache->deleteItem(self::CACHE_PREFIX . $node->getId()); - if ($this->cache instanceof TagAwareCacheInterface) { - /* - * If cache pool supports tags, we can invalidate all nodes at once. - */ - $this->cache->invalidateTags([self::CACHE_TAG_PREFIX . $node->getId()]); - } else { - $ancestorsId = $this->managerRegistry - ->getRepository(Node::class) - ->findAllParentsIdByNode($node); - foreach ($ancestorsId as $ancestorId) { - $this->cache->deleteItem(self::CACHE_PREFIX . $ancestorId); - } - } - } -} diff --git a/src/Node/CachedNodeOffspringResolverInterface.php b/src/Node/CachedNodeOffspringResolverInterface.php deleted file mode 100644 index 5e79bc61..00000000 --- a/src/Node/CachedNodeOffspringResolverInterface.php +++ /dev/null @@ -1,14 +0,0 @@ -getNodeSources()->first(); - if (false === $nodeSource) { - throw new \RuntimeException('Node source is missing.'); - } $node->setNodeName( - $this->nodeNamePolicy->getSafeNodeName($nodeSource) + $this->nodeNamePolicy->getSafeNodeName($node->getNodeSources()->first()) ); /** @var Node $child */ @@ -102,7 +98,8 @@ private function doDuplicate(Node &$node): Node $nsDoc->setNodeSource($nodeSource); $doc = $nsDoc->getDocument(); $nsDoc->setDocument($doc); - $nsDoc->setFieldName($nsDoc->getFieldName()); + $f = $nsDoc->getField(); + $nsDoc->setField($f); $this->objectManager->persist($nsDoc); } } @@ -140,11 +137,9 @@ private function doDuplicate(Node &$node): Node */ private function doDuplicateNodeRelations(Node $node): Node { - /** @var NodesToNodes[] $nodeRelations */ - $nodeRelations = $node->getBNodes()->toArray(); + $nodeRelations = new ArrayCollection($node->getBNodes()->toArray()); foreach ($nodeRelations as $position => $nodeRelation) { - $ntn = new NodesToNodes($node, $nodeRelation->getNodeB()); - $ntn->setFieldName($nodeRelation->getFieldName()); + $ntn = new NodesToNodes($node, $nodeRelation->getNodeB(), $nodeRelation->getField()); $ntn->setPosition($position); $this->objectManager->persist($ntn); } diff --git a/src/Node/NodeFactory.php b/src/Node/NodeFactory.php index 1cbc852b..47b33cbb 100644 --- a/src/Node/NodeFactory.php +++ b/src/Node/NodeFactory.php @@ -17,10 +17,19 @@ final class NodeFactory { + private ManagerRegistry $managerRegistry; + private NodeNamePolicyInterface $nodeNamePolicy; + + /** + * @param ManagerRegistry $managerRegistry + * @param NodeNamePolicyInterface $nodeNamePolicy + */ public function __construct( - private readonly ManagerRegistry $managerRegistry, - private readonly NodeNamePolicyInterface $nodeNamePolicy + ManagerRegistry $managerRegistry, + NodeNamePolicyInterface $nodeNamePolicy ) { + $this->nodeNamePolicy = $nodeNamePolicy; + $this->managerRegistry = $managerRegistry; } public function create( @@ -43,14 +52,12 @@ public function create( } if ($node === null) { - $node = new Node(); - $node->setNodeType($type); + $node = new Node($type); } - if ($node->getNodeType() instanceof NodeType) { - $node->setTtl($node->getNodeType()->getDefaultTtl()); + if ($type instanceof NodeType) { + $node->setTtl($type->getDefaultTtl()); } - if (null !== $parent) { $node->setParent($parent); } @@ -93,12 +100,10 @@ public function createWithUrlAlias( ?Node $parent = null ): Node { $node = $this->create($title, $type, $translation, $node, $parent); - $nodeSource = $node->getNodeSources()->first(); /** @var UrlAliasRepository $repository */ $repository = $this->managerRegistry->getRepository(UrlAlias::class); - if (false !== $nodeSource && false === $repository->exists($urlAlias)) { - $alias = new UrlAlias(); - $alias->setNodeSource($nodeSource); + if (false === $repository->exists($urlAlias)) { + $alias = new UrlAlias($node->getNodeSources()->first()); $alias->setAlias($urlAlias); $this->managerRegistry->getManagerForClass(UrlAlias::class)->persist($alias); } diff --git a/src/Node/NodeMover.php b/src/Node/NodeMover.php index 8deaff0d..2e866db9 100644 --- a/src/Node/NodeMover.php +++ b/src/Node/NodeMover.php @@ -14,8 +14,6 @@ use RZ\Roadiz\CoreBundle\Entity\Redirection; use RZ\Roadiz\Core\Handlers\HandlerFactoryInterface; use RZ\Roadiz\CoreBundle\EntityHandler\NodeHandler; -use RZ\Roadiz\CoreBundle\Event\Redirection\PostCreatedRedirectionEvent; -use RZ\Roadiz\CoreBundle\Event\Redirection\PostUpdatedRedirectionEvent; use RZ\Roadiz\CoreBundle\Repository\EntityRepository; use RZ\Roadiz\CoreBundle\Routing\NodeRouter; use RZ\Roadiz\CoreBundle\Node\Exception\SameNodeUrlException; @@ -203,7 +201,6 @@ protected function redirect(NodesSources $nodeSource, string $previousPath, bool $this->getManager()->remove($loopingRedirection); } - /** @var Redirection|null $existingRedirection */ $existingRedirection = $redirectionRepo->findOneBy([ 'query' => $previousPath, ]); @@ -213,7 +210,7 @@ protected function redirect(NodesSources $nodeSource, string $previousPath, bool $existingRedirection->setQuery($previousPath); $this->logger->info('New redirection created', [ 'oldPath' => $previousPath, - 'nodeSource' => $nodeSource, + 'nodeSource' => $nodeSource->getId(), ]); } $existingRedirection->setRedirectNodeSource($nodeSource); @@ -222,7 +219,6 @@ protected function redirect(NodesSources $nodeSource, string $previousPath, bool } else { $existingRedirection->setType(Response::HTTP_FOUND); } - $this->dispatcher->dispatch(new PostUpdatedRedirectionEvent($existingRedirection)); } return $nodeSource; diff --git a/src/Node/NodeNameChecker.php b/src/Node/NodeNameChecker.php index b9b54d77..e1c4f98b 100644 --- a/src/Node/NodeNameChecker.php +++ b/src/Node/NodeNameChecker.php @@ -53,7 +53,7 @@ public function getCanonicalNodeName(NodesSources $nodeSource): string return sprintf( '%s-%s', $nodeTypeSuffix, - null !== $nodeSource->getNode()->getId() + null !== $nodeSource->getNode() ? $nodeSource->getNode()->getId() : $nodeSource->getId() ); } diff --git a/src/Node/NodeOffspringResolverInterface.php b/src/Node/NodeOffspringResolverInterface.php deleted file mode 100644 index 138c8958..00000000 --- a/src/Node/NodeOffspringResolverInterface.php +++ /dev/null @@ -1,16 +0,0 @@ - - */ - public function getAllOffspringIds(Node $ancestor): array; -} diff --git a/src/Node/NodeTranstyper.php b/src/Node/NodeTranstyper.php index 2109757e..f4a20417 100644 --- a/src/Node/NodeTranstyper.php +++ b/src/Node/NodeTranstyper.php @@ -13,6 +13,7 @@ use RZ\Roadiz\Contracts\NodeType\NodeTypeInterface; use RZ\Roadiz\Core\AbstractEntities\AbstractField; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; +use RZ\Roadiz\CoreBundle\Entity\Log; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\NodesSourcesDocuments; @@ -97,7 +98,6 @@ public function transtype(Node $node, NodeTypeInterface $destinationNodeType, bo } $this->logger->debug('Get matching fields'); - /** @var class-string $sourceClass */ $sourceClass = $destinationNodeType->getSourceEntityFullQualifiedClassName(); /* @@ -113,6 +113,14 @@ public function transtype(Node $node, NodeTypeInterface $destinationNodeType, bo * Perform actual trans-typing */ $existingSources = $node->getNodeSources()->toArray(); + $existingLogs = []; + /** @var NodesSources $existingSource */ + foreach ($existingSources as $existingSource) { + $existingLogs[$existingSource->getTranslation()->getLocale()] = array_map(function (Log $log) { + $this->managerRegistry->getManager()->detach($log); + return $log; + }, $existingSource->getLogs()->toArray()); + } $existingRedirections = []; /** @var NodesSources $existingSource */ foreach ($existingSources as $existingSource) { @@ -133,6 +141,7 @@ public function transtype(Node $node, NodeTypeInterface $destinationNodeType, bo $existingSource->getTranslation(), $sourceClass, $fieldAssociations, + $existingLogs, $existingRedirections ); $this->logger->debug('Transtyped: ' . $existingSource->getTranslation()->getLocale()); @@ -165,8 +174,9 @@ protected function removeOldSources(Node $node, array &$sources): void * @param Node $node * @param NodesSources $existingSource * @param TranslationInterface $translation - * @param class-string $sourceClass + * @param string $sourceClass * @param array $fieldAssociations + * @param array $existingLogs * @param array $existingRedirections * @return NodesSources */ @@ -176,6 +186,7 @@ protected function doTranstypeSingleSource( TranslationInterface $translation, string $sourceClass, array &$fieldAssociations, + array &$existingLogs, array &$existingRedirections ): NodesSources { /** @var NodesSources $source */ @@ -202,8 +213,7 @@ protected function doTranstypeSingleSource( */ $documents = $existingSource->getDocumentsByFieldsWithName($oldField->getName()); foreach ($documents as $document) { - $nsDoc = new NodesSourcesDocuments($source, $document); - $nsDoc->setFieldName($matchingField->getName()); + $nsDoc = new NodesSourcesDocuments($source, $document, $matchingField); $this->getManager()->persist($nsDoc); $source->getDocumentsByFields()->add($nsDoc); } @@ -211,13 +221,26 @@ protected function doTranstypeSingleSource( } $this->logger->debug('Fill existing data'); + + /** @var Log $log */ + foreach ($existingLogs[$translation->getLocale()] as $log) { + $newLog = clone $log; + $newLog->setAdditionalData($log->getAdditionalData()); + $newLog->setChannel($log->getChannel()); + $newLog->setClientIp($log->getClientIp()); + $newLog->setUser($log->getUser()); + $newLog->setUsername($log->getUsername()); + $this->getManager()->persist($newLog); + $newLog->setNodeSource($source); + } + $this->logger->debug('Recreate logs'); + /* * Recreate url-aliases too. */ /** @var UrlAlias $urlAlias */ foreach ($existingSource->getUrlAliases() as $urlAlias) { - $newUrlAlias = new UrlAlias(); - $newUrlAlias->setNodeSource($source); + $newUrlAlias = new UrlAlias($source); $this->getManager()->persist($newUrlAlias); $newUrlAlias->setAlias($urlAlias->getAlias()); $source->addUrlAlias($newUrlAlias); @@ -259,7 +282,6 @@ protected function mockTranstype(NodeTypeInterface $nodeType): void * transtype, not to get an orphan node. */ $node = new Node(); - $node->setNodeType($nodeType); $node->setNodeName('testing_before_transtype' . $uniqueId); $this->getManager()->persist($node); diff --git a/src/Node/UniqueNodeGenerator.php b/src/Node/UniqueNodeGenerator.php index 718cea75..3f3c6762 100644 --- a/src/Node/UniqueNodeGenerator.php +++ b/src/Node/UniqueNodeGenerator.php @@ -4,9 +4,6 @@ namespace RZ\Roadiz\CoreBundle\Node; -use DateTime; -use Doctrine\ORM\OptimisticLockException; -use Doctrine\ORM\ORMException; use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; use RZ\Roadiz\CoreBundle\Entity\Node; @@ -14,19 +11,22 @@ use RZ\Roadiz\CoreBundle\Entity\NodeType; use RZ\Roadiz\CoreBundle\Entity\Tag; use RZ\Roadiz\CoreBundle\Entity\Translation; -use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; -use Symfony\Component\Security\Core\Exception\AccessDeniedException; -use Symfony\Bundle\SecurityBundle\Security; class UniqueNodeGenerator { - public function __construct( - protected ManagerRegistry $managerRegistry, - protected NodeNamePolicyInterface $nodeNamePolicy, - protected Security $security, - ) { + protected NodeNamePolicyInterface $nodeNamePolicy; + private ManagerRegistry $managerRegistry; + + /** + * @param ManagerRegistry $managerRegistry + * @param NodeNamePolicyInterface $nodeNamePolicy + */ + public function __construct(ManagerRegistry $managerRegistry, NodeNamePolicyInterface $nodeNamePolicy) + { + $this->nodeNamePolicy = $nodeNamePolicy; + $this->managerRegistry = $managerRegistry; } /** @@ -49,14 +49,15 @@ public function generate( bool $pushToTop = false ): NodesSources { $name = $nodeType->getDisplayName() . " " . uniqid(); - $node = new Node(); - $node->setNodeType($nodeType); + $node = new Node($nodeType); $node->setTtl($nodeType->getDefaultTtl()); if (null !== $tag) { $node->addTag($tag); } - $parent?->addChild($node); + if (null !== $parent) { + $parent->addChild($node); + } if ($pushToTop) { /* @@ -65,12 +66,11 @@ public function generate( $node->setPosition(0.5); } - /** @var class-string $sourceClass */ # phpstan hint $sourceClass = NodeType::getGeneratedEntitiesNamespace() . "\\" . $nodeType->getSourceEntityClassName(); $source = new $sourceClass($node, $translation); $source->setTitle($name); - $source->setPublishedAt(new DateTime()); + $source->setPublishedAt(new \DateTime()); $node->setNodeName($this->nodeNamePolicy->getCanonicalNodeName($source)); $manager = $this->managerRegistry->getManagerForClass(Node::class); @@ -89,8 +89,8 @@ public function generate( * @param Request $request * * @return NodesSources - * @throws ORMException - * @throws OptimisticLockException + * @throws \Doctrine\ORM\ORMException + * @throws \Doctrine\ORM\OptimisticLockException */ public function generateFromRequest(Request $request): NodesSources { @@ -112,13 +112,7 @@ public function generateFromRequest(Request $request): NodesSources $parent = $this->managerRegistry ->getRepository(Node::class) ->find((int) $request->get('parentNodeId')); - if (null === $parent || !$this->security->isGranted(NodeVoter::CREATE, $parent)) { - throw new BadRequestHttpException("Parent node does not exist."); - } } else { - if (!$this->security->isGranted(NodeVoter::CREATE_AT_ROOT)) { - throw new AccessDeniedException('You are not allowed to create a node at root.'); - } $parent = null; } @@ -145,8 +139,8 @@ public function generateFromRequest(Request $request): NodesSources /* * If parent has only on translation, use parent translation instead of default one. */ - if (null !== $parent && false !== $parentNodeSource = $parent->getNodeSources()->first()) { - $translation = $parentNodeSource->getTranslation(); + if (null !== $parent && $parent->getNodeSources()->count() === 1) { + $translation = $parent->getNodeSources()->first()->getTranslation(); } else { /** @var Translation $translation */ $translation = $this->managerRegistry diff --git a/src/Node/UniversalDataDuplicator.php b/src/Node/UniversalDataDuplicator.php index c806f3ae..41b0ae45 100644 --- a/src/Node/UniversalDataDuplicator.php +++ b/src/Node/UniversalDataDuplicator.php @@ -5,7 +5,6 @@ namespace RZ\Roadiz\CoreBundle\Node; use Doctrine\Persistence\ManagerRegistry; -use RZ\Roadiz\Contracts\NodeType\NodeTypeFieldInterface; use RZ\Roadiz\Core\AbstractEntities\AbstractField; use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\NodesSourcesDocuments; @@ -111,10 +110,15 @@ private function hasDefaultTranslation(NodesSources $source): bool return $sourceCount === 1; } + /** + * @param NodesSources $universalSource + * @param NodesSources $destSource + * @param NodeTypeField $field + */ protected function duplicateNonVirtualField( NodesSources $universalSource, NodesSources $destSource, - NodeTypeFieldInterface $field + NodeTypeField $field ): void { $getter = $field->getGetterName(); $setter = $field->getSetterName(); @@ -122,18 +126,23 @@ protected function duplicateNonVirtualField( $destSource->$setter($universalSource->$getter()); } + /** + * @param NodesSources $universalSource + * @param NodesSources $destSource + * @param NodeTypeField $field + */ protected function duplicateDocumentsField( NodesSources $universalSource, NodesSources $destSource, - NodeTypeFieldInterface $field + NodeTypeField $field ): void { $newDocuments = $this->managerRegistry ->getRepository(NodesSourcesDocuments::class) - ->findBy(['nodeSource' => $universalSource, 'fieldName' => $field->getName()]); + ->findBy(['nodeSource' => $universalSource, 'field' => $field]); $formerDocuments = $this->managerRegistry ->getRepository(NodesSourcesDocuments::class) - ->findBy(['nodeSource' => $destSource, 'fieldName' => $field->getName()]); + ->findBy(['nodeSource' => $destSource, 'field' => $field]); $manager = $this->managerRegistry->getManagerForClass(NodesSourcesDocuments::class); if (null === $manager) { diff --git a/src/NodeType/ApiResourceGenerator.php b/src/NodeType/ApiResourceGenerator.php index 6506c450..59192eaa 100644 --- a/src/NodeType/ApiResourceGenerator.php +++ b/src/NodeType/ApiResourceGenerator.php @@ -4,32 +4,19 @@ namespace RZ\Roadiz\CoreBundle\NodeType; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\GetCollection; -use Doctrine\Inflector\InflectorFactory; use LogicException; -use Psr\Log\LoggerInterface; use RZ\Roadiz\Contracts\NodeType\NodeTypeInterface; -use RZ\Roadiz\CoreBundle\Api\Controller\GetWebResponseByPathController; -use RZ\Roadiz\CoreBundle\Api\Model\WebResponseInterface; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\String\UnicodeString; use Symfony\Component\Yaml\Yaml; final class ApiResourceGenerator { - /** - * @param ApiResourceOperationNameGenerator $apiResourceOperationNameGenerator - * @param string $apiResourcesDir - * @param LoggerInterface $logger - * @param class-string $webResponseClass - */ - public function __construct( - private readonly ApiResourceOperationNameGenerator $apiResourceOperationNameGenerator, - private readonly string $apiResourcesDir, - private readonly LoggerInterface $logger, - private readonly string $webResponseClass - ) { + private string $apiResourcesDir; + + public function __construct(string $apiResourcesDir) + { + $this->apiResourcesDir = $apiResourcesDir; } /** @@ -45,38 +32,12 @@ public function generate(NodeTypeInterface $nodeType): ?string } $resourcePath = $this->getResourcePath($nodeType); - $webResponseResourcePath = $this->getWebResponseResourcePath(); - - if (!$filesystem->exists($webResponseResourcePath)) { - $filesystem->dumpFile( - $webResponseResourcePath, - Yaml::dump([ - 'resources' => [ - $this->webResponseClass => [ - 'operations' => [], - ] - ] - ], 7) - ); - } - $filesystem->dumpFile( - $webResponseResourcePath, - Yaml::dump($this->addWebResponseResourceOperation($nodeType, $webResponseResourcePath), 7) - ); - $this->logger->info('API WebResponse config file has been updated.', [ - 'file' => $webResponseResourcePath, - ]); - \clearstatcache(true, $webResponseResourcePath); if (!$filesystem->exists($resourcePath)) { $filesystem->dumpFile( $resourcePath, - Yaml::dump($this->getApiResourceDefinition($nodeType), 7) + Yaml::dump($this->getApiResourceDefinition($nodeType), 6) ); - $this->logger->info('API resource config file has been generated.', [ - 'nodeType' => $nodeType->getName(), - 'file' => $resourcePath, - ]); \clearstatcache(true, $resourcePath); return $resourcePath; } else { @@ -93,30 +54,14 @@ public function remove(NodeTypeInterface $nodeType): void } $resourcePath = $this->getResourcePath($nodeType); - $webResponseResourcePath = $this->getWebResponseResourcePath(); - - if ($filesystem->exists($webResponseResourcePath)) { - $filesystem->dumpFile( - $webResponseResourcePath, - Yaml::dump($this->removeWebResponseResourceOperation($nodeType, $webResponseResourcePath), 7) - ); - $this->logger->info('API WebResponse config file has been updated.', [ - 'file' => $webResponseResourcePath, - ]); - \clearstatcache(true, $webResponseResourcePath); - } if ($filesystem->exists($resourcePath)) { $filesystem->remove($resourcePath); - $this->logger->info('API resource config file has been removed.', [ - 'nodeType' => $nodeType->getName(), - 'file' => $resourcePath, - ]); @\clearstatcache(true, $resourcePath); } } - public function getResourcePath(NodeTypeInterface $nodeType): string + protected function getResourcePath(NodeTypeInterface $nodeType): string { return $this->apiResourcesDir . '/' . (new UnicodeString($nodeType->getName())) ->lower() @@ -125,148 +70,22 @@ public function getResourcePath(NodeTypeInterface $nodeType): string ->toString(); } - protected function getWebResponseResourcePath(): string - { - return $this->apiResourcesDir . '/web_response.yml'; - } - - protected function getResourceName(string $nodeTypeName): string - { - return (new UnicodeString($nodeTypeName)) - ->snake() - ->lower() - ->toString(); - } - - protected function getResourceUriPrefix(NodeTypeInterface $nodeType): string - { - $pluralNodeTypeName = InflectorFactory::create()->build()->pluralize($nodeType->getName()); - return '/' . $this->getResourceName($pluralNodeTypeName); - } - protected function getApiResourceDefinition(NodeTypeInterface $nodeType): array { $fqcn = (new UnicodeString($nodeType->getSourceEntityFullQualifiedClassName())) ->trimStart('\\') ->toString(); - return [ - 'resources' => [ - $fqcn => [ - 'shortName' => $nodeType->getName(), - 'types' => [$nodeType->getName()], - 'operations' => [ - ...$this->getCollectionOperations($nodeType), - ...$this->getItemOperations($nodeType) - ], - ] - ] - ]; - } - - protected function addWebResponseResourceOperation(NodeTypeInterface $nodeType, string $webResponseResourcePath): array - { - $getByPathOperationName = $this->apiResourceOperationNameGenerator->generateGetByPath( - $nodeType->getSourceEntityFullQualifiedClassName() - ); - $webResponseResource = Yaml::parseFile($webResponseResourcePath); - - if (!\array_key_exists($this->webResponseClass, $webResponseResource['resources'])) { - $webResponseResource = [ - 'resources' => [ - $this->webResponseClass => [ - 'operations' => [], - ] - ], - ]; - } - - if (\array_key_exists('operations', $webResponseResource['resources'][$this->webResponseClass])) { - $operations = $webResponseResource['resources'][$this->webResponseClass]['operations']; - } else { - $operations = []; - } - - if (!$nodeType->isReachable()) { - // Do not add operation if node-type is not reachable - return $webResponseResource; - } - if (\array_key_exists($getByPathOperationName, $operations)) { - // Do not add operation if already exists - return $webResponseResource; - } - - $groups = $this->getItemOperationSerializationGroups($nodeType); - $operations[$getByPathOperationName] = [ - 'method' => 'GET', - 'class' => Get::class, - 'uriTemplate' => '/web_response_by_path', - 'read' => false, - 'controller' => GetWebResponseByPathController::class, - 'normalizationContext' => [ - 'pagination_enabled' => false, - 'enable_max_depth' => true, - 'groups' => [ - $getByPathOperationName, - ...array_values(array_filter(array_unique($groups))), - ...[ - 'web_response', - 'walker', - 'children', - ] - ] - ], - 'openapiContext' => [ - 'tags' => ['WebResponse'], - 'summary' => 'Get a ' . $nodeType->getName() . ' by its path wrapped in a WebResponse object', - 'description' => 'Get a ' . $nodeType->getName() . ' by its path wrapped in a WebResponse', - 'parameters' => [ - [ - 'type' => 'string', - 'name' => 'path', - 'in' => 'query', - 'required' => true, - 'description' => 'Resource path, or `/` for home page', - 'schema' => [ - 'type' => 'string', - ], - ] - ] - ] - ]; - - $webResponseResource['resources'][$this->webResponseClass]['operations'] = $operations; - return $webResponseResource; - } - - protected function removeWebResponseResourceOperation(NodeTypeInterface $nodeType, string $webResponseResourcePath): array - { - $getByPathOperationName = $this->apiResourceOperationNameGenerator->generateGetByPath( - $nodeType->getSourceEntityFullQualifiedClassName() - ); - $webResponseResource = Yaml::parseFile($webResponseResourcePath); - - if (!\array_key_exists($this->webResponseClass, $webResponseResource['resources'])) { - return $webResponseResource; - } - if (\array_key_exists('operations', $webResponseResource['resources'][$this->webResponseClass])) { - $operations = $webResponseResource['resources'][$this->webResponseClass]['operations']; - } else { - return $webResponseResource; - } - if (!\array_key_exists($getByPathOperationName, $operations)) { - // Do not remove operation if it does not exist - return $webResponseResource; - } - - unset($operations[$getByPathOperationName]); - $webResponseResource['resources'][$this->webResponseClass]['operations'] = array_filter($operations); - return $webResponseResource; + return [ $fqcn => [ + 'iri' => $nodeType->getName(), + 'shortName' => $nodeType->getName(), + 'collectionOperations' => $this->getCollectionOperations($nodeType), + 'itemOperations' => $this->getItemOperations($nodeType), + ]]; } protected function getCollectionOperations(NodeTypeInterface $nodeType): array { - $operations = []; if ($nodeType->isReachable()) { $groups = [ "nodes_sources_base", @@ -279,60 +98,23 @@ protected function getCollectionOperations(NodeTypeInterface $nodeType): array "document_display_sources", ...$this->getGroupedFieldsSerializationGroups($nodeType) ]; - - $collectionOperationName = $this->apiResourceOperationNameGenerator->generate( - $nodeType->getSourceEntityFullQualifiedClassName(), - 'get_collection' - ); - $operations = array_merge( - $operations, - [ - $collectionOperationName => [ - 'method' => 'GET', - 'class' => GetCollection::class, - 'shortName' => $nodeType->getName(), - 'normalizationContext' => [ - 'enable_max_depth' => true, - 'groups' => array_values(array_filter(array_unique($groups))) - ], - ] - ] - ); - } - if ($nodeType->isPublishable()) { - $archivesOperationName = $this->apiResourceOperationNameGenerator->generate( - $nodeType->getSourceEntityFullQualifiedClassName(), - 'archives_collection' - ); - $operations = array_merge( - $operations, - [ - $archivesOperationName => [ - 'method' => 'GET', - 'class' => GetCollection::class, - 'shortName' => $nodeType->getName(), - 'uriTemplate' => $this->getResourceUriPrefix($nodeType) . '/archives', - 'extraProperties' => [ - 'archive_enabled' => true, - ], - 'openapiContext' => [ - 'summary' => sprintf( - 'Retrieve all %s ressources archives months and years', - $nodeType->getName() - ), - ], - ] + return [ + 'get' => [ + 'method' => 'GET', + 'normalization_context' => [ + 'enable_max_depth' => true, + 'groups' => array_values(array_filter(array_unique($groups))) + ], ] - ); + ]; } - return $operations; + return []; } - protected function getItemOperationSerializationGroups(NodeTypeInterface $nodeType): array + protected function getItemOperations(NodeTypeInterface $nodeType): array { - return [ + $groups = [ "nodes_sources", - "node_listing", "urls", "tag_base", "translation_base", @@ -341,25 +123,40 @@ protected function getItemOperationSerializationGroups(NodeTypeInterface $nodeTy "document_display_sources", ...$this->getGroupedFieldsSerializationGroups($nodeType) ]; - } - - protected function getItemOperations(NodeTypeInterface $nodeType): array - { - $groups = $this->getItemOperationSerializationGroups($nodeType); - $itemOperationName = $this->apiResourceOperationNameGenerator->generate( - $nodeType->getSourceEntityFullQualifiedClassName(), - 'get' - ); - return [ - $itemOperationName => [ + $operations = [ + 'get' => [ 'method' => 'GET', - 'class' => Get::class, - 'shortName' => $nodeType->getName(), - 'normalizationContext' => [ + 'normalization_context' => [ 'groups' => array_values(array_filter(array_unique($groups))) ], ] ]; + + /* + * Create itemOperation for WebResponseController action + */ + if ($nodeType->isReachable()) { + $operations['getByPath'] = [ + 'method' => 'GET', + 'path' => '/web_response_by_path', + 'read' => false, + 'controller' => 'RZ\Roadiz\CoreBundle\Api\Controller\GetWebResponseByPathController', + 'pagination_enabled' => false, + 'normalization_context' => [ + 'enable_max_depth' => true, + 'groups' => array_merge(array_values(array_filter(array_unique($groups))), [ + 'web_response', + 'walker', + 'walker_level', + 'walker_metadata', + 'meta', + 'children', + ]) + ], + ]; + } + + return $operations; } protected function getGroupedFieldsSerializationGroups(NodeTypeInterface $nodeType): array diff --git a/src/NodeType/ApiResourceOperationNameGenerator.php b/src/NodeType/ApiResourceOperationNameGenerator.php deleted file mode 100644 index bd3da5fa..00000000 --- a/src/NodeType/ApiResourceOperationNameGenerator.php +++ /dev/null @@ -1,28 +0,0 @@ -afterLast('\\') - ->trimPrefix('NS') - ->lower() - ->toString(), - $operation - ); - } - - public function generateGetByPath(string $resourceClass): string - { - return self::generate($resourceClass, 'get_by_path'); - } -} diff --git a/src/NodeType/DefaultValuesResolver.php b/src/NodeType/DefaultValuesResolver.php index e2180403..cc2fc94d 100644 --- a/src/NodeType/DefaultValuesResolver.php +++ b/src/NodeType/DefaultValuesResolver.php @@ -23,6 +23,7 @@ public function __construct( $this->inheritanceType = $inheritanceType; } + public function getDefaultValuesAmongAllFields(NodeTypeFieldInterface $field): array { /* @@ -30,7 +31,7 @@ public function getDefaultValuesAmongAllFields(NodeTypeFieldInterface $field): a * SQL field won't be shared between all node types. */ if ($this->inheritanceType === Configuration::INHERITANCE_TYPE_JOINED) { - return array_map('trim', explode(',', $field->getDefaultValues() ?? '')); + return array_map('trim', explode(',', $field->getDefaultValues())); } else { /* * With single table inheritance, we need to get all default values @@ -42,7 +43,7 @@ public function getDefaultValuesAmongAllFields(NodeTypeFieldInterface $field): a 'type' => $field->getType(), ]); foreach ($nodeTypeFields as $nodeTypeField) { - $defaultValues = array_merge($defaultValues, array_map('trim', explode(',', $nodeTypeField->getDefaultValues() ?? ''))); + $defaultValues = array_merge($defaultValues, array_map('trim', explode(',', $nodeTypeField->getDefaultValues()))); } return $defaultValues; } diff --git a/src/Preview/EventSubscriber/PreviewBarSubscriber.php b/src/Preview/EventSubscriber/PreviewBarSubscriber.php index a3ab4354..f1e05262 100644 --- a/src/Preview/EventSubscriber/PreviewBarSubscriber.php +++ b/src/Preview/EventSubscriber/PreviewBarSubscriber.php @@ -10,10 +10,16 @@ use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; -final class PreviewBarSubscriber implements EventSubscriberInterface +class PreviewBarSubscriber implements EventSubscriberInterface { - public function __construct(private readonly PreviewResolverInterface $previewResolver) + protected PreviewResolverInterface $previewResolver; + + /** + * @param PreviewResolverInterface $previewResolver + */ + public function __construct(PreviewResolverInterface $previewResolver) { + $this->previewResolver = $previewResolver; } /** diff --git a/src/Preview/EventSubscriber/PreviewModeSubscriber.php b/src/Preview/EventSubscriber/PreviewModeSubscriber.php index 82540443..2850b83f 100644 --- a/src/Preview/EventSubscriber/PreviewModeSubscriber.php +++ b/src/Preview/EventSubscriber/PreviewModeSubscriber.php @@ -6,21 +6,31 @@ use RZ\Roadiz\CoreBundle\Preview\Exception\PreviewNotAllowedException; use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; -use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Security; -final class PreviewModeSubscriber implements EventSubscriberInterface +class PreviewModeSubscriber implements EventSubscriberInterface { public const QUERY_PARAM_NAME = '_preview'; + protected PreviewResolverInterface $previewResolver; + protected TokenStorageInterface $tokenStorage; + protected Security $security; + public function __construct( - private readonly PreviewResolverInterface $previewResolver, - private readonly Security $security + PreviewResolverInterface $previewResolver, + TokenStorageInterface $tokenStorage, + Security $security ) { + $this->previewResolver = $previewResolver; + $this->tokenStorage = $tokenStorage; + $this->security = $security; } /** @@ -29,10 +39,9 @@ public function __construct( public static function getSubscribedEvents(): array { return [ - KernelEvents::REQUEST => ['onKernelRequest', 2047], + KernelEvents::REQUEST => ['onKernelRequest', 9999], KernelEvents::CONTROLLER => ['onControllerMatched', 10], - // Must Triggered after API platform AddHeadersListener - KernelEvents::RESPONSE => ['onResponse', -255], + KernelEvents::RESPONSE => 'onResponse', ]; } @@ -52,9 +61,9 @@ public function onKernelRequest(RequestEvent $event): void $request = $event->getRequest(); if ( $event->isMainRequest() && - $request->query->has(self::QUERY_PARAM_NAME) && + $request->query->has(static::QUERY_PARAM_NAME) && \in_array( - $request->query->get(self::QUERY_PARAM_NAME, 0), + $request->query->get(static::QUERY_PARAM_NAME, 0), ['true', true, '1', 1, 'on', 'yes', 'y'], true ) @@ -74,6 +83,11 @@ public function onKernelRequest(RequestEvent $event): void public function onControllerMatched(ControllerEvent $event): void { if ($this->supports() && $event->isMainRequest()) { + /** @var TokenInterface|null $token */ + $token = $this->tokenStorage->getToken(); + if (null === $token || !$token->isAuthenticated()) { + throw new PreviewNotAllowedException('You are not authenticated to use preview mode.'); + } if (!$this->security->isGranted($this->previewResolver->getRequiredRole())) { throw new PreviewNotAllowedException('You are not granted to use preview mode.'); } @@ -89,11 +103,10 @@ public function onResponse(ResponseEvent $event): void { if ($this->supports()) { $response = $event->getResponse(); - $response->setMaxAge(0); - $response->setSharedMaxAge(0); + $response->expire(); $response->headers->addCacheControlDirective('no-store'); $response->headers->add(['X-Roadiz-Preview' => true]); - $response->setPrivate(); + $event->setResponse($response); } } } diff --git a/src/Preview/Exception/PreviewNotAllowedException.php b/src/Preview/Exception/PreviewNotAllowedException.php index 07da1c7b..2c376595 100644 --- a/src/Preview/Exception/PreviewNotAllowedException.php +++ b/src/Preview/Exception/PreviewNotAllowedException.php @@ -12,7 +12,7 @@ */ class PreviewNotAllowedException extends AccessDeniedHttpException { - public function __construct(string $message = "You are not allowed to use preview mode.") + public function __construct($message = "You are not allowed to use preview mode.") { parent::__construct($message); } diff --git a/src/Preview/RequestPreviewRevolver.php b/src/Preview/RequestPreviewRevolver.php index b11944fb..632f9889 100644 --- a/src/Preview/RequestPreviewRevolver.php +++ b/src/Preview/RequestPreviewRevolver.php @@ -33,7 +33,7 @@ public function isPreview(): bool if (null === $request) { return false; } - return $request->attributes->getBoolean('preview'); + return $request->attributes->get('preview', false); } public function getRequiredRole(): string diff --git a/src/Preview/User/PreviewUserProvider.php b/src/Preview/User/PreviewUserProvider.php index 70e07fb8..cea9ee39 100644 --- a/src/Preview/User/PreviewUserProvider.php +++ b/src/Preview/User/PreviewUserProvider.php @@ -6,7 +6,7 @@ use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; use Symfony\Component\Security\Core\Exception\AccessDeniedException; -use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\User\UserInterface; final class PreviewUserProvider implements PreviewUserProviderInterface diff --git a/src/Realm/RealmResolver.php b/src/Realm/RealmResolver.php index d900db6b..fa4f0bf2 100644 --- a/src/Realm/RealmResolver.php +++ b/src/Realm/RealmResolver.php @@ -5,38 +5,30 @@ namespace RZ\Roadiz\CoreBundle\Realm; use Doctrine\Persistence\ManagerRegistry; -use Psr\Cache\CacheItemPoolInterface; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\Realm; use RZ\Roadiz\CoreBundle\Model\RealmInterface; use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\RealmVoter; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; -use Symfony\Bundle\SecurityBundle\Security; -use Symfony\Component\String\Slugger\AsciiSlugger; +use Symfony\Component\Security\Core\Security; final class RealmResolver implements RealmResolverInterface { - public function __construct( - private readonly ManagerRegistry $managerRegistry, - private readonly Security $security, - private readonly CacheItemPoolInterface $cache - ) { - } + private ManagerRegistry $managerRegistry; + private Security $security; - public function getRealms(?Node $node): array + public function __construct(ManagerRegistry $managerRegistry, Security $security) { - if (null === $node) { - return []; - } - return $this->managerRegistry->getRepository(Realm::class)->findByNode($node); + $this->managerRegistry = $managerRegistry; + $this->security = $security; } - public function getRealmsWithSerializationGroup(?Node $node): array + public function getRealms(?Node $node): array { if (null === $node) { return []; } - return $this->managerRegistry->getRepository(Realm::class)->findByNodeWithSerializationGroup($node); + return $this->managerRegistry->getRepository(Realm::class)->findByNode($node); } public function isGranted(RealmInterface $realm): bool @@ -53,59 +45,4 @@ public function denyUnlessGranted(RealmInterface $realm): void ); } } - - private function getUserCacheKey(): string - { - return (new AsciiSlugger()) - ->slug($this->security->getUser()?->getUserIdentifier() ?? 'anonymous') - ->__toString(); - } - - public function getGrantedRealms(): array - { - $cacheItem = $this->cache->getItem('granted_realms_' . $this->getUserCacheKey()); - if (!$cacheItem->isHit()) { - $allRealms = $this->managerRegistry->getRepository(Realm::class)->findBy([]); - $cacheItem->set(array_filter($allRealms, fn(RealmInterface $realm) => $this->isGranted($realm))); - $cacheItem->expiresAfter(new \DateInterval('PT1H')); - $this->cache->save($cacheItem); - } - return $cacheItem->get(); - } - - public function getDeniedRealms(): array - { - $cacheItem = $this->cache->getItem('denied_realms_' . $this->getUserCacheKey()); - if (!$cacheItem->isHit()) { - $allRealms = $this->managerRegistry->getRepository(Realm::class)->findBy([]); - $cacheItem->set(array_filter($allRealms, fn(RealmInterface $realm) => !$this->isGranted($realm))); - $cacheItem->expiresAfter(new \DateInterval('PT1H')); - $this->cache->save($cacheItem); - } - return $cacheItem->get(); - } - - public function hasRealms(): bool - { - $cacheItem = $this->cache->getItem('app_has_realms'); - if (!$cacheItem->isHit()) { - $hasRealms = $this->managerRegistry->getRepository(Realm::class)->countBy([]) > 0; - $cacheItem->set($hasRealms); - $cacheItem->expiresAfter(new \DateInterval('PT2H')); - $this->cache->save($cacheItem); - } - return $cacheItem->get(); - } - - public function hasRealmsWithSerializationGroup(): bool - { - $cacheItem = $this->cache->getItem('app_has_realms_with_serialization_group'); - if (!$cacheItem->isHit()) { - $hasRealms = $this->managerRegistry->getRepository(Realm::class)->countWithSerializationGroup() > 0; - $cacheItem->set($hasRealms); - $cacheItem->expiresAfter(new \DateInterval('PT2H')); - $this->cache->save($cacheItem); - } - return $cacheItem->get(); - } } diff --git a/src/Realm/RealmResolverInterface.php b/src/Realm/RealmResolverInterface.php index 0124db98..970b094f 100644 --- a/src/Realm/RealmResolverInterface.php +++ b/src/Realm/RealmResolverInterface.php @@ -10,24 +10,11 @@ interface RealmResolverInterface { - /** - * @return bool Does current application has realms? - */ - public function hasRealms(): bool; - /** - * @return bool Does current application has realms with serialization groups? - */ - public function hasRealmsWithSerializationGroup(): bool; /** * @param Node|null $node * @return RealmInterface[] */ public function getRealms(?Node $node): array; - /** - * @param Node|null $node - * @return RealmInterface[] - */ - public function getRealmsWithSerializationGroup(?Node $node): array; public function isGranted(RealmInterface $realm): bool; /** @@ -36,14 +23,4 @@ public function isGranted(RealmInterface $realm): bool; * @throws UnauthorizedHttpException */ public function denyUnlessGranted(RealmInterface $realm): void; - - /** - * @return RealmInterface[] Return all realms granted to current user. - */ - public function getGrantedRealms(): array; - - /** - * @return RealmInterface[] Return all realms denied from current user. - */ - public function getDeniedRealms(): array; } diff --git a/src/Repository/AttributeValueRepository.php b/src/Repository/AttributeValueRepository.php index c49f0774..c049e1dd 100644 --- a/src/Repository/AttributeValueRepository.php +++ b/src/Repository/AttributeValueRepository.php @@ -24,15 +24,14 @@ public function __construct( /** * @param AttributableInterface $attributable - * @param bool $orderByWeight - * @return array + * + * @return array */ public function findByAttributable( - AttributableInterface $attributable, - bool $orderByWeight = false + AttributableInterface $attributable ): array { $qb = $this->createQueryBuilder('av'); - $qb = $qb->addSelect('avt') + return $qb->addSelect('avt') ->addSelect('a') ->addSelect('at') ->addSelect('ad') @@ -46,18 +45,12 @@ public function findByAttributable( ->leftJoin('a.group', 'ag') ->leftJoin('ag.attributeGroupTranslations', 'agt') ->andWhere($qb->expr()->eq('av.node', ':attributable')) + ->addOrderBy('av.position', 'ASC') ->setParameters([ 'attributable' => $attributable, ]) - ->setCacheable(true); - - if ($orderByWeight) { - $qb->addOrderBy('a.weight', 'DESC'); - } else { - $qb->addOrderBy('av.position', 'ASC'); - } - - return $qb->getQuery() + ->setCacheable(true) + ->getQuery() ->getResult(); } diff --git a/src/Repository/CustomFormAnswerRepository.php b/src/Repository/CustomFormAnswerRepository.php index d7774a73..3810a296 100644 --- a/src/Repository/CustomFormAnswerRepository.php +++ b/src/Repository/CustomFormAnswerRepository.php @@ -53,6 +53,6 @@ public function deleteByCustomFormSubmittedBefore(CustomForm $customForm, \DateT ->delete() ->setParameter(':customForm', $customForm) ->setParameter(':submittedAt', $submittedAt); - return (int) $qb->getQuery()->getSingleScalarResult(); + return $qb->getQuery()->getSingleScalarResult(); } } diff --git a/src/Repository/CustomFormRepository.php b/src/Repository/CustomFormRepository.php index 62df6b92..027ea52e 100644 --- a/src/Repository/CustomFormRepository.php +++ b/src/Repository/CustomFormRepository.php @@ -33,38 +33,14 @@ public function findAllWithRetentionTime(): array ->getResult(); } - /** - * @param Node $node - * @param NodeTypeFieldInterface $field - * @return CustomForm[] - * @deprecated Use findByNodeAndFieldName instead - */ public function findByNodeAndField(Node $node, NodeTypeFieldInterface $field): array { $query = $this->_em->createQuery(' SELECT cf FROM RZ\Roadiz\CoreBundle\Entity\CustomForm cf INNER JOIN cf.nodes ncf - WHERE ncf.fieldName = :fieldName AND ncf.node = :node - ORDER BY ncf.position ASC') - ->setParameter('fieldName', $field->getName()) - ->setParameter('node', $node); - - return $query->getResult(); - } - - /** - * @param Node $node - * @param string $fieldName - * @return CustomForm[] - */ - public function findByNodeAndFieldName(Node $node, string $fieldName): array - { - $query = $this->_em->createQuery(' - SELECT cf FROM RZ\Roadiz\CoreBundle\Entity\CustomForm cf - INNER JOIN cf.nodes ncf - WHERE ncf.fieldName = :fieldName AND ncf.node = :node + WHERE ncf.field = :field AND ncf.node = :node ORDER BY ncf.position ASC') - ->setParameter('fieldName', $fieldName) + ->setParameter('field', $field) ->setParameter('node', $node); return $query->getResult(); diff --git a/src/Repository/DocumentRepository.php b/src/Repository/DocumentRepository.php index 163c8268..a135e1e6 100644 --- a/src/Repository/DocumentRepository.php +++ b/src/Repository/DocumentRepository.php @@ -528,8 +528,7 @@ public function countBy( /** * @param NodesSources $nodeSource * @param NodeTypeFieldInterface $field - * @return array - * @deprecated Use findByNodeSourceAndFieldName instead + * @return array */ public function findByNodeSourceAndField( NodesSources $nodeSource, @@ -539,35 +538,10 @@ public function findByNodeSourceAndField( $qb->addSelect('dt') ->leftJoin('d.documentTranslations', 'dt', 'WITH', 'dt.translation = :translation') ->innerJoin('d.nodesSourcesByFields', 'nsf', 'WITH', 'nsf.nodeSource = :nodeSource') - ->andWhere($qb->expr()->eq('nsf.fieldName', ':fieldName')) - ->andWhere($qb->expr()->eq('d.raw', ':raw')) - ->addOrderBy('nsf.position', 'ASC') - ->setParameter('fieldName', $field->getName()) - ->setParameter('nodeSource', $nodeSource) - ->setParameter('translation', $nodeSource->getTranslation()) - ->setParameter('raw', false) - ->setCacheable(true); - - return $qb->getQuery()->getResult(); - } - - /** - * @param NodesSources $nodeSource - * @param string $fieldName - * @return array - */ - public function findByNodeSourceAndFieldName( - NodesSources $nodeSource, - string $fieldName - ): array { - $qb = $this->createQueryBuilder('d'); - $qb->addSelect('dt') - ->leftJoin('d.documentTranslations', 'dt', 'WITH', 'dt.translation = :translation') - ->innerJoin('d.nodesSourcesByFields', 'nsf', 'WITH', 'nsf.nodeSource = :nodeSource') - ->andWhere($qb->expr()->eq('nsf.fieldName', ':fieldName')) + ->andWhere($qb->expr()->eq('nsf.field', ':field')) ->andWhere($qb->expr()->eq('d.raw', ':raw')) ->addOrderBy('nsf.position', 'ASC') - ->setParameter('fieldName', $fieldName) + ->setParameter('field', $field) ->setParameter('nodeSource', $nodeSource) ->setParameter('translation', $nodeSource->getTranslation()) ->setParameter('raw', false) diff --git a/src/Repository/EntityRepository.php b/src/Repository/EntityRepository.php index 1b0e0223..ddf6f510 100644 --- a/src/Repository/EntityRepository.php +++ b/src/Repository/EntityRepository.php @@ -24,7 +24,6 @@ use RZ\Roadiz\CoreBundle\Doctrine\Event\QueryEvent; use RZ\Roadiz\CoreBundle\Doctrine\ORM\SimpleQueryBuilder; use RZ\Roadiz\CoreBundle\Entity\Tag; -use Symfony\Contracts\EventDispatcher\Event; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** @@ -91,7 +90,6 @@ public function __construct(ManagerRegistry $registry, string $entityClass, Even */ protected function dispatchQueryBuilderEvent(QueryBuilder $qb, string $entityClass): void { - // @phpstan-ignore-next-line $this->dispatcher->dispatch(new QueryBuilderSelectEvent($qb, $entityClass)); } @@ -100,11 +98,10 @@ protected function dispatchQueryBuilderEvent(QueryBuilder $qb, string $entityCla * @param string $property * @param mixed $value * - * @return Event + * @return QueryBuilderBuildEvent */ protected function dispatchQueryBuilderBuildEvent(QueryBuilder $qb, string $property, mixed $value): object { - // @phpstan-ignore-next-line return $this->dispatcher->dispatch(new QueryBuilderBuildEvent( $qb, $this->getEntityName(), @@ -117,11 +114,10 @@ protected function dispatchQueryBuilderBuildEvent(QueryBuilder $qb, string $prop /** * @param Query $query * - * @return Event + * @return QueryEvent */ protected function dispatchQueryEvent(Query $query): object { - // @phpstan-ignore-next-line return $this->dispatcher->dispatch(new QueryEvent( $query, $this->getEntityName() @@ -133,11 +129,10 @@ protected function dispatchQueryEvent(Query $query): object * @param string $property * @param mixed $value * - * @return Event + * @return QueryBuilderApplyEvent */ protected function dispatchQueryBuilderApplyEvent(QueryBuilder $qb, string $property, mixed $value): object { - // @phpstan-ignore-next-line return $this->dispatcher->dispatch(new QueryBuilderApplyEvent( $qb, $this->getEntityName(), @@ -262,6 +257,7 @@ public static function getSearchableColumnsNames(ClassMetadataInfo $metadata): a 'childrenOrder', 'childrenOrderDirection', 'password', + 'salt', 'token', 'confirmationToken' ]) @@ -345,13 +341,12 @@ public function searchBy( // Add ordering foreach ($orders as $key => $value) { if ( - (\str_starts_with($key, 'node.') || \str_starts_with($key, static::NODE_ALIAS . '.')) && + str_contains($key, static::NODE_ALIAS . '.') && $this->hasJoinedNode($qb, $alias) ) { - $key = preg_replace('#^node\.#', static::NODE_ALIAS . '.', $key); $qb->addOrderBy($key, $value); } elseif ( - \str_starts_with($key, static::NODESSOURCES_ALIAS . '.') && + str_contains($key, static::NODESSOURCES_ALIAS . '.') && $this->hasJoinedNodesSources($qb, $alias) ) { $qb->addOrderBy($key, $value); @@ -495,7 +490,7 @@ protected function applyFilterByTag(array &$criteria, QueryBuilder $qb): void * * @param QueryBuilder $qb * @param string $alias - * @return bool + * @return boolean */ protected function hasJoinedNode(QueryBuilder $qb, string $alias) { @@ -507,7 +502,7 @@ protected function hasJoinedNode(QueryBuilder $qb, string $alias) * * @param QueryBuilder $qb * @param string $alias - * @return bool + * @return boolean */ protected function hasJoinedNodesSources(QueryBuilder $qb, string $alias) { @@ -519,7 +514,7 @@ protected function hasJoinedNodesSources(QueryBuilder $qb, string $alias) * * @param QueryBuilder $qb * @param string $alias - * @return bool + * @return boolean */ protected function hasJoinedNodeType(QueryBuilder $qb, string $alias) { diff --git a/src/Repository/LogRepository.php b/src/Repository/LogRepository.php index 3419bca8..ac633d1f 100644 --- a/src/Repository/LogRepository.php +++ b/src/Repository/LogRepository.php @@ -5,12 +5,9 @@ namespace RZ\Roadiz\CoreBundle\Repository; use Doctrine\ORM\Query; -use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\Tools\Pagination\Paginator; use Doctrine\Persistence\ManagerRegistry; -use RZ\Roadiz\CoreBundle\Entity\Node; -use RZ\Roadiz\CoreBundle\Entity\NodesSources; -use RZ\Roadiz\CoreBundle\Logger\Entity\Log; +use RZ\Roadiz\CoreBundle\Entity\Log; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** @@ -33,16 +30,30 @@ public function __construct( */ public function findLatestByNodesSources(int $maxResult = 5): Paginator { + /* + * We need to split this query in 2 for performance matter. + * + * SELECT l1_.id, l1_.datetime, n0_.id + * FROM log AS l1_ + * INNER JOIN nodes_sources n0_ ON l1_.node_source_id = n0_.id + * WHERE l1_.id IN ( + * SELECT MAX(id) + * FROM log + * GROUP BY node_source_id + * ) + * ORDER BY l1_.datetime DESC + * LIMIT 8 + */ + $subQb = $this->createQueryBuilder('slog'); $subQb->select($subQb->expr()->max('slog.id')) - ->andWhere($subQb->expr()->in('slog.entityClass', ':entityClass')) - ->addGroupBy('slog.entityId'); + ->addGroupBy('slog.nodeSource'); $qb = $this->createQueryBuilder('log'); $qb->select('log.id as id') + ->innerJoin('log.nodeSource', 'ns') ->andWhere($qb->expr()->in('log.id', $subQb->getQuery()->getDQL())) ->orderBy('log.datetime', 'DESC') - ->setParameter(':entityClass', [NodesSources::class, Node::class]) ->setMaxResults($maxResult) ; $ids = $qb->getQuery() @@ -50,7 +61,11 @@ public function findLatestByNodesSources(int $maxResult = 5): Paginator ->getScalarResult(); $qb2 = $this->createQueryBuilder('log'); - $qb2->andWhere($qb2->expr()->in('log.id', ':id')) + $qb2->addSelect('ns, n, dbf') + ->andWhere($qb2->expr()->in('log.id', ':id')) + ->innerJoin('log.nodeSource', 'ns') + ->leftJoin('ns.documentsByFields', 'dbf') + ->innerJoin('ns.node', 'n') ->orderBy('log.datetime', 'DESC') ->setParameter(':id', array_map(function (array $item) { return $item['id']; @@ -58,33 +73,4 @@ public function findLatestByNodesSources(int $maxResult = 5): Paginator return new Paginator($qb2->getQuery(), true); } - - public function findByNode(Node $node): array - { - $qb = $this->getAllRelatedToNodeQueryBuilder($node); - return $qb->getQuery()->getResult(); - } - - public function getAllRelatedToNodeQueryBuilder(Node $node): QueryBuilder - { - $qb = $this->createQueryBuilder('obj'); - $qb->andWhere($qb->expr()->orX( - $qb->expr()->andX( - $qb->expr()->eq('obj.entityClass', ':nodeClass'), - $qb->expr()->in('obj.entityId', ':nodeId') - ), - $qb->expr()->andX( - $qb->expr()->eq('obj.entityClass', ':nodeSourceClass'), - $qb->expr()->in('obj.entityId', ':nodeSourceId') - ), - )); - $qb->addOrderBy('obj.datetime', 'DESC'); - $qb->setParameter('nodeClass', Node::class); - $qb->setParameter('nodeSourceClass', NodesSources::class); - $qb->setParameter('nodeId', [$node->getId()]); - $qb->setParameter('nodeSourceId', $node->getNodeSources()->map(function (NodesSources $ns) { - return $ns->getId(); - })->toArray()); - return $qb; - } } diff --git a/src/Repository/LoginAttemptRepository.php b/src/Repository/LoginAttemptRepository.php new file mode 100644 index 00000000..377cb35c --- /dev/null +++ b/src/Repository/LoginAttemptRepository.php @@ -0,0 +1,139 @@ + + */ +final class LoginAttemptRepository extends EntityRepository +{ + public function __construct( + ManagerRegistry $registry, + EventDispatcherInterface $dispatcher + ) { + parent::__construct($registry, LoginAttempt::class, $dispatcher); + } + + /** + * @param string $username + * + * @return bool + * @throws \Doctrine\ORM\NoResultException + * @throws \Doctrine\ORM\NonUniqueResultException + */ + public function isUsernameBlocked(string $username): bool + { + $qb = $this->createQueryBuilder('la'); + return $qb->select('COUNT(la)') + ->andWhere($qb->expr()->gte('la.blocksLoginUntil', ':now')) + ->andWhere($qb->expr()->eq('la.username', ':username')) + ->getQuery() + ->setParameters([ + 'now' => new \DateTime('now'), + 'username' => $username, + ]) + ->getSingleScalarResult() > 0 + ; + } + + /** + * Checks if an IP address tries more than 10 usernames + * in the last 5 minutes. + * + * @param string $ipAddress + * @param int $seconds + * @param int $count + * + * @return bool + * @throws \Doctrine\ORM\NoResultException + * @throws \Doctrine\ORM\NonUniqueResultException + */ + public function isIpAddressBlocked(string $ipAddress, int $seconds = 1200, int $count = 10): bool + { + $qb = $this->createQueryBuilder('la'); + $query = $qb->select('SUM(la.attemptCount)') + ->andWhere($qb->expr()->gte('la.date', ':now')) + ->andWhere($qb->expr()->eq('la.ipAddress', ':ipAddress')) + ->getQuery() + ->setParameters([ + 'now' => (new \DateTime())->sub(new \DateInterval('PT' . $seconds . 'S')), + 'ipAddress' => $ipAddress, + ]) + ; + return $query->getSingleScalarResult() > $count ? true : false; + } + + /** + * @param string $ipAddress + * @param string $username + * + * @return LoginAttempt + * @throws \Doctrine\ORM\ORMException + */ + public function findOrCreateOneByIpAddressAndUsername(string $ipAddress, string $username): LoginAttempt + { + /** @var LoginAttempt|null $loginAttempt */ + $loginAttempt = $this->findOneBy([ + 'ipAddress' => $ipAddress, + 'username' => $username, + ]); + if (null === $loginAttempt) { + $loginAttempt = new LoginAttempt($ipAddress, $username); + $this->_em->persist($loginAttempt); + } + + return $loginAttempt; + } + + /** + * @param string $ipAddress + * @param string $username + */ + public function resetLoginAttempts(string $ipAddress, string $username): void + { + $qb = $this->_em->createQueryBuilder(); + $qb->delete(LoginAttempt::class, 'la') + ->andWhere($qb->expr()->eq('la.ipAddress', ':ipAddress')) + ->andWhere($qb->expr()->eq('la.username', ':username')) + ->getQuery() + ->execute([ + 'username' => $username, + 'ipAddress' => $ipAddress, + ]) + ; + } + + /** + * @param string $ipAddress + */ + public function purgeLoginAttempts(string $ipAddress): void + { + $qb = $this->_em->createQueryBuilder(); + $qb->delete(LoginAttempt::class, 'la') + ->andWhere($qb->expr()->eq('la.ipAddress', ':ipAddress')) + ->getQuery() + ->execute([ + 'ipAddress' => $ipAddress, + ]) + ; + } + + public function cleanLoginAttempts(): void + { + $qb = $this->_em->createQueryBuilder(); + $qb->delete(LoginAttempt::class, 'la') + ->andWhere($qb->expr()->lte('la.blocksLoginUntil', ':date')) + ->getQuery() + ->execute([ + 'date' => (new \DateTime())->sub(new \DateInterval('P1D')), + ]) + ; + } +} diff --git a/src/Repository/NodeRepository.php b/src/Repository/NodeRepository.php index 441ade0f..33dfcfa2 100644 --- a/src/Repository/NodeRepository.php +++ b/src/Repository/NodeRepository.php @@ -20,10 +20,10 @@ use RZ\Roadiz\CoreBundle\Doctrine\ORM\SimpleQueryBuilder; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\NodesSources; +use RZ\Roadiz\CoreBundle\Entity\NodeTypeField; +use RZ\Roadiz\CoreBundle\Entity\UrlAlias; use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; -use Symfony\Bundle\SecurityBundle\Security; -use Symfony\Component\String\Slugger\AsciiSlugger; -use Symfony\Contracts\EventDispatcher\Event; +use Symfony\Component\Security\Core\Security; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** @@ -45,11 +45,10 @@ public function __construct( * @param string $property * @param mixed $value * - * @return Event + * @return object|QueryBuilderBuildEvent */ protected function dispatchQueryBuilderBuildEvent(QueryBuilder $qb, string $property, mixed $value): object { - // @phpstan-ignore-next-line return $this->dispatcher->dispatch( new QueryBuilderBuildEvent($qb, Node::class, $property, $value, $this->getEntityName()) ); @@ -60,11 +59,10 @@ protected function dispatchQueryBuilderBuildEvent(QueryBuilder $qb, string $prop * @param string $property * @param mixed $value * - * @return Event + * @return object|QueryBuilderApplyEvent */ protected function dispatchQueryBuilderApplyEvent(QueryBuilder $qb, string $property, mixed $value): object { - // @phpstan-ignore-next-line return $this->dispatcher->dispatch( new QueryBuilderApplyEvent($qb, Node::class, $property, $value, $this->getEntityName()) ); @@ -330,7 +328,6 @@ public function findBy( $this->applyFilterByTag($criteria, $qb); $this->applyFilterByCriteria($criteria, $qb); $this->applyTranslationByTag($qb, $translation); - // @phpstan-ignore-next-line $query = $qb->getQuery(); $this->dispatchQueryEvent($query); @@ -454,7 +451,6 @@ public function findOneBy( $this->applyFilterByTag($criteria, $qb); $this->applyFilterByCriteria($criteria, $qb); $this->applyTranslationByTag($qb, $translation); - // @phpstan-ignore-next-line $query = $qb->getQuery(); $this->dispatchQueryEvent($query); @@ -809,14 +805,14 @@ public function findByNodeAndField( $qb = $this->createQueryBuilder(self::NODE_ALIAS); $qb->select(self::NODE_ALIAS) ->innerJoin('n.aNodes', 'ntn') - ->andWhere($qb->expr()->eq('ntn.fieldName', ':fieldName')) + ->andWhere($qb->expr()->eq('ntn.field', ':field')) ->andWhere($qb->expr()->eq('ntn.nodeA', ':nodeA')) ->addOrderBy('ntn.position', 'ASC') ->setCacheable(true); $this->alterQueryBuilderWithAuthorizationChecker($qb); - $qb->setParameter('fieldName', $field->getName()) + $qb->setParameter('field', $field) ->setParameter('nodeA', $node); return $qb->getQuery()->getResult(); @@ -837,7 +833,7 @@ public function findByNodeAndFieldAndTranslation( $qb->select('n, ns') ->innerJoin('n.aNodes', 'ntn') ->innerJoin('n.nodeSources', self::NODESSOURCES_ALIAS) - ->andWhere($qb->expr()->eq('ntn.fieldName', ':fieldName')) + ->andWhere($qb->expr()->eq('ntn.field', ':field')) ->andWhere($qb->expr()->eq('ntn.nodeA', ':nodeA')) ->andWhere($qb->expr()->eq('ns.translation', ':translation')) ->addOrderBy('ntn.position', 'ASC') @@ -845,7 +841,7 @@ public function findByNodeAndFieldAndTranslation( $this->alterQueryBuilderWithAuthorizationChecker($qb); - $qb->setParameter('fieldName', $field->getName()) + $qb->setParameter('field', $field) ->setParameter('nodeA', $node) ->setParameter('translation', $translation); @@ -864,14 +860,14 @@ public function findByReverseNodeAndField( $qb = $this->createQueryBuilder(self::NODE_ALIAS); $qb->select(self::NODE_ALIAS) ->innerJoin('n.bNodes', 'ntn') - ->andWhere($qb->expr()->eq('ntn.fieldName', ':fieldName')) + ->andWhere($qb->expr()->eq('ntn.field', ':field')) ->andWhere($qb->expr()->eq('ntn.nodeB', ':nodeB')) ->addOrderBy('ntn.position', 'ASC') ->setCacheable(true); $this->alterQueryBuilderWithAuthorizationChecker($qb); - $qb->setParameter('fieldName', $field->getName()) + $qb->setParameter('field', $field) ->setParameter('nodeB', $node); return $qb->getQuery()->getResult(); @@ -892,7 +888,7 @@ public function findByReverseNodeAndFieldAndTranslation( $qb->select('n, ns') ->innerJoin('n.bNodes', 'ntn') ->innerJoin('n.nodeSources', self::NODESSOURCES_ALIAS) - ->andWhere($qb->expr()->eq('ntn.fieldName', ':fieldName')) + ->andWhere($qb->expr()->eq('ntn.field', ':field')) ->andWhere($qb->expr()->eq('ns.translation', ':translation')) ->andWhere($qb->expr()->eq('ntn.nodeB', ':nodeB')) ->addOrderBy('ntn.position', 'ASC') @@ -900,7 +896,7 @@ public function findByReverseNodeAndFieldAndTranslation( $this->alterQueryBuilderWithAuthorizationChecker($qb); - $qb->setParameter('fieldName', $field->getName()) + $qb->setParameter('field', $field) ->setParameter('translation', $translation) ->setParameter('nodeB', $node); @@ -909,16 +905,15 @@ public function findByReverseNodeAndFieldAndTranslation( /** * @param Node $node - * @return array - * @internal Use NodeOffspringResolverInterface service instead + * @return array */ - public function findAllOffspringIdByNode(Node $node): array + public function findAllOffspringIdByNode(Node $node) { - $theOffsprings = []; - $in = \array_filter([(int) $node->getId()]); + $theOffprings = []; + $in = [$node->getId()]; do { - $theOffsprings = array_merge($theOffsprings, $in); + $theOffprings = array_merge($theOffprings, $in); $subQb = $this->createQueryBuilder('n'); $subQb->select('n.id') ->andWhere($subQb->expr()->in('n.parent', ':tab')) @@ -932,7 +927,7 @@ public function findAllOffspringIdByNode(Node $node): array $in[] = (int) $item['id']; } } while (!empty($in)); - return $theOffsprings; + return $theOffprings; } /** @@ -970,10 +965,6 @@ public function findAllNodeParentsBy( ); } - /** - * @param Node $node - * @return array - */ public function findAllParentsIdByNode(Node $node): array { $theParents = []; @@ -984,7 +975,7 @@ public function findAllParentsIdByNode(Node $node): array $parent = $parent->getParent(); } - return array_filter($theParents); + return $theParents; } /** @@ -1116,122 +1107,4 @@ public function findLatestPositionInParent(Node $parent = null): int return (int) $qb->getQuery()->getSingleScalarResult(); } - - /** - * Use by UniqueEntity Validator to bypass node status query filtering. - * - * @param array $criteria - * @return Node|null - * @throws NonUniqueResultException - */ - public function findOneWithoutSecurity(array $criteria): ?Node - { - $this->setDisplayingAllNodesStatuses(true); - if (count($criteria) === 1 && !empty($criteria['nodeName'])) { - /* - * Test if nodeName is used as an url-alias too - */ - $nodeName = (new AsciiSlugger())->slug($criteria['nodeName'])->lower()->trim()->toString(); - - $qb = $this->createQueryBuilder('o'); - $qb->leftJoin('o.nodeSources', 'ns') - ->leftJoin('ns.urlAliases', 'ua') - ->andWhere($qb->expr()->orX( - $qb->expr()->eq('ua.alias', ':nodeName'), - $qb->expr()->eq('o.nodeName', ':nodeName') - )) - ->setParameter('nodeName', $nodeName) - ->setMaxResults(1) - ->setCacheable(true); - ; - return $qb->getQuery()->getOneOrNullResult(); - } - return $this->findOneBy($criteria); - } - - protected function classicLikeComparison( - string $pattern, - QueryBuilder $qb, - string $alias = EntityRepository::DEFAULT_ALIAS - ): QueryBuilder { - $qb = parent::classicLikeComparison($pattern, $qb, $alias); - $qb - ->leftJoin($alias . '.attributeValues', 'av') - ->leftJoin('av.attributeValueTranslations', 'avt') - ; - $value = '%' . strip_tags(\mb_strtolower($pattern)) . '%'; - $qb->orWhere($qb->expr()->like('LOWER(avt.value)', $qb->expr()->literal($value))); - $qb->orWhere($qb->expr()->like('LOWER(' . $alias . '.nodeName)', $qb->expr()->literal($value))); - return $qb; - } - - /** - * Get previous node from hierarchy - */ - public function findPreviousNode( - Node $node, - ?array $criteria = null, - ?array $order = null - ): ?Node { - if ($node->getPosition() <= 1) { - return null; - } - if (null === $order) { - $order = []; - } - - if (null === $criteria) { - $criteria = []; - } - - $criteria['parent'] = $node->getParent(); - /* - * Use < operator to get first previous nodeSource - * even if it’s not the previous position index - */ - $criteria['position'] = [ - '<', - $node->getPosition(), - ]; - - $order['position'] = 'DESC'; - - return $this->findOneBy( - $criteria, - $order - ); - } - - /** - * Get next node from hierarchy. - */ - public function findNextNode( - Node $node, - ?array $criteria = null, - ?array $order = null - ): ?Node { - if (null === $criteria) { - $criteria = []; - } - if (null === $order) { - $order = []; - } - - $criteria['parent'] = $node->getParent(); - - /* - * Use > operator to get first next nodeSource - * even if it’s not the next position index - */ - $criteria['position'] = [ - '>', - $node->getPosition(), - ]; - $order['position'] = 'ASC'; - - return $this->findOneBy( - $criteria, - $order - ); - } } diff --git a/src/Repository/NodesCustomFormsRepository.php b/src/Repository/NodesCustomFormsRepository.php index 112e0e90..dc41cec0 100644 --- a/src/Repository/NodesCustomFormsRepository.php +++ b/src/Repository/NodesCustomFormsRepository.php @@ -5,7 +5,6 @@ namespace RZ\Roadiz\CoreBundle\Repository; use Doctrine\Persistence\ManagerRegistry; -use RZ\Roadiz\Contracts\NodeType\NodeTypeFieldInterface; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\NodesCustomForms; use RZ\Roadiz\CoreBundle\Entity\NodeTypeField; @@ -23,38 +22,20 @@ public function __construct( ) { parent::__construct($registry, NodesCustomForms::class, $dispatcher); } - /** - * @param Node $node - * @param NodeTypeFieldInterface $field - * @return int - * @throws \Doctrine\ORM\NoResultException - * @throws \Doctrine\ORM\NonUniqueResultException - * @deprecated Use getLatestPositionForFieldName instead + * @param Node $node + * @param NodeTypeField $field + * + * @return integer */ - public function getLatestPosition(Node $node, NodeTypeFieldInterface $field): int - { - $query = $this->_em->createQuery(' - SELECT MAX(ncf.position) FROM RZ\Roadiz\CoreBundle\Entity\NodesCustomForms ncf - WHERE ncf.node = :node AND ncf.fieldName = :fieldName') - ->setParameter('node', $node) - ->setParameter('fieldName', $field->getName()); - - $latestPosition = $query->getSingleScalarResult(); - - return is_numeric($latestPosition) ? (int) $latestPosition : 0; - } - - public function getLatestPositionForFieldName(Node $node, string $fieldName): int + public function getLatestPosition(Node $node, NodeTypeField $field) { $query = $this->_em->createQuery(' SELECT MAX(ncf.position) FROM RZ\Roadiz\CoreBundle\Entity\NodesCustomForms ncf - WHERE ncf.node = :node AND ncf.fieldName = :fieldName') + WHERE ncf.node = :node AND ncf.field = :field') ->setParameter('node', $node) - ->setParameter('fieldName', $fieldName); - - $latestPosition = $query->getSingleScalarResult(); + ->setParameter('field', $field); - return is_numeric($latestPosition) ? (int) $latestPosition : 0; + return (int) $query->getSingleScalarResult(); } } diff --git a/src/Repository/NodesSourcesDocumentsRepository.php b/src/Repository/NodesSourcesDocumentsRepository.php index 3cc0ea1c..0c24dc5d 100644 --- a/src/Repository/NodesSourcesDocumentsRepository.php +++ b/src/Repository/NodesSourcesDocumentsRepository.php @@ -7,7 +7,6 @@ use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; use Doctrine\Persistence\ManagerRegistry; -use RZ\Roadiz\Contracts\NodeType\NodeTypeFieldInterface; use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\NodesSourcesDocuments; use RZ\Roadiz\CoreBundle\Entity\NodeTypeField; @@ -27,35 +26,19 @@ public function __construct( /** * @param NodesSources $nodeSource - * @param NodeTypeFieldInterface $field + * @param NodeTypeField $field * @return int * @throws NoResultException * @throws NonUniqueResultException - * @deprecated Use getLatestPositionForFieldName instead */ - public function getLatestPosition(NodesSources $nodeSource, NodeTypeFieldInterface $field): int + public function getLatestPosition(NodesSources $nodeSource, NodeTypeField $field): int { $query = $this->_em->createQuery(' SELECT MAX(nsd.position) FROM RZ\Roadiz\CoreBundle\Entity\NodesSourcesDocuments nsd - WHERE nsd.nodeSource = :nodeSource AND nsd.fieldName = :fieldName') + WHERE nsd.nodeSource = :nodeSource AND nsd.field = :field') ->setParameter('nodeSource', $nodeSource) - ->setParameter('fieldName', $field->getName()); + ->setParameter('field', $field); - $latestPosition = $query->getSingleScalarResult(); - - return is_numeric($latestPosition) ? (int) $latestPosition : 0; - } - - public function getLatestPositionForFieldName(NodesSources $nodeSource, string $fieldName): int - { - $query = $this->_em->createQuery(' - SELECT MAX(nsd.position) FROM RZ\Roadiz\CoreBundle\Entity\NodesSourcesDocuments nsd - WHERE nsd.nodeSource = :nodeSource AND nsd.fieldName = :fieldName') - ->setParameter('nodeSource', $nodeSource) - ->setParameter('fieldName', $fieldName); - - $latestPosition = $query->getSingleScalarResult(); - - return is_numeric($latestPosition) ? (int) $latestPosition : 0; + return (int) $query->getSingleScalarResult(); } } diff --git a/src/Repository/NodesSourcesRepository.php b/src/Repository/NodesSourcesRepository.php index 088a52cd..dfe4b3d3 100644 --- a/src/Repository/NodesSourcesRepository.php +++ b/src/Repository/NodesSourcesRepository.php @@ -4,7 +4,6 @@ namespace RZ\Roadiz\CoreBundle\Repository; -use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\Tools\Pagination\Paginator; @@ -15,25 +14,24 @@ use RZ\Roadiz\CoreBundle\Doctrine\Event\QueryBuilder\QueryBuilderNodesSourcesBuildEvent; use RZ\Roadiz\CoreBundle\Doctrine\Event\QueryNodesSourcesEvent; use RZ\Roadiz\CoreBundle\Doctrine\ORM\SimpleQueryBuilder; +use RZ\Roadiz\CoreBundle\Entity\Log; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\NodesSources; +use RZ\Roadiz\CoreBundle\Entity\NodeTypeField; use RZ\Roadiz\CoreBundle\Exception\SolrServerNotAvailableException; -use RZ\Roadiz\CoreBundle\Logger\Entity\Log; use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; use RZ\Roadiz\CoreBundle\SearchEngine\NodeSourceSearchHandlerInterface; use RZ\Roadiz\CoreBundle\SearchEngine\SearchResultsInterface; -use RZ\Roadiz\CoreBundle\SearchEngine\SolrSearchResultItem; use RZ\Roadiz\CoreBundle\SearchEngine\SolrSearchResults; -use Symfony\Bundle\SecurityBundle\Security; -use Symfony\Contracts\EventDispatcher\Event; +use Symfony\Component\Security\Core\Security; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * EntityRepository that implements search engine query with Solr. * * @template T of NodesSources - * @extends StatusAwareRepository - * @template-extends StatusAwareRepository + * @extends StatusAwareRepository + * @template-extends StatusAwareRepository */ class NodesSourcesRepository extends StatusAwareRepository { @@ -62,11 +60,10 @@ public function __construct( * @param string $property * @param mixed $value * - * @return Event + * @return object|QueryBuilderNodesSourcesBuildEvent */ protected function dispatchQueryBuilderBuildEvent(QueryBuilder $qb, string $property, mixed $value): object { - // @phpstan-ignore-next-line return $this->dispatcher->dispatch( new QueryBuilderNodesSourcesBuildEvent($qb, $property, $value, $this->getEntityName()) ); @@ -77,11 +74,10 @@ protected function dispatchQueryBuilderBuildEvent(QueryBuilder $qb, string $prop * @param string $property * @param mixed $value * - * @return Event + * @return object|QueryBuilderNodesSourcesApplyEvent */ protected function dispatchQueryBuilderApplyEvent(QueryBuilder $qb, string $property, mixed $value): object { - // @phpstan-ignore-next-line return $this->dispatcher->dispatch( new QueryBuilderNodesSourcesApplyEvent($qb, $property, $value, $this->getEntityName()) ); @@ -90,11 +86,10 @@ protected function dispatchQueryBuilderApplyEvent(QueryBuilder $qb, string $prop /** * @param Query $query * - * @return Event + * @return object|QueryNodesSourcesEvent */ protected function dispatchQueryEvent(Query $query): object { - // @phpstan-ignore-next-line return $this->dispatcher->dispatch( new QueryNodesSourcesEvent($query, $this->getEntityName()) ); @@ -152,10 +147,9 @@ protected function filterByCriteria( if ($key == "tags" || $key == "tagExclusive") { continue; } - /** + /* * Main QueryBuilder dispatch loop for * custom properties criteria. - * @var QueryBuilderNodesSourcesBuildEvent $event */ $event = $this->dispatchQueryBuilderBuildEvent($qb, $key, $value); @@ -189,7 +183,6 @@ protected function applyFilterByCriteria(array &$criteria, QueryBuilder $qb): vo continue; } - /** @var QueryBuilderNodesSourcesApplyEvent $event */ $event = $this->dispatchQueryBuilderApplyEvent($qb, $key, $value); if (!$event->isPropagationStopped()) { $simpleQB->bindValue($key, $value); @@ -219,17 +212,29 @@ public function alterQueryBuilderWithAuthorizationChecker( * Forbid deleted node for backend user when authorizationChecker not null. */ if (!$this->hasJoinedNode($qb, $prefix)) { - $qb->innerJoin($prefix . '.node', static::NODE_ALIAS); + $qb->innerJoin( + $prefix . '.node', + static::NODE_ALIAS, + 'WITH', + $qb->expr()->lte(static::NODE_ALIAS . '.status', Node::PUBLISHED) + ); + } else { + $qb->andWhere($qb->expr()->lte(static::NODE_ALIAS . '.status', Node::PUBLISHED)); } - $qb->andWhere($qb->expr()->lte(static::NODE_ALIAS . '.status', Node::PUBLISHED)); } else { /* * Forbid unpublished node for anonymous and not backend users. */ if (!$this->hasJoinedNode($qb, $prefix)) { - $qb->innerJoin($prefix . '.node', static::NODE_ALIAS); + $qb->innerJoin( + $prefix . '.node', + static::NODE_ALIAS, + 'WITH', + $qb->expr()->eq(static::NODE_ALIAS . '.status', Node::PUBLISHED) + ); + } else { + $qb->andWhere($qb->expr()->eq(static::NODE_ALIAS . '.status', Node::PUBLISHED)); } - $qb->andWhere($qb->expr()->eq(static::NODE_ALIAS . '.status', Node::PUBLISHED)); } return $qb; } @@ -240,8 +245,8 @@ public function alterQueryBuilderWithAuthorizationChecker( * * @param array $criteria * @param array|null $orderBy - * @param int|null $limit - * @param int|null $offset + * @param integer|null $limit + * @param integer|null $offset * @return QueryBuilder */ protected function getContextualQuery( @@ -249,7 +254,7 @@ protected function getContextualQuery( array $orderBy = null, $limit = null, $offset = null - ): QueryBuilder { + ) { $qb = $this->createQueryBuilder(static::NODESSOURCES_ALIAS); $this->alterQueryBuilderWithAuthorizationChecker($qb, static::NODESSOURCES_ALIAS); $qb->addSelect(static::NODE_ALIAS); @@ -262,7 +267,7 @@ protected function getContextualQuery( // Add ordering if (null !== $orderBy) { foreach ($orderBy as $key => $value) { - if (\str_contains($key, 'node.')) { + if (false !== \mb_strpos($key, 'node.')) { $simpleKey = str_replace('node.', '', $key); $qb->addOrderBy(static::NODE_ALIAS . '.' . $simpleKey, $value); } else { @@ -352,7 +357,7 @@ public function countBy(mixed $criteria): int * @param array|null $orderBy * @param int|null $limit * @param int|null $offset - * @return array|Paginator + * @return array|Paginator */ public function findBy( array $criteria, @@ -407,7 +412,7 @@ public function findBy( public function findOneBy( array $criteria, array $orderBy = null - ): ?NodesSources { + ) { $qb = $this->getContextualQuery( $criteria, $orderBy, @@ -436,7 +441,7 @@ public function findOneBy( * * @param string $query Solr query string (for example: `text:Lorem Ipsum`) * @param int $limit Result number to fetch (default: all) - * @return array> + * @return array */ public function findBySearchQuery(string $query, int $limit = 25): array { @@ -516,6 +521,7 @@ public function findByTextQuery( ->andWhere($qb->expr()->orX( $qb->expr()->like(static::NODESSOURCES_ALIAS . '.title', ':query'), $qb->expr()->like(static::NODESSOURCES_ALIAS . '.metaTitle', ':query'), + $qb->expr()->like(static::NODESSOURCES_ALIAS . '.metaKeywords', ':query'), $qb->expr()->like(static::NODESSOURCES_ALIAS . '.metaDescription', ':query') )) ->orderBy(static::NODESSOURCES_ALIAS . '.title', 'ASC') @@ -557,20 +563,18 @@ public function findByTextQuery( * @param int $maxResult * @return Paginator */ - public function findByLatestUpdated(int $maxResult = 5): Paginator + public function findByLatestUpdated($maxResult = 5) { $subQuery = $this->_em->createQueryBuilder(); - $subQuery->select('slog.entityId') + $subQuery->select('sns.id') ->from(Log::class, 'slog') - ->andWhere($subQuery->expr()->eq('slog.entityClass', ':entityClass')) + ->innerJoin(NodesSources::class, 'sns') + ->andWhere($subQuery->expr()->isNotNull('slog.nodeSource')) ->orderBy('slog.datetime', 'DESC'); $query = $this->createQueryBuilder(static::NODESSOURCES_ALIAS); - $query - ->andWhere($query->expr()->in(static::NODESSOURCES_ALIAS . '.id', $subQuery->getQuery()->getDQL())) - ->setParameter(':entityClass', NodesSources::class) - ->setMaxResults($maxResult) - ; + $query->andWhere($query->expr()->in(static::NODESSOURCES_ALIAS . '.id', $subQuery->getQuery()->getDQL())); + $query->setMaxResults($maxResult); return new Paginator($query->getQuery()); } @@ -642,7 +646,7 @@ protected function prepareComparisons(array &$criteria, QueryBuilder $qb, $alias if (!$event->isPropagationStopped()) { $baseKey = $simpleQB->getParameterKey($key); - if (\str_contains($key, 'node.nodeType.')) { + if (false !== \mb_strpos($key, 'node.nodeType.')) { if (!$this->hasJoinedNode($qb, $alias)) { $qb->innerJoin($alias . '.node', static::NODE_ALIAS); } @@ -652,7 +656,7 @@ protected function prepareComparisons(array &$criteria, QueryBuilder $qb, $alias $prefix = static::NODETYPE_ALIAS . '.'; $simpleKey = str_replace('node.nodeType.', '', $key); $qb->andWhere($simpleQB->buildExpressionWithoutBinding($value, $prefix, $simpleKey, $baseKey)); - } elseif (\str_contains($key, 'node.')) { + } elseif (false !== \mb_strpos($key, 'node.')) { if (!$this->hasJoinedNode($qb, $alias)) { $qb->innerJoin($alias . '.node', static::NODE_ALIAS); } @@ -700,7 +704,7 @@ public function searchBy( /** * @param NodesSources $nodesSources * @param NodeTypeFieldInterface $field - * @deprecated Use findByNodesSourcesAndFieldNameAndTranslation instead + * * @return array|null */ public function findByNodesSourcesAndFieldAndTranslation( @@ -712,37 +716,7 @@ public function findByNodesSourcesAndFieldAndTranslation( ->innerJoin('ns.node', static::NODE_ALIAS) ->leftJoin('ns.urlAliases', 'ua') ->innerJoin('n.aNodes', 'ntn') - ->andWhere($qb->expr()->eq('ntn.fieldName', ':fieldName')) - ->andWhere($qb->expr()->eq('ntn.nodeA', ':nodeA')) - ->andWhere($qb->expr()->eq('ns.translation', ':translation')) - ->addOrderBy('ntn.position', 'ASC') - ->setCacheable(true); - - $this->alterQueryBuilderWithAuthorizationChecker($qb); - - $qb->setParameter('fieldName', $field->getName()) - ->setParameter('nodeA', $nodesSources->getNode()) - ->setParameter('translation', $nodesSources->getTranslation()); - - return $qb->getQuery()->getResult(); - } - - /** - * @param NodesSources $nodesSources - * @param string $fieldName - * - * @return array|null - */ - public function findByNodesSourcesAndFieldNameAndTranslation( - NodesSources $nodesSources, - string $fieldName - ): ?array { - $qb = $this->createQueryBuilder(static::NODESSOURCES_ALIAS); - $qb->select('ns, n, ua') - ->innerJoin('ns.node', static::NODE_ALIAS) - ->leftJoin('ns.urlAliases', 'ua') - ->innerJoin('n.aNodes', 'ntn') - ->andWhere($qb->expr()->eq('ntn.fieldName', ':fieldName')) + ->andWhere($qb->expr()->eq('ntn.field', ':field')) ->andWhere($qb->expr()->eq('ntn.nodeA', ':nodeA')) ->andWhere($qb->expr()->eq('ns.translation', ':translation')) ->addOrderBy('ntn.position', 'ASC') @@ -750,7 +724,7 @@ public function findByNodesSourcesAndFieldNameAndTranslation( $this->alterQueryBuilderWithAuthorizationChecker($qb); - $qb->setParameter('fieldName', $fieldName) + $qb->setParameter('field', $field) ->setParameter('nodeA', $nodesSources->getNode()) ->setParameter('translation', $nodesSources->getTranslation()); @@ -778,319 +752,4 @@ public function findByNode(Node $node): array return $qb->getQuery()->getResult(); } - - protected function classicLikeComparison( - string $pattern, - QueryBuilder $qb, - string $alias = EntityRepository::DEFAULT_ALIAS - ): QueryBuilder { - $qb = parent::classicLikeComparison($pattern, $qb, $alias); - $qb - ->innerJoin($alias . '.node', static::NODE_ALIAS) - ->leftJoin(static::NODE_ALIAS . '.attributeValues', 'av') - ->leftJoin('av.attributeValueTranslations', 'avt') - ; - $value = '%' . strip_tags(\mb_strtolower($pattern)) . '%'; - $qb->orWhere($qb->expr()->like('LOWER(avt.value)', $qb->expr()->literal($value))); - $qb->orWhere($qb->expr()->like('LOWER(' . static::NODE_ALIAS . '.nodeName)', $qb->expr()->literal($value))); - return $qb; - } - - /** - * Get every nodeSources parents from direct parent to farthest ancestor. - * - * @param NodesSources $nodeSource - * @param array|null $criteria - * @return array - * @throws \Doctrine\ORM\NonUniqueResultException - */ - public function findParents( - NodesSources $nodeSource, - ?array $criteria = null - ): array { - $parentsNodeSources = []; - - if (null === $criteria) { - $criteria = []; - } - - $parent = $nodeSource; - - while (null !== $parent) { - $criteria = array_merge( - $criteria, - [ - 'node' => $parent->getNode()->getParent(), - 'translation' => $nodeSource->getTranslation(), - ] - ); - $currentParent = $this->findOneBy( - $criteria, - [] - ); - - if (null !== $currentParent) { - $parentsNodeSources[] = $currentParent; - } - - $parent = $currentParent; - } - - return $parentsNodeSources; - } - - /** - * Get children nodes sources to lock with current translation. - * - * @param NodesSources $nodeSource - * @param array|null $criteria Additional criteria - * @param array|null $order Non default ordering - * - * @return Paginator|array - */ - public function findChildren( - NodesSources $nodeSource, - array $criteria = null, - array $order = null - ): Paginator|array { - $defaultCriteria = [ - 'node.parent' => $nodeSource->getNode(), - 'translation' => $nodeSource->getTranslation(), - ]; - - if (null !== $order) { - $defaultOrder = $order; - } else { - $defaultOrder = [ - 'node.position' => 'ASC', - ]; - } - - if (null !== $criteria) { - $defaultCriteria = array_merge($defaultCriteria, $criteria); - } - - return $this->findBy( - $defaultCriteria, - $defaultOrder - ); - } - - /** - * Get first node-source among current node-source children. - * - * @param NodesSources|null $nodeSource - * @param array|null $criteria - * @param array|null $order - * - * @return NodesSources|null - * @throws NonUniqueResultException - */ - public function findFirstChild( - ?NodesSources $nodeSource, - array $criteria = null, - array $order = null - ): ?NodesSources { - $defaultCriteria = [ - 'node.parent' => $nodeSource?->getNode() ?? null, - ]; - - if (null !== $nodeSource) { - $defaultCriteria['translation'] = $nodeSource->getTranslation(); - } - - if (null !== $order) { - $defaultOrder = $order; - } else { - $defaultOrder = [ - 'node.position' => 'ASC', - ]; - } - - if (null !== $criteria) { - $defaultCriteria = array_merge($defaultCriteria, $criteria); - } - - return $this->findOneBy( - $defaultCriteria, - $defaultOrder - ); - } - - /** - * Get last node-source among current node-source children. - * - * @param NodesSources|null $nodeSource - * @param array|null $criteria - * @param array|null $order - * - * @return NodesSources|null - * @throws NonUniqueResultException - */ - public function findLastChild( - ?NodesSources $nodeSource, - array $criteria = null, - array $order = null - ): ?NodesSources { - $defaultCriteria = [ - 'node.parent' => $nodeSource?->getNode() ?? null, - ]; - - if (null !== $nodeSource) { - $defaultCriteria['translation'] = $nodeSource->getTranslation(); - } - - if (null !== $order) { - $defaultOrder = $order; - } else { - $defaultOrder = [ - 'node.position' => 'DESC', - ]; - } - - if (null !== $criteria) { - $defaultCriteria = array_merge($defaultCriteria, $criteria); - } - - return $this->findOneBy( - $defaultCriteria, - $defaultOrder - ); - } - - /** - * Get first node-source in the same parent as current node-source. - * - * @param NodesSources $nodeSource - * @param array|null $criteria - * @param array|null $order - * - * @return NodesSources|null - * @throws NonUniqueResultException - */ - public function findFirstSibling( - NodesSources $nodeSource, - array $criteria = null, - array $order = null - ): ?NodesSources { - if (null !== $nodeSource->getParent()) { - return $this->findFirstChild($nodeSource->getParent(), $criteria, $order); - } - return $this->findFirstChild(null, $criteria, $order); - } - - /** - * Get last node-source in the same parent as current node-source. - * - * @param NodesSources $nodeSource - * @param array|null $criteria - * @param array|null $order - * - * @return NodesSources|null - * @throws NonUniqueResultException - */ - public function findLastSibling( - NodesSources $nodeSource, - array $criteria = null, - array $order = null - ): ?NodesSources { - if (null !== $nodeSource->getParent()) { - return $this->findLastChild($nodeSource->getParent(), $criteria, $order); - } - return $this->findLastChild(null, $criteria, $order); - } - - /** - * Get previous node-source from hierarchy. - * - * @param NodesSources $nodeSource - * @param array|null $criteria - * @param array|null $order - * - * @return NodesSources|null - * @throws NonUniqueResultException - */ - public function findPrevious( - NodesSources $nodeSource, - array $criteria = null, - array $order = null - ): ?NodesSources { - if ($nodeSource->getNode()->getPosition() <= 1) { - return null; - } - - $defaultCriteria = [ - /* - * Use < operator to get first next nodeSource - * even if it’s not the next position index - */ - 'node.position' => [ - '<', - $nodeSource - ->getNode() - ->getPosition(), - ], - 'node.parent' => $nodeSource->getNode()->getParent(), - 'translation' => $nodeSource->getTranslation(), - ]; - if (null !== $criteria) { - $defaultCriteria = array_merge($defaultCriteria, $criteria); - } - - if (null === $order) { - $order = []; - } - - $order['node.position'] = 'DESC'; - - return $this->findOneBy( - $defaultCriteria, - $order - ); - } - - /** - * Get next node-source from hierarchy. - * - * @param NodesSources $nodeSource - * @param array|null $criteria - * @param array|null $order - * - * @return NodesSources|null - * @throws NonUniqueResultException - */ - public function findNext( - NodesSources $nodeSource, - array $criteria = null, - array $order = null - ): ?NodesSources { - $defaultCriteria = [ - /* - * Use > operator to get first next nodeSource - * even if it’s not the next position index - */ - 'node.position' => [ - '>', - $nodeSource - ->getNode() - ->getPosition(), - ], - 'node.parent' => $nodeSource->getNode()->getParent(), - 'translation' => $nodeSource->getTranslation(), - ]; - if (null !== $criteria) { - $defaultCriteria = array_merge($defaultCriteria, $criteria); - } - - if (null === $order) { - $order = []; - } - - $order['node.position'] = 'ASC'; - - return $this->findOneBy( - $defaultCriteria, - $order - ); - } } diff --git a/src/Repository/NodesToNodesRepository.php b/src/Repository/NodesToNodesRepository.php index d23079d9..a64042a3 100644 --- a/src/Repository/NodesToNodesRepository.php +++ b/src/Repository/NodesToNodesRepository.php @@ -7,7 +7,6 @@ use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; use Doctrine\Persistence\ManagerRegistry; -use RZ\Roadiz\Contracts\NodeType\NodeTypeFieldInterface; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\NodesToNodes; use RZ\Roadiz\CoreBundle\Entity\NodeTypeField; @@ -28,35 +27,20 @@ public function __construct( /** * @param Node $node - * @param NodeTypeFieldInterface $field - * @return int + * @param NodeTypeField $field + * + * @return integer * @throws NoResultException * @throws NonUniqueResultException - * @deprecated Use getLatestPositionForFieldName instead */ - public function getLatestPosition(Node $node, NodeTypeFieldInterface $field): int + public function getLatestPosition(Node $node, NodeTypeField $field): int { $query = $this->_em->createQuery(' SELECT MAX(ntn.position) FROM RZ\Roadiz\CoreBundle\Entity\NodesToNodes ntn - WHERE ntn.nodeA = :nodeA AND ntn.fieldName = :fieldName') + WHERE ntn.nodeA = :nodeA AND ntn.field = :field') ->setParameter('nodeA', $node) - ->setParameter('fieldName', $field->getName()); + ->setParameter('field', $field); - $latestPosition = $query->getSingleScalarResult(); - - return is_numeric($latestPosition) ? (int) $latestPosition : 0; - } - - public function getLatestPositionForFieldName(Node $node, string $fieldName): int - { - $query = $this->_em->createQuery(' - SELECT MAX(ntn.position) FROM RZ\Roadiz\CoreBundle\Entity\NodesToNodes ntn - WHERE ntn.nodeA = :nodeA AND ntn.fieldName = :fieldName') - ->setParameter('nodeA', $node) - ->setParameter('fieldName', $fieldName); - - $latestPosition = $query->getSingleScalarResult(); - - return is_numeric($latestPosition) ? (int) $latestPosition : 0; + return (int) $query->getSingleScalarResult(); } } diff --git a/src/Repository/PrefixAwareRepository.php b/src/Repository/PrefixAwareRepository.php index 8db2f7bc..4754a8ab 100644 --- a/src/Repository/PrefixAwareRepository.php +++ b/src/Repository/PrefixAwareRepository.php @@ -321,11 +321,9 @@ protected function classicLikeComparison( } foreach ($criteriaFields as $key => $value) { - if (\is_string($key)) { - $realKey = $this->getRealKey($qb, $key); - $fullKey = sprintf('LOWER(%s)', $realKey['prefix'] . $realKey['key']); - $qb->orWhere($qb->expr()->like($fullKey, $qb->expr()->literal($value))); - } + $realKey = $this->getRealKey($qb, $key); + $fullKey = sprintf('LOWER(%s)', $realKey['prefix'] . $realKey['key']); + $qb->orWhere($qb->expr()->like($fullKey, $qb->expr()->literal($value))); } return $qb; } diff --git a/src/Repository/RealmNodeRepository.php b/src/Repository/RealmNodeRepository.php index 4af3e76f..0e68b682 100644 --- a/src/Repository/RealmNodeRepository.php +++ b/src/Repository/RealmNodeRepository.php @@ -17,7 +17,7 @@ */ final class RealmNodeRepository extends EntityRepository { - public function findByNodeIdsAndRealmId(array $nodeIds, int|string $realmId): array + public function findByNodeIdsAndRealmId(array $nodeIds, int $realmId): array { $nodeIds = array_filter($nodeIds); if (empty($nodeIds)) { diff --git a/src/Repository/RealmRepository.php b/src/Repository/RealmRepository.php index 25382cba..21b4c507 100644 --- a/src/Repository/RealmRepository.php +++ b/src/Repository/RealmRepository.php @@ -41,13 +41,4 @@ public function findByNodeAndBehaviour(Node $node, string $realmBehaviour): arra return $qb->getQuery()->getResult(); } - - public function countWithSerializationGroup(): int - { - $qb = $this->createQueryBuilder('r'); - $qb->select($qb->expr()->count('r')) - ->andWhere($qb->expr()->isNotNull('r.serializationGroup')); - - return intval($qb->getQuery()->getSingleScalarResult()); - } } diff --git a/src/Repository/StatusAwareRepository.php b/src/Repository/StatusAwareRepository.php index 8aaf6ede..87fed3d5 100644 --- a/src/Repository/StatusAwareRepository.php +++ b/src/Repository/StatusAwareRepository.php @@ -8,7 +8,7 @@ use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; -use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Security; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** @@ -24,7 +24,7 @@ abstract class StatusAwareRepository extends EntityRepository /** * @param ManagerRegistry $registry - * @param class-string $entityClass + * @param string $entityClass * @param PreviewResolverInterface $previewResolver * @param EventDispatcherInterface $dispatcher * @param Security $security diff --git a/src/Repository/TagRepository.php b/src/Repository/TagRepository.php index 6d0eef31..6f8c1f17 100644 --- a/src/Repository/TagRepository.php +++ b/src/Repository/TagRepository.php @@ -13,7 +13,6 @@ use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; use RZ\Roadiz\CoreBundle\Doctrine\ORM\SimpleQueryBuilder; use RZ\Roadiz\CoreBundle\Entity\Node; -use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\Tag; use RZ\Roadiz\CoreBundle\Entity\TagTranslation; use RZ\Roadiz\CoreBundle\Entity\Translation; @@ -142,7 +141,7 @@ protected function filterByTranslation( $qb->leftJoin('tg.translatedTags', 'tt'); $qb->leftJoin( 'tt.translation', - self::TRANSLATION_ALIAS, + static::TRANSLATION_ALIAS, 'WITH', 't.defaultTranslation = true' ); @@ -234,7 +233,7 @@ protected function getCountContextualQueryWithTranslation( * @param integer|null $offset * @param TranslationInterface|null $translation * - * @return array|Paginator + * @return array|Paginator */ public function findBy( array $criteria, @@ -255,7 +254,6 @@ public function findBy( $this->applyFilterByNodes($criteria, $qb); $this->applyFilterByCriteria($criteria, $qb); $this->applyTranslationByTag($qb, $translation); - // @phpstan-ignore-next-line $query = $qb->getQuery(); $this->dispatchQueryEvent($query); @@ -300,7 +298,6 @@ public function findOneBy( $this->applyFilterByNodes($criteria, $qb); $this->applyFilterByCriteria($criteria, $qb); $this->applyTranslationByTag($qb, $translation); - // @phpstan-ignore-next-line $query = $qb->getQuery(); $this->dispatchQueryEvent($query); @@ -611,19 +608,19 @@ protected function prepareComparisons(array &$criteria, QueryBuilder $qb, $alias // Dots are forbidden in field definitions $baseKey = $simpleQB->getParameterKey($key); - if (\str_contains($key, 'translation.')) { + if (false !== \mb_strpos($key, 'translation.')) { /* * Search in translation fields */ $prefix = static::TRANSLATION_ALIAS . '.'; $key = str_replace('translation.', '', $key); - } elseif (\str_contains($key, 'nodes.')) { + } elseif (false !== \mb_strpos($key, 'nodes.')) { /* * Search in node fields */ $prefix = static::NODE_ALIAS . '.'; $key = str_replace('nodes.', '', $key); - } elseif (\str_contains($key, 'translatedTag.')) { + } elseif (false !== \mb_strpos($key, 'translatedTag.')) { /* * Search in translatedTags fields */ @@ -721,7 +718,7 @@ public function findOrCreateByPath(string $tagPath, ?TranslationInterface $trans * * @return Tag|null */ - public function findByPath(string $tagPath): ?Tag + public function findByPath(string $tagPath) { $tagPath = trim($tagPath); $tags = explode('/', $tagPath); @@ -746,12 +743,10 @@ public function findByPath(string $tagPath): ?Tag * * Parent can be null for tag root * - * @param Tag|null $parent + * @param Tag|null $parent * @return int - * @throws NoResultException - * @throws NonUniqueResultException */ - public function findLatestPositionInParent(Tag $parent = null): int + public function findLatestPositionInParent(Tag $parent = null) { $qb = $this->createQueryBuilder('t'); $qb->select($qb->expr()->max('t.position')); @@ -765,15 +760,4 @@ public function findLatestPositionInParent(Tag $parent = null): int return (int) $qb->getQuery()->getSingleScalarResult(); } - - public function findByNodesSources(NodesSources $nodesSources): array|Paginator - { - // @phpstan-ignore-next-line - return $this->findBy([ - "nodes" => $nodesSources->getNode(), - "translation" => $nodesSources->getTranslation(), - ], [ - 'position' => 'ASC', - ]); - } } diff --git a/src/Repository/UrlAliasRepository.php b/src/Repository/UrlAliasRepository.php index 86815976..f9728c08 100644 --- a/src/Repository/UrlAliasRepository.php +++ b/src/Repository/UrlAliasRepository.php @@ -4,8 +4,6 @@ namespace RZ\Roadiz\CoreBundle\Repository; -use Doctrine\ORM\NonUniqueResultException; -use Doctrine\ORM\NoResultException; use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\CoreBundle\Entity\UrlAlias; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; @@ -30,21 +28,18 @@ public function __construct( /** * Get all url aliases linked to given node. * - * @param int|string|null $nodeId + * @param integer $nodeId * - * @return iterable + * @return array */ - public function findAllFromNode(int|string|null $nodeId): iterable + public function findAllFromNode($nodeId) { - if (null === $nodeId) { - return []; - } $query = $this->_em->createQuery(' SELECT ua FROM RZ\Roadiz\CoreBundle\Entity\UrlAlias ua INNER JOIN ua.nodeSource ns INNER JOIN ns.node n WHERE n.id = :nodeId') - ->setParameter('nodeId', $nodeId); + ->setParameter('nodeId', (int) $nodeId); return $query->getResult(); } @@ -53,16 +48,14 @@ public function findAllFromNode(int|string|null $nodeId): iterable * @param string $alias * * @return boolean - * @throws NoResultException - * @throws NonUniqueResultException */ - public function exists(string $alias): bool + public function exists($alias) { $query = $this->_em->createQuery(' SELECT COUNT(ua.alias) FROM RZ\Roadiz\CoreBundle\Entity\UrlAlias ua WHERE ua.alias = :alias') ->setParameter('alias', $alias); - return $query->getSingleScalarResult() > 0; + return (bool) $query->getSingleScalarResult(); } } diff --git a/src/Repository/UserLogEntryRepository.php b/src/Repository/UserLogEntryRepository.php index a3a3a171..44caf056 100644 --- a/src/Repository/UserLogEntryRepository.php +++ b/src/Repository/UserLogEntryRepository.php @@ -18,6 +18,7 @@ * @method UserLogEntry|null findOneBy(array $criteria, array $orderBy = null) * @method UserLogEntry[] findAll() * @method UserLogEntry[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + * @extends LogEntryRepository */ final class UserLogEntryRepository extends LogEntryRepository implements ServiceEntityRepositoryInterface { diff --git a/src/RoadizCoreBundle.php b/src/RoadizCoreBundle.php index a37d6c1c..540b3975 100644 --- a/src/RoadizCoreBundle.php +++ b/src/RoadizCoreBundle.php @@ -14,7 +14,6 @@ use RZ\Roadiz\CoreBundle\DependencyInjection\Compiler\NodeWorkflowCompilerPass; use RZ\Roadiz\CoreBundle\DependencyInjection\Compiler\PathResolverCompilerPass; use RZ\Roadiz\CoreBundle\DependencyInjection\Compiler\RateLimitersCompilerPass; -use RZ\Roadiz\CoreBundle\DependencyInjection\Compiler\TreeWalkerDefinitionFactoryCompilerPass; use RZ\Roadiz\CoreBundle\DependencyInjection\Compiler\TwigLoaderCompilerPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -41,6 +40,5 @@ public function build(ContainerBuilder $container): void $container->addCompilerPass(new PathResolverCompilerPass()); $container->addCompilerPass(new FlysystemStorageCompilerPass()); $container->addCompilerPass(new TwigLoaderCompilerPass()); - $container->addCompilerPass(new TreeWalkerDefinitionFactoryCompilerPass()); } } diff --git a/src/Routing/DeferredRouteCollection.php b/src/Routing/DeferredRouteCollection.php new file mode 100644 index 00000000..4597717c --- /dev/null +++ b/src/Routing/DeferredRouteCollection.php @@ -0,0 +1,29 @@ +stopwatch = $stopwatch; + $this->logger = $logger ?? new NullLogger(); + $this->previewResolver = $previewResolver; } } diff --git a/src/Routing/InstallRouteCollection.php b/src/Routing/InstallRouteCollection.php new file mode 100644 index 00000000..7c86ff93 --- /dev/null +++ b/src/Routing/InstallRouteCollection.php @@ -0,0 +1,36 @@ +installClassname = $installClassname; + } + + /** + * {@inheritdoc} + */ + public function parseResources(): void + { + if (class_exists($this->installClassname)) { + $collection = call_user_func([$this->installClassname, 'getRoutes']); + if (null !== $collection) { + $this->addCollection($collection); + } + } else { + throw new \RuntimeException("Install class “" . $this->installClassname . "” does not exist.", 1); + } + } +} diff --git a/src/Routing/NodePathInfo.php b/src/Routing/NodePathInfo.php index 30e8710b..9ee32964 100644 --- a/src/Routing/NodePathInfo.php +++ b/src/Routing/NodePathInfo.php @@ -96,16 +96,12 @@ public function setContainsScheme(bool $containsScheme): NodePathInfo */ public function serialize(): string { - $json = \json_encode([ + return \json_encode([ 'path' => $this->getPath(), 'parameters' => $this->getParameters(), 'is_complete' => $this->isComplete(), 'contains_scheme' => $this->containsScheme() ]); - if (false === $json) { - throw new \RuntimeException('Unable to serialize NodePathInfo'); - } - return $json; } public function __serialize(): array diff --git a/src/Routing/NodeRouteHelper.php b/src/Routing/NodeRouteHelper.php index dfeec92a..f51117d2 100644 --- a/src/Routing/NodeRouteHelper.php +++ b/src/Routing/NodeRouteHelper.php @@ -19,11 +19,11 @@ final class NodeRouteHelper private LoggerInterface $logger; private string $defaultControllerNamespace; /** - * @var class-string + * @var class-string */ private string $defaultControllerClass; /** - * @var class-string|null + * @var class-string|null */ private ?string $controller = null; @@ -54,35 +54,24 @@ public function __construct( /** * Get controller class path for a given node. * - * @return class-string|null + * @return string */ - public function getController(): ?string + public function getController(): string { if (null === $this->controller) { - if (!$this->node->getNodeType()->isReachable()) { - return null; - } - $controllerClassName = $this->getControllerNamespace() . '\\' . + $namespace = $this->getControllerNamespace(); + $this->controller = $namespace . '\\' . StringHandler::classify($this->node->getNodeType()->getName()) . 'Controller'; - if (\class_exists($controllerClassName)) { - $reflection = new \ReflectionClass($controllerClassName); - if (!$reflection->isSubclassOf(AbstractController::class)) { - throw new \InvalidArgumentException( - 'Controller class ' . $controllerClassName . ' must extends ' . AbstractController::class - ); - } - // @phpstan-ignore-next-line - $this->controller = $controllerClassName; - } else { - /* - * Use a default controller if no controller was found in Theme. - */ + /* + * Use a default controller if no controller was found in Theme. + */ + if (!class_exists($this->controller) && $this->node->getNodeType()->isReachable()) { $this->controller = $this->defaultControllerClass; } } - // @phpstan-ignore-next-line + return $this->controller; } @@ -90,8 +79,8 @@ protected function getControllerNamespace(): string { $namespace = $this->defaultControllerNamespace; if (null !== $this->theme) { - $reflection = new \ReflectionClass($this->theme->getClassName()); - $namespace = $reflection->getNamespaceName() . '\\Controllers'; + $refl = new \ReflectionClass($this->theme->getClassName()); + $namespace = $refl->getNamespaceName() . '\\Controllers'; } return $namespace; } @@ -105,6 +94,7 @@ public function getMethod(): string * Return FALSE or TRUE if node is viewable. * * @return bool + * @throws \ReflectionException */ public function isViewable(): bool { @@ -119,14 +109,33 @@ public function isViewable(): bool ); return false; } + /* + * For archived and deleted nodes + */ + if ($this->node->getStatus() > Node::PUBLISHED) { + /* + * Not allowed to see deleted and archived nodes + * even for Admins + */ + return false; + } - if ($this->previewResolver->isPreview()) { - return $this->node->isDraft() || $this->node->isPending() || $this->node->isPublished(); + /* + * For unpublished nodes + */ + if ($this->node->getStatus() < Node::PUBLISHED) { + if (true === $this->previewResolver->isPreview()) { + return true; + } + /* + * Not allowed to see unpublished nodes + */ + return false; } /* * Everyone can view published nodes. */ - return $this->node->isPublished(); + return true; } } diff --git a/src/Routing/NodeRouter.php b/src/Routing/NodeRouter.php index 4d839b48..bdea3b74 100644 --- a/src/Routing/NodeRouter.php +++ b/src/Routing/NodeRouter.php @@ -5,7 +5,6 @@ namespace RZ\Roadiz\CoreBundle\Routing; use Psr\Cache\CacheItemPoolInterface; -use Psr\Cache\InvalidArgumentException; use Psr\Log\LoggerInterface; use RZ\Roadiz\CoreBundle\Bag\Settings; use RZ\Roadiz\CoreBundle\Entity\NodesSources; @@ -16,6 +15,7 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Routing\Exception\InvalidParameterException; use Symfony\Component\Routing\Exception\RouteNotFoundException; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Matcher\UrlMatcherInterface; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\RouteCollection; @@ -28,12 +28,24 @@ class NodeRouter extends Router implements VersatileGeneratorInterface */ public const NO_CACHE_PARAMETER = '_no_cache'; private ?Theme $theme = null; + private CacheItemPoolInterface $nodeSourceUrlCacheAdapter; + private Settings $settingsBag; + private EventDispatcherInterface $eventDispatcher; + /** + * @param NodeUrlMatcherInterface $matcher + * @param Settings $settingsBag + * @param EventDispatcherInterface $eventDispatcher + * @param CacheItemPoolInterface $nodeSourceUrlCacheAdapter + * @param array $options + * @param RequestContext $context + * @param LoggerInterface $logger + */ public function __construct( NodeUrlMatcherInterface $matcher, - protected readonly Settings $settingsBag, - protected readonly EventDispatcherInterface $eventDispatcher, - protected readonly CacheItemPoolInterface $nodeSourceUrlCacheAdapter, + Settings $settingsBag, + EventDispatcherInterface $eventDispatcher, + CacheItemPoolInterface $nodeSourceUrlCacheAdapter, RequestContext $context, LoggerInterface $logger, array $options = [] @@ -45,7 +57,10 @@ public function __construct( $context, $logger ); + $this->settingsBag = $settingsBag; + $this->eventDispatcher = $eventDispatcher; $this->matcher = $matcher; + $this->nodeSourceUrlCacheAdapter = $nodeSourceUrlCacheAdapter; } /** @@ -66,6 +81,22 @@ public function getMatcher(): UrlMatcherInterface return $this->matcher; } + /** + * No generator for a node router. + */ + public function getGenerator(): UrlGeneratorInterface + { + throw new \BadMethodCallException(get_class($this) . ' does not support path generation.'); + } + + /** + * @inheritDoc + */ + public function supports($name): bool + { + return ($name instanceof NodesSources || $name === RouteObjectInterface::OBJECT_BASED_ROUTE_NAME); + } + /** * @return Theme|null */ @@ -85,11 +116,24 @@ public function setTheme(?Theme $theme): NodeRouter } /** - * @inheritDoc + * Convert a route identifier (name, content object etc) into a string + * usable for logging and other debug/error messages + * + * @param mixed $name + * @param array $parameters which should contain a content field containing + * a RouteReferrersReadInterface object + * + * @return string */ - public function getRouteDebugMessage(string $name, array $parameters = []): string + public function getRouteDebugMessage($name, array $parameters = []): string { - if (RouteObjectInterface::OBJECT_BASED_ROUTE_NAME === $name) { + if ($name instanceof NodesSources) { + @trigger_error('Passing an object as route name is deprecated since version 1.5. Pass the `RouteObjectInterface::OBJECT_BASED_ROUTE_NAME` as route name and the object in the parameters with key `RouteObjectInterface::ROUTE_OBJECT` resp the content id with content_id.', E_USER_DEPRECATED); + return '[' . $name->getTranslation()->getLocale() . ']' . + $name->getTitle() . ' - ' . + $name->getNode()->getNodeName() . + '[' . $name->getNode()->getId() . ']'; + } elseif (RouteObjectInterface::OBJECT_BASED_ROUTE_NAME === $name) { if ( array_key_exists(RouteObjectInterface::ROUTE_OBJECT, $parameters) && $parameters[RouteObjectInterface::ROUTE_OBJECT] instanceof NodesSources @@ -106,20 +150,19 @@ public function getRouteDebugMessage(string $name, array $parameters = []): stri /** * {@inheritdoc} - * @throws InvalidArgumentException */ public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string { - if (RouteObjectInterface::OBJECT_BASED_ROUTE_NAME !== $name) { - throw new RouteNotFoundException(); - } - - if ( - array_key_exists(RouteObjectInterface::ROUTE_OBJECT, $parameters) && - $parameters[RouteObjectInterface::ROUTE_OBJECT] instanceof NodesSources - ) { - $route = $parameters[RouteObjectInterface::ROUTE_OBJECT]; - unset($parameters[RouteObjectInterface::ROUTE_OBJECT]); + if (RouteObjectInterface::OBJECT_BASED_ROUTE_NAME === $name) { + if ( + array_key_exists(RouteObjectInterface::ROUTE_OBJECT, $parameters) && + $parameters[RouteObjectInterface::ROUTE_OBJECT] instanceof NodesSources + ) { + $route = $parameters[RouteObjectInterface::ROUTE_OBJECT]; + unset($parameters[RouteObjectInterface::ROUTE_OBJECT]); + } else { + $route = null; + } } else { $route = null; } diff --git a/src/Routing/NodeUrlMatcher.php b/src/Routing/NodeUrlMatcher.php index 743e405f..0ef42348 100644 --- a/src/Routing/NodeUrlMatcher.php +++ b/src/Routing/NodeUrlMatcher.php @@ -8,35 +8,22 @@ use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\Theme; use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Stopwatch\Stopwatch; /** - * UrlMatcher which tries to grab Node and Translation information for a route. + * UrlMatcher which tries to grab Node and Translation + * information for a route. */ final class NodeUrlMatcher extends DynamicUrlMatcher implements NodeUrlMatcherInterface { + protected PathResolverInterface $pathResolver; /** - * @param PathResolverInterface $pathResolver - * @param RequestContext $context - * @param PreviewResolverInterface $previewResolver - * @param Stopwatch $stopwatch - * @param LoggerInterface $logger - * @param class-string $defaultControllerClass + * @var class-string */ - public function __construct( - private readonly PathResolverInterface $pathResolver, - RequestContext $context, - PreviewResolverInterface $previewResolver, - Stopwatch $stopwatch, - LoggerInterface $logger, - private readonly string $defaultControllerClass - ) { - parent::__construct($context, $previewResolver, $stopwatch, $logger); - } + private string $defaultControllerClass; /** * @return array @@ -55,7 +42,28 @@ public function getDefaultSupportedFormatExtension(): string } /** - * @inheritDoc + * @param PathResolverInterface $pathResolver + * @param RequestContext $context + * @param PreviewResolverInterface $previewResolver + * @param Stopwatch $stopwatch + * @param LoggerInterface $logger + * @param class-string $defaultControllerClass + */ + public function __construct( + PathResolverInterface $pathResolver, + RequestContext $context, + PreviewResolverInterface $previewResolver, + Stopwatch $stopwatch, + LoggerInterface $logger, + string $defaultControllerClass + ) { + parent::__construct($context, $previewResolver, $stopwatch, $logger); + $this->pathResolver = $pathResolver; + $this->defaultControllerClass = $defaultControllerClass; + } + + /** + * {@inheritdoc} */ public function match(string $pathinfo): array { @@ -81,6 +89,7 @@ protected function getNodeRouteHelper(NodesSources $nodeSource, ?Theme $theme): * @param string $decodedUrl * @param Theme|null $theme * @return array + * @throws \ReflectionException */ public function matchNode(string $decodedUrl, ?Theme $theme): array { diff --git a/src/Routing/NodesSourcesPathResolver.php b/src/Routing/NodesSourcesPathResolver.php index 2e8167c1..8fe3f0f5 100644 --- a/src/Routing/NodesSourcesPathResolver.php +++ b/src/Routing/NodesSourcesPathResolver.php @@ -4,7 +4,6 @@ namespace RZ\Roadiz\CoreBundle\Routing; -use Doctrine\ORM\NonUniqueResultException; use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; use RZ\Roadiz\CoreBundle\Bag\Settings; @@ -20,16 +19,28 @@ final class NodesSourcesPathResolver implements PathResolverInterface { + private ManagerRegistry $managerRegistry; + private Stopwatch $stopwatch; private static string $nodeNamePattern = '[a-zA-Z0-9\-\_\.]+'; + private PreviewResolverInterface $previewResolver; + private Settings $settingsBag; + private RequestStack $requestStack; + private bool $useAcceptLanguageHeader; public function __construct( - private readonly ManagerRegistry $managerRegistry, - private readonly PreviewResolverInterface $previewResolver, - private readonly Stopwatch $stopwatch, - private readonly Settings $settingsBag, - private readonly RequestStack $requestStack, - private readonly bool $useAcceptLanguageHeader + ManagerRegistry $managerRegistry, + PreviewResolverInterface $previewResolver, + Stopwatch $stopwatch, + Settings $settingsBag, + RequestStack $requestStack, + bool $useAcceptLanguageHeader ) { + $this->stopwatch = $stopwatch; + $this->previewResolver = $previewResolver; + $this->managerRegistry = $managerRegistry; + $this->settingsBag = $settingsBag; + $this->requestStack = $requestStack; + $this->useAcceptLanguageHeader = $useAcceptLanguageHeader; } /** @@ -50,7 +61,7 @@ public function resolvePath( } if ($path === '/') { - $this->stopwatch->start('parseRootPath', 'routing'); + $this->stopwatch->start('parseRootPath'); $translation = $this->parseTranslation(); $nodeSource = $this->getHome($translation); $this->stopwatch->stop('parseRootPath'); @@ -85,13 +96,13 @@ public function resolvePath( } } - $this->stopwatch->start('parseTranslation', 'routing'); + $this->stopwatch->start('parseTranslation'); $translation = $this->parseTranslation($tokens); $this->stopwatch->stop('parseTranslation'); /* * Try with URL Aliases OR nodeName */ - $this->stopwatch->start('parseFromIdentifier', 'routing'); + $this->stopwatch->start('parseFromIdentifier'); $nodeSource = $this->parseFromIdentifier($tokens, $translation, $allowNonReachableNodes); $this->stopwatch->stop('parseFromIdentifier'); } @@ -145,7 +156,6 @@ private function getHome(TranslationInterface $translation): ?NodesSources * @param array $tokens * * @return TranslationInterface|null - * @throws NonUniqueResultException */ private function parseTranslation(array &$tokens = []): ?TranslationInterface { diff --git a/src/Routing/NodesSourcesUrlGenerator.php b/src/Routing/NodesSourcesUrlGenerator.php index 943aa29e..293d5e5e 100644 --- a/src/Routing/NodesSourcesUrlGenerator.php +++ b/src/Routing/NodesSourcesUrlGenerator.php @@ -13,12 +13,31 @@ */ final class NodesSourcesUrlGenerator { + protected ?Request $request; + protected ?NodesSources $nodeSource; + protected bool $forceLocale; + protected bool $forceLocaleWithUrlAlias; + protected NodesSourcesPathAggregator $pathAggregator; + + /** + * @param NodesSourcesPathAggregator $pathAggregator + * @param Request|null $request + * @param NodesSources|null $nodeSource + * @param bool $forceLocale + * @param bool $forceLocaleWithUrlAlias + */ public function __construct( - private readonly NodesSourcesPathAggregator $pathAggregator, - private readonly ?NodesSources $nodeSource = null, - private readonly bool $forceLocale = false, - private readonly bool $forceLocaleWithUrlAlias = false + NodesSourcesPathAggregator $pathAggregator, + Request $request = null, + NodesSources $nodeSource = null, + bool $forceLocale = false, + bool $forceLocaleWithUrlAlias = false ) { + $this->pathAggregator = $pathAggregator; + $this->request = $request; + $this->nodeSource = $nodeSource; + $this->forceLocale = $forceLocale; + $this->forceLocaleWithUrlAlias = $forceLocaleWithUrlAlias; } /** diff --git a/src/Routing/NullLoader.php b/src/Routing/NullLoader.php index 673b621d..70eaacd3 100644 --- a/src/Routing/NullLoader.php +++ b/src/Routing/NullLoader.php @@ -10,23 +10,36 @@ final class NullLoader implements LoaderInterface { /** - * @inheritDoc + * Loads a resource. + * + * @param mixed $resource The resource + * @param string|null $type The resource type or null if unknown + * @return mixed + * + * @throws \Exception If something went wrong */ - public function load(mixed $resource, string $type = null): mixed + public function load($resource, $type = null): mixed { return null; } /** - * @inheritDoc + * Returns whether this class supports the given resource. + * + * @param mixed $resource A resource + * @param string|null $type The resource type or null if unknown + * + * @return bool True if this class supports the given resource, false otherwise */ - public function supports(mixed $resource, string $type = null): bool + public function supports($resource, $type = null): bool { return true; } /** - * @inheritDoc + * Gets the loader resolver. + * + * @return LoaderResolverInterface|null A LoaderResolverInterface instance */ public function getResolver(): ?LoaderResolverInterface { @@ -34,7 +47,10 @@ public function getResolver(): ?LoaderResolverInterface } /** - * @inheritDoc + * Sets the loader resolver. + * + * @param LoaderResolverInterface $resolver + * @return NullLoader */ public function setResolver(LoaderResolverInterface $resolver): self { diff --git a/src/Routing/OptimizedNodesSourcesGraphPathAggregator.php b/src/Routing/OptimizedNodesSourcesGraphPathAggregator.php index 0d171edf..e37cdf9d 100644 --- a/src/Routing/OptimizedNodesSourcesGraphPathAggregator.php +++ b/src/Routing/OptimizedNodesSourcesGraphPathAggregator.php @@ -7,16 +7,22 @@ use Doctrine\ORM\Query; use Doctrine\Persistence\ManagerRegistry; use Psr\Cache\CacheItemPoolInterface; -use Psr\Cache\InvalidArgumentException; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\NodesSources; final class OptimizedNodesSourcesGraphPathAggregator implements NodesSourcesPathAggregator { - public function __construct( - private readonly ManagerRegistry $managerRegistry, - private readonly CacheItemPoolInterface $cacheAdapter - ) { + private ManagerRegistry $managerRegistry; + private CacheItemPoolInterface $cacheAdapter; + + /** + * @param ManagerRegistry $managerRegistry + * @param CacheItemPoolInterface $cacheAdapter + */ + public function __construct(ManagerRegistry $managerRegistry, CacheItemPoolInterface $cacheAdapter) + { + $this->managerRegistry = $managerRegistry; + $this->cacheAdapter = $cacheAdapter; } private function getCacheKey(NodesSources $nodesSources): string @@ -28,7 +34,6 @@ private function getCacheKey(NodesSources $nodesSources): string * @param NodesSources $nodesSources * @param array $parameters * @return string - * @throws InvalidArgumentException */ public function aggregatePath(NodesSources $nodesSources, array $parameters = []): string { @@ -52,9 +57,9 @@ public function aggregatePath(NodesSources $nodesSources, array $parameters = [] /** * @param Node $parent * - * @return array + * @return array */ - private function getParentsIds(Node $parent): array + protected function getParentsIds(Node $parent): array { $parentIds = []; while ($parent !== null && !$parent->isHome()) { @@ -67,13 +72,13 @@ private function getParentsIds(Node $parent): array /** * Get every nodeSource parents identifier from current to - * farthest ancestor. + * farest ancestor. * * @param NodesSources $source * * @return array */ - private function getIdentifiers(NodesSources $source): array + protected function getIdentifiers(NodesSources $source): array { $urlTokens = []; $parents = []; diff --git a/src/Routing/RedirectableUrlMatcher.php b/src/Routing/RedirectableUrlMatcher.php index b6f3c486..a686e08c 100644 --- a/src/Routing/RedirectableUrlMatcher.php +++ b/src/Routing/RedirectableUrlMatcher.php @@ -18,7 +18,7 @@ final class RedirectableUrlMatcher extends BaseMatcher * * @return array An array of parameters */ - public function redirect(string $path, string $route, ?string $scheme = null): array + public function redirect($path, $route, $scheme = null): array { return [ '_controller' => RedirectionController::class . '::redirectToRouteAction', diff --git a/src/Routing/RedirectionMatcher.php b/src/Routing/RedirectionMatcher.php index 81001bee..3579b0e1 100644 --- a/src/Routing/RedirectionMatcher.php +++ b/src/Routing/RedirectionMatcher.php @@ -4,13 +4,16 @@ namespace RZ\Roadiz\CoreBundle\Routing; +use Doctrine\Persistence\ManagerRegistry; use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; use RZ\Roadiz\CoreBundle\Controller\RedirectionController; use RZ\Roadiz\CoreBundle\Entity\Redirection; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\Matcher\UrlMatcher; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\RouteCollection; +use Symfony\Component\Stopwatch\Stopwatch; /** * UrlMatcher which tries to grab Node and Translation @@ -18,19 +21,34 @@ */ final class RedirectionMatcher extends UrlMatcher { + private ManagerRegistry $managerRegistry; + private Stopwatch $stopwatch; + private LoggerInterface $logger; + + /** + * @param RequestContext $context + * @param ManagerRegistry $managerRegistry + * @param Stopwatch $stopwatch + * @param LoggerInterface $logger + */ public function __construct( RequestContext $context, - private readonly RedirectionPathResolver $pathResolver, - private readonly LoggerInterface $logger + ManagerRegistry $managerRegistry, + Stopwatch $stopwatch, + LoggerInterface $logger ) { parent::__construct(new RouteCollection(), $context); + $this->stopwatch = $stopwatch; + $this->logger = $logger; + $this->managerRegistry = $managerRegistry; } /** * {@inheritdoc} */ - public function match(string $pathinfo): array + public function match($pathinfo): array { + $this->stopwatch->start('findRedirection'); $decodedUrl = rawurldecode($pathinfo); /* @@ -38,12 +56,14 @@ public function match(string $pathinfo): array */ if (null !== $redirection = $this->matchRedirection($decodedUrl)) { $this->logger->debug('Matched redirection.', ['query' => $redirection->getQuery()]); + $this->stopwatch->stop('findRedirection'); return [ '_controller' => RedirectionController::class . '::redirectAction', 'redirection' => $redirection, '_route' => null, ]; } + $this->stopwatch->stop('findRedirection'); throw new ResourceNotFoundException(sprintf('%s did not match any Doctrine Redirection', $pathinfo)); } @@ -54,7 +74,6 @@ public function match(string $pathinfo): array */ protected function matchRedirection(string $decodedUrl): ?Redirection { - $resource = $this->pathResolver->resolvePath($decodedUrl)->getResource(); - return $resource instanceof Redirection ? $resource : null; + return $this->managerRegistry->getRepository(Redirection::class)->findOneByQuery($decodedUrl); } } diff --git a/src/Routing/RedirectionPathResolver.php b/src/Routing/RedirectionPathResolver.php index d6947849..b684f7f4 100644 --- a/src/Routing/RedirectionPathResolver.php +++ b/src/Routing/RedirectionPathResolver.php @@ -5,20 +5,16 @@ namespace RZ\Roadiz\CoreBundle\Routing; use Doctrine\Persistence\ManagerRegistry; -use Psr\Cache\CacheItemPoolInterface; use RZ\Roadiz\CoreBundle\Entity\Redirection; use Symfony\Component\Routing\Exception\ResourceNotFoundException; -use Symfony\Component\Stopwatch\Stopwatch; final class RedirectionPathResolver implements PathResolverInterface { - public const CACHE_KEY = 'redirection_path_resolver_cache'; + private ManagerRegistry $managerRegistry; - public function __construct( - private readonly ManagerRegistry $managerRegistry, - private readonly CacheItemPoolInterface $cacheAdapter, - private readonly Stopwatch $stopwatch, - ) { + public function __construct(ManagerRegistry $managerRegistry) + { + $this->managerRegistry = $managerRegistry; } public function resolvePath( @@ -27,49 +23,15 @@ public function resolvePath( bool $allowRootPaths = false, bool $allowNonReachableNodes = true ): ResourceInfo { - $this->stopwatch->start('lookForRedirection', 'routing'); - $cacheItem = $this->cacheAdapter->getItem(self::CACHE_KEY); - if (!$cacheItem->isHit()) { - // Populate cache item - /** @var array[] $redirections */ - $redirections = $this->managerRegistry - ->getRepository(Redirection::class) - ->createQueryBuilder('r') - ->select(['r.id', 'r.query']) - ->getQuery() - ->getArrayResult(); - $redirections = array_combine( - array_column($redirections, 'query'), - array_column($redirections, 'id') - ); - $cacheItem->set($redirections); - $this->cacheAdapter->save($cacheItem); - } else { - /** @var array[] $redirections */ - $redirections = $cacheItem->get(); - } - - /** @var int|null $redirectionId */ - $redirectionId = $redirections[$path] ?? null; - $this->stopwatch->stop('lookForRedirection'); - - if (null === $redirectionId) { - throw new ResourceNotFoundException(); - } - $this->stopwatch->start('findRedirection', 'routing'); $redirection = $this->managerRegistry ->getRepository(Redirection::class) - ->find($redirectionId); - $this->stopwatch->stop('findRedirection'); + ->findOneByQuery($path); + if (null === $redirection) { throw new ResourceNotFoundException(); } - $this->stopwatch->start('incrementRedirection', 'routing'); - $redirection->incrementUseCount(); - $this->managerRegistry->getManagerForClass(Redirection::class)->flush(); - $this->stopwatch->stop('incrementRedirection'); - - return (new ResourceInfo())->setResource($redirection); + return (new ResourceInfo()) + ->setResource($redirection); } } diff --git a/src/Routing/RedirectionRouter.php b/src/Routing/RedirectionRouter.php index 1ec14a2e..8df68ff7 100644 --- a/src/Routing/RedirectionRouter.php +++ b/src/Routing/RedirectionRouter.php @@ -7,7 +7,6 @@ use Doctrine\Persistence\ManagerRegistry; use Psr\Log\LoggerInterface; use Symfony\Cmf\Component\Routing\VersatileGeneratorInterface; -use Symfony\Component\Routing\Exception\RouteNotFoundException; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\RouteCollection; @@ -16,13 +15,24 @@ class RedirectionRouter extends Router implements VersatileGeneratorInterface { + protected ManagerRegistry $managerRegistry; + protected ?Stopwatch $stopwatch; + + /** + * @param RedirectionMatcher $matcher + * @param ManagerRegistry $managerRegistry + * @param array $options + * @param RequestContext|null $context + * @param LoggerInterface|null $logger + * @param Stopwatch|null $stopwatch + */ public function __construct( RedirectionMatcher $matcher, - protected readonly ManagerRegistry $managerRegistry, - protected readonly Stopwatch $stopwatch, + ManagerRegistry $managerRegistry, array $options = [], RequestContext $context = null, LoggerInterface $logger = null, + Stopwatch $stopwatch = null ) { parent::__construct( new NullLoader(), @@ -31,6 +41,8 @@ public function __construct( $context, $logger ); + $this->stopwatch = $stopwatch; + $this->managerRegistry = $managerRegistry; $this->matcher = $matcher; } @@ -47,7 +59,7 @@ public function getRouteCollection(): RouteCollection */ public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string { - throw new RouteNotFoundException(get_class($this) . ' does not support path generation.'); + return ''; } /** @@ -58,7 +70,12 @@ public function getGenerator(): UrlGeneratorInterface throw new \BadMethodCallException(get_class($this) . ' does not support path generation.'); } - public function getRouteDebugMessage(mixed $name, array $parameters = []): string + public function supports($name): bool + { + return false; + } + + public function getRouteDebugMessage($name, array $parameters = []): string { return 'RedirectionRouter does not support path generation.'; } diff --git a/src/Routing/StaticRouter.php b/src/Routing/StaticRouter.php new file mode 100644 index 00000000..4f9b917c --- /dev/null +++ b/src/Routing/StaticRouter.php @@ -0,0 +1,52 @@ +routeCollection = $routeCollection; + } + + /** + * @return RouteCollection + */ + public function getRouteCollection(): RouteCollection + { + if (null === $this->collection) { + $this->routeCollection->parseResources(); + $this->collection = $this->routeCollection; + } + return $this->collection; + } +} diff --git a/src/SearchEngine/AbstractSearchHandler.php b/src/SearchEngine/AbstractSearchHandler.php index 133f3691..cd2c6f5f 100644 --- a/src/SearchEngine/AbstractSearchHandler.php +++ b/src/SearchEngine/AbstractSearchHandler.php @@ -11,26 +11,22 @@ use Solarium\Core\Client\Client; use Solarium\Core\Query\Helper; use Solarium\QueryType\Select\Query\Query; -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; abstract class AbstractSearchHandler implements SearchHandlerInterface { - protected ClientRegistry $clientRegistry; + private ClientRegistry $clientRegistry; protected ObjectManager $em; protected LoggerInterface $logger; - protected EventDispatcherInterface $eventDispatcher; protected int $highlightingFragmentSize = 150; public function __construct( ClientRegistry $clientRegistry, ObjectManager $em, - LoggerInterface $searchEngineLogger, - EventDispatcherInterface $eventDispatcher + LoggerInterface $searchEngineLogger ) { $this->clientRegistry = $clientRegistry; $this->em = $em; $this->logger = $searchEngineLogger; - $this->eventDispatcher = $eventDispatcher; } public function getSolr(): Client @@ -54,6 +50,7 @@ public function getSolr(): Client * @param array $args * @param int $rows * @param bool $searchTags Search in tags/folders too, even if a node don’t match + * @param int $proximity Proximity matching: Lucene supports finding words are a within a specific distance away. * @param int $page * * @return SearchResultsInterface Return a SearchResultsInterface iterable object. @@ -63,13 +60,14 @@ public function searchWithHighlight( array $args = [], int $rows = 20, bool $searchTags = false, + int $proximity = 1, int $page = 1 ): SearchResultsInterface { $args = $this->argFqProcess($args); $args["fq"][] = "document_type_s:" . $this->getDocumentType(); - $args["hl.q"] = $this->buildHighlightingQuery($q); + $args["hl.q"] = $this->escapeQuery(trim($q)); $args = array_merge($this->getHighlightingOptions($args), $args); - $response = $this->nativeSearch($q, $args, $rows, $searchTags, $page); + $response = $this->nativeSearch($q, $args, $rows, $searchTags, $proximity, $page); return $this->createSearchResultsFromResponse($response); } @@ -97,7 +95,7 @@ protected function getHighlightingOptions(array &$args = []): array { $tmp = []; $tmp["hl"] = true; - $tmp["hl.fl"] = $this->getTitleField($args) . ' ' . $this->getCollectionField($args); + $tmp["hl.fl"] = $this->getCollectionField($args); $tmp["hl.fragsize"] = $this->getHighlightingFragmentSize(); $tmp["hl.simple.pre"] = ''; $tmp["hl.simple.post"] = ''; @@ -150,6 +148,7 @@ public function setHighlightingFragmentSize(int $highlightingFragmentSize): Abst * @param array $args * @param int $rows * @param bool $searchTags + * @param int $proximity Proximity matching: Lucene supports finding words are a within a specific distance away. * @param int $page * * @return array|null @@ -159,6 +158,7 @@ abstract protected function nativeSearch( array $args = [], int $rows = 20, bool $searchTags = false, + int $proximity = 1, int $page = 1 ): ?array; @@ -175,7 +175,7 @@ abstract protected function nativeSearch( * ### For node-sources: * * * status (int) - * * visible (bool) + * * visible (boolean) * * nodeType (RZ\Roadiz\CoreBundle\Entity\NodeType or string or array) * * tags (RZ\Roadiz\CoreBundle\Entity\Tag or array of Tag) * * translation (RZ\Roadiz\CoreBundle\Entity\Translation) @@ -190,7 +190,8 @@ abstract protected function nativeSearch( * @param string $q * @param array $args * @param int $rows Results per page - * @param bool $searchTags Search in tags/folders too, even if a node don’t match + * @param boolean $searchTags Search in tags/folders too, even if a node don’t match + * @param int $proximity Proximity matching: Lucene supports finding words are a within a specific distance away. Default 10000000 * @param int $page Retrieve a specific page * * @return SearchResultsInterface Return an array of doctrine Entities (Document, NodesSources) @@ -200,6 +201,7 @@ public function search( array $args = [], int $rows = 20, bool $searchTags = false, + int $proximity = 1, int $page = 1 ): SearchResultsInterface { $args = $this->argFqProcess($args); @@ -207,7 +209,7 @@ public function search( $tmp = []; $args = array_merge($tmp, $args); - $response = $this->nativeSearch($q, $args, $rows, $searchTags, $page); + $response = $this->nativeSearch($q, $args, $rows, $searchTags, $proximity, $page); return $this->createSearchResultsFromResponse($response); } @@ -229,9 +231,10 @@ public function escapeQuery(string $input): string /** * @param string $q + * @param int $proximity * @return array [$exactQuery, $fuzzyQuery, $wildcardQuery] */ - protected function getFormattedQuery(string $q): array + protected function getFormattedQuery(string $q, int $proximity = 1): array { $q = trim($q); /** @@ -239,16 +242,13 @@ protected function getFormattedQuery(string $q): array * @see https://lucene.apache.org/solr/guide/6_6/the-standard-query-parser.html#TheStandardQueryParser-FuzzySearches */ $words = preg_split('#[\s,]+#', $q, -1, PREG_SPLIT_NO_EMPTY); - if (false === $words) { - throw new \RuntimeException('Cannot split query string.'); - } - $fuzzyiedQuery = implode(' ', array_map(function (string $word) { + $fuzzyiedQuery = implode(' ', array_map(function (string $word) use ($proximity) { /* * Do not fuzz short words: Solr crashes * Proximity is set to 1 by default for single-words */ if (\mb_strlen($word) > 3) { - return $this->escapeQuery($word) . '~2'; + return $this->escapeQuery($word) . '~' . $proximity; } return $this->escapeQuery($word); }, $words)); @@ -259,7 +259,7 @@ protected function getFormattedQuery(string $q): array /* * Wildcard search for allowing autocomplete */ - $wildcardQuery = $this->escapeQuery($q) . '*~2'; + $wildcardQuery = $this->escapeQuery($q) . '*~' . $proximity; return [$exactQuery, $fuzzyiedQuery, $wildcardQuery]; } @@ -272,14 +272,15 @@ protected function getFormattedQuery(string $q): array * @param string $q * @param array $args * @param bool $searchTags + * @param int $proximity * @return string */ - protected function buildQuery(string $q, array &$args, bool $searchTags = false): string + protected function buildQuery(string $q, array &$args, bool $searchTags = false, int $proximity = 1): string { $titleField = $this->getTitleField($args); $collectionField = $this->getCollectionField($args); $tagsField = $this->getTagsField($args); - [$exactQuery, $fuzzyiedQuery, $wildcardQuery] = $this->getFormattedQuery($q); + [$exactQuery, $fuzzyiedQuery, $wildcardQuery] = $this->getFormattedQuery($q, $proximity); /* * Search in node-sources tags name… @@ -308,35 +309,6 @@ protected function buildQuery(string $q, array &$args, bool $searchTags = false) } } - protected function buildHighlightingQuery(string $q): string - { - $q = trim($q); - $words = preg_split('#[\s,]+#', $q, -1, PREG_SPLIT_NO_EMPTY); - if (\is_array($words) && \count($words) > 1) { - return $this->escapeQuery($q); - } - - $q = $this->escapeQuery($q); - return sprintf('%s~2', $q); - } - - /** - * @param array $args - * @param bool $searchTags - * @return string - */ - protected function buildQueryFields(array &$args, bool $searchTags = true): string - { - $titleField = $this->getTitleField($args); - $collectionField = $this->getCollectionField($args); - $tagsField = $this->getTagsField($args); - - if ($searchTags) { - return $titleField . '^10 ' . $collectionField . '^2 ' . $tagsField . ' slug_s'; - } - return $titleField . ' ' . $collectionField . ' slug_s'; - } - /** * @param string $q * diff --git a/src/SearchEngine/ClientRegistry.php b/src/SearchEngine/ClientRegistry.php index 0e9cae6c..e15699dc 100644 --- a/src/SearchEngine/ClientRegistry.php +++ b/src/SearchEngine/ClientRegistry.php @@ -21,17 +21,10 @@ public function __construct(ContainerInterface $container) public function getClient(): ?Client { - $client = $this->container->get( + return $this->container->get( 'roadiz_core.solr.client', ContainerInterface::NULL_ON_INVALID_REFERENCE ); - if (null === $client) { - return null; - } - if (!($client instanceof Client)) { - throw new \RuntimeException('Solr client must be an instance of ' . Client::class); - } - return $client; } public function isClientReady(?Client $client): bool diff --git a/src/SearchEngine/DocumentSearchHandler.php b/src/SearchEngine/DocumentSearchHandler.php index eb83052a..0804fbcb 100644 --- a/src/SearchEngine/DocumentSearchHandler.php +++ b/src/SearchEngine/DocumentSearchHandler.php @@ -6,7 +6,6 @@ use RZ\Roadiz\CoreBundle\Entity\Folder; use RZ\Roadiz\CoreBundle\Entity\Translation; -use RZ\Roadiz\CoreBundle\SearchEngine\Event\DocumentSearchQueryEvent; /** * @package RZ\Roadiz\CoreBundle\SearchEngine @@ -17,7 +16,8 @@ class DocumentSearchHandler extends AbstractSearchHandler * @param string $q * @param array $args * @param integer $rows - * @param bool $searchTags + * @param boolean $searchTags + * @param integer $proximity Proximity matching: Lucene supports finding words are a within a specific distance away. * @param integer $page * * @return array|null @@ -27,42 +27,38 @@ protected function nativeSearch( array $args = [], int $rows = 20, bool $searchTags = false, + int $proximity = 1, int $page = 1 ): ?array { - if (empty($q)) { + if (!empty($q)) { + $query = $this->createSolrQuery($args, $rows, $page); + $queryTxt = $this->buildQuery($q, $args, $searchTags, $proximity); + $query->setQuery($queryTxt); + + /* + * Only need these fields as Doctrine + * will do the rest. + */ + $query->setFields([ + 'id', + 'sort', + 'document_type_s', + SolariumDocumentTranslation::IDENTIFIER_KEY, + 'filename_s', + 'locale_s', + ]); + + $this->logger->debug('[Solr] Request document search…', [ + 'query' => $queryTxt, + 'fq' => $args["fq"] ?? [], + 'params' => $query->getParams(), + ]); + + $solrRequest = $this->getSolr()->execute($query); + return $solrRequest->getData(); + } else { return null; } - $query = $this->createSolrQuery($args, $rows, $page); - $queryTxt = $this->buildQuery($q, $args, $searchTags); - $query->setQuery($queryTxt); - - /* - * Only need these fields as Doctrine - * will do the rest. - */ - $query->setFields([ - 'id', - 'sort', - 'document_type_s', - SolariumDocumentTranslation::IDENTIFIER_KEY, - 'filename_s', - 'locale_s', - ]); - - $this->logger->debug('[Solr] Request document search…', [ - 'query' => $queryTxt, - 'fq' => $args["fq"] ?? [], - 'params' => $query->getParams(), - ]); - - /** @var DocumentSearchQueryEvent $event */ - $event = $this->eventDispatcher->dispatch( - new DocumentSearchQueryEvent($query, $args) - ); - $query = $event->getQuery(); - - $solrRequest = $this->getSolr()->execute($query); - return $solrRequest->getData(); } /** diff --git a/src/SearchEngine/Event/AbstractSearchQueryEvent.php b/src/SearchEngine/Event/AbstractSearchQueryEvent.php deleted file mode 100644 index 0b65abda..00000000 --- a/src/SearchEngine/Event/AbstractSearchQueryEvent.php +++ /dev/null @@ -1,36 +0,0 @@ -query = $query; - $this->args = $args; - } - - /** - * @return Query - */ - public function getQuery(): Query - { - return $this->query; - } - - /** - * @return array - */ - public function getArgs(): array - { - return $this->args; - } -} diff --git a/src/SearchEngine/Event/DocumentSearchQueryEvent.php b/src/SearchEngine/Event/DocumentSearchQueryEvent.php deleted file mode 100644 index b7d98ec2..00000000 --- a/src/SearchEngine/Event/DocumentSearchQueryEvent.php +++ /dev/null @@ -1,9 +0,0 @@ -> $nodesSources + /* + * First try with Solr */ + /** @var array $nodesSources */ $nodesSources = $this->getRepository()->findBySearchQuery( $safeSearchTerms, $resultCount ); - if (count($nodesSources) > 0) { - return array_map(function (SolrSearchResultItem $item) { - return $item->getItem(); - }, $nodesSources); - } - /* * Second try with sources fields */ - $nodesSources = $this->getRepository()->searchBy( - $safeSearchTerms, - [], - [], - $resultCount - ); - if (count($nodesSources) === 0) { - /* - * Then try with node name. - */ - $qb = $this->getRepository()->createQueryBuilder('ns'); + $nodesSources = $this->getRepository()->searchBy( + $safeSearchTerms, + [], + [], + $resultCount + ); - $qb->select('ns, n') - ->innerJoin('ns.node', 'n') - ->andWhere($qb->expr()->orX( - $qb->expr()->like('n.nodeName', ':nodeName'), - $qb->expr()->like('ns.title', ':nodeName') - )) - ->setMaxResults($resultCount) - ->setParameter('nodeName', '%' . $safeSearchTerms . '%'); + if (count($nodesSources) === 0) { + /* + * Then try with node name. + */ + $qb = $this->getRepository()->createQueryBuilder('ns'); - if (null !== $translation) { - $qb->andWhere($qb->expr()->eq('ns.translation', ':translation')) - ->setParameter('translation', $translation); - } - try { - return $qb->getQuery()->getResult(); - } catch (NoResultException $e) { - return []; + $qb->select('ns, n') + ->innerJoin('ns.node', 'n') + ->andWhere($qb->expr()->orX( + $qb->expr()->like('n.nodeName', ':nodeName'), + $qb->expr()->like('ns.title', ':nodeName') + )) + ->setMaxResults($resultCount) + ->setParameter('nodeName', '%' . $safeSearchTerms . '%'); + + if (null !== $translation) { + $qb->andWhere($qb->expr()->eq('ns.translation', ':translation')) + ->setParameter('translation', $translation); + } + try { + return $qb->getQuery()->getResult(); + } catch (NoResultException $e) { + return []; + } } } diff --git a/src/SearchEngine/Indexer/AbstractIndexer.php b/src/SearchEngine/Indexer/AbstractIndexer.php index 94e9716c..ee723f7f 100644 --- a/src/SearchEngine/Indexer/AbstractIndexer.php +++ b/src/SearchEngine/Indexer/AbstractIndexer.php @@ -14,16 +14,22 @@ abstract class AbstractIndexer implements CliAwareIndexer { + private ClientRegistry $clientRegistry; + protected SolariumFactoryInterface $solariumFactory; protected LoggerInterface $logger; protected ?SymfonyStyle $io = null; + protected ManagerRegistry $managerRegistry; public function __construct( - protected readonly ClientRegistry $clientRegistry, - protected readonly ManagerRegistry $managerRegistry, - protected readonly SolariumFactoryInterface $solariumFactory, - readonly LoggerInterface $searchEngineLogger + ClientRegistry $clientRegistry, + ManagerRegistry $managerRegistry, + SolariumFactoryInterface $solariumFactory, + LoggerInterface $searchEngineLogger ) { + $this->solariumFactory = $solariumFactory; + $this->clientRegistry = $clientRegistry; $this->logger = $searchEngineLogger; + $this->managerRegistry = $managerRegistry; } /** diff --git a/src/SearchEngine/Indexer/DocumentIndexer.php b/src/SearchEngine/Indexer/DocumentIndexer.php index 7118e785..20992dff 100644 --- a/src/SearchEngine/Indexer/DocumentIndexer.php +++ b/src/SearchEngine/Indexer/DocumentIndexer.php @@ -61,8 +61,9 @@ public function reindexAll(): void ->createQueryBuilder('d') ->getQuery(); - $this->io?->title(get_class($this)); - $this->io?->progressStart((int) $countQuery->getSingleScalarResult()); + if (null !== $this->io) { + $this->io->progressStart((int) $countQuery->getSingleScalarResult()); + } foreach ($q->toIterable() as $row) { $solarium = $this->solariumFactory->createWithDocument($row); @@ -71,7 +72,9 @@ public function reindexAll(): void foreach ($solarium->getDocuments() as $document) { $buffer->addDocument($document); } - $this->io?->progressAdvance(); + if (null !== $this->io) { + $this->io->progressAdvance(); + } // detach from Doctrine, so that it can be Garbage-Collected immediately $this->managerRegistry->getManager()->detach($row); } @@ -80,6 +83,8 @@ public function reindexAll(): void // optimize the index $this->optimizeSolr(); - $this->io?->progressFinish(); + if (null !== $this->io) { + $this->io->progressFinish(); + } } } diff --git a/src/SearchEngine/Indexer/NodesSourcesIndexer.php b/src/SearchEngine/Indexer/NodesSourcesIndexer.php index 65279dd0..a7780845 100644 --- a/src/SearchEngine/Indexer/NodesSourcesIndexer.php +++ b/src/SearchEngine/Indexer/NodesSourcesIndexer.php @@ -115,8 +115,9 @@ public function reindexAll(int $batchCount = 1, int $batchNumber = 0): void */ $paginator = new Paginator($baseQb->getQuery(), true); - $this->io?->title(get_class($this)); - $this->io?->progressStart($count); + if (null !== $this->io) { + $this->io->progressStart($count); + } foreach ($paginator as $row) { $solarium = $this->solariumFactory->createWithNodesSources($row); @@ -124,7 +125,9 @@ public function reindexAll(int $batchCount = 1, int $batchNumber = 0): void $solarium->index(); $buffer->addDocument($solarium->getDocument()); - $this->io?->progressAdvance(); + if (null !== $this->io) { + $this->io->progressAdvance(); + } // detach from Doctrine, so that it can be Garbage-Collected immediately $this->managerRegistry->getManager()->detach($row); } @@ -134,6 +137,8 @@ public function reindexAll(int $batchCount = 1, int $batchNumber = 0): void // optimize the index $this->optimizeSolr(); - $this->io?->progressFinish(); + if (null !== $this->io) { + $this->io->progressFinish(); + } } } diff --git a/src/SearchEngine/Message/AbstractSolrMessage.php b/src/SearchEngine/Message/AbstractSolrMessage.php index 9ce292a4..16c215a0 100644 --- a/src/SearchEngine/Message/AbstractSolrMessage.php +++ b/src/SearchEngine/Message/AbstractSolrMessage.php @@ -9,8 +9,7 @@ abstract class AbstractSolrMessage implements AsyncMessage { /** - * Cannot typehint with class-string: breaks Symfony Serializer 5.4 - * @var string + * @var class-string */ protected string $classname; /** @@ -18,10 +17,6 @@ abstract class AbstractSolrMessage implements AsyncMessage */ protected mixed $identifier; - /** - * @param string $classname - * @param mixed $identifier - */ public function __construct(string $classname, mixed $identifier) { $this->classname = $classname; diff --git a/src/SearchEngine/Message/Handler/SolrDeleteMessageHandler.php b/src/SearchEngine/Message/Handler/SolrDeleteMessageHandler.php index 587b9680..bf208a6a 100644 --- a/src/SearchEngine/Message/Handler/SolrDeleteMessageHandler.php +++ b/src/SearchEngine/Message/Handler/SolrDeleteMessageHandler.php @@ -25,8 +25,6 @@ public function __invoke(SolrDeleteMessage $message): void { try { if (!empty($message->getIdentifier())) { - // Cannot typehint with class-string: breaks Symfony Serializer 5.4 - // @phpstan-ignore-next-line $this->indexerFactory->getIndexerFor($message->getClassname())->delete($message->getIdentifier()); } } catch (SolrServerNotAvailableException $exception) { diff --git a/src/SearchEngine/Message/Handler/SolrReindexMessageHandler.php b/src/SearchEngine/Message/Handler/SolrReindexMessageHandler.php index af3e006c..15b092f7 100644 --- a/src/SearchEngine/Message/Handler/SolrReindexMessageHandler.php +++ b/src/SearchEngine/Message/Handler/SolrReindexMessageHandler.php @@ -25,8 +25,6 @@ public function __invoke(SolrReindexMessage $message): void { try { if (!empty($message->getIdentifier())) { - // Cannot typehint with class-string: breaks Symfony Serializer 5.4 - // @phpstan-ignore-next-line $this->indexerFactory->getIndexerFor($message->getClassname())->index($message->getIdentifier()); } } catch (SolrServerNotAvailableException $exception) { diff --git a/src/SearchEngine/NodeSourceSearchHandler.php b/src/SearchEngine/NodeSourceSearchHandler.php index 56c6a166..3e41672a 100644 --- a/src/SearchEngine/NodeSourceSearchHandler.php +++ b/src/SearchEngine/NodeSourceSearchHandler.php @@ -9,7 +9,6 @@ use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\Tag; -use RZ\Roadiz\CoreBundle\SearchEngine\Event\NodeSourceSearchQueryEvent; /** * @package RZ\Roadiz\CoreBundle\SearchEngine @@ -25,6 +24,7 @@ class NodeSourceSearchHandler extends AbstractSearchHandler implements NodeSourc * @param array $args * @param integer $rows * @param bool $searchTags + * @param int $proximity Proximity matching: Lucene supports finding words are a within a specific distance away. * @param int $page * * @return array|null @@ -34,56 +34,52 @@ protected function nativeSearch( array $args = [], int $rows = 20, bool $searchTags = false, + int $proximity = 1, int $page = 1 ): ?array { - if (empty($q)) { - return null; - } - $query = $this->createSolrQuery($args, $rows, $page); - $queryTxt = $this->buildQuery($q, $args, $searchTags); + if (!empty($q)) { + $query = $this->createSolrQuery($args, $rows, $page); + $queryTxt = $this->buildQuery($q, $args, $searchTags, $proximity); - if ($this->boostByPublicationDate) { - $boost = '{!boost b=recip(ms(NOW,published_at_dt),3.16e-11,1,1)}'; - $queryTxt = $boost . $queryTxt; - } - if ($this->boostByUpdateDate) { - $boost = '{!boost b=recip(ms(NOW,updated_at_dt),3.16e-11,1,1)}'; - $queryTxt = $boost . $queryTxt; - } - if ($this->boostByCreationDate) { - $boost = '{!boost b=recip(ms(NOW,created_at_dt),3.16e-11,1,1)}'; - $queryTxt = $boost . $queryTxt; - } - - $query->setQuery($queryTxt); + if ($this->boostByPublicationDate) { + $boost = '{!boost b=recip(ms(NOW,published_at_dt),3.16e-11,1,1)}'; + $queryTxt = $boost . $queryTxt; + } + if ($this->boostByUpdateDate) { + $boost = '{!boost b=recip(ms(NOW,updated_at_dt),3.16e-11,1,1)}'; + $queryTxt = $boost . $queryTxt; + } + if ($this->boostByCreationDate) { + $boost = '{!boost b=recip(ms(NOW,created_at_dt),3.16e-11,1,1)}'; + $queryTxt = $boost . $queryTxt; + } - /* - * Only need these fields as Doctrine - * will do the rest. - */ - $query->setFields([ - 'score', - 'id', - 'document_type_s', - SolariumNodeSource::IDENTIFIER_KEY, - 'node_name_s', - 'locale_s', - ]); + $query->setQuery($queryTxt); - $this->logger->debug('[Solr] Request node-sources search…', [ - 'query' => $queryTxt, - 'fq' => $args["fq"] ?? [], - 'params' => $query->getParams(), - ]); + /* + * Only need these fields as Doctrine + * will do the rest. + */ + $query->setFields([ + 'score', + 'id', + 'document_type_s', + SolariumNodeSource::IDENTIFIER_KEY, + 'node_name_s', + 'locale_s', + ]); - /** @var NodeSourceSearchQueryEvent $event */ - $event = $this->eventDispatcher->dispatch( - new NodeSourceSearchQueryEvent($query, $args) - ); - $query = $event->getQuery(); + $this->logger->debug('[Solr] Request node-sources search…', [ + 'query' => $queryTxt, + 'fq' => $args["fq"] ?? [], + 'params' => $query->getParams(), + ]); - $solrRequest = $this->getSolr()->execute($query); - return $solrRequest->getData(); + $solrRequest = $this->getSolr()->execute($query); + return $solrRequest->getData(); + } else { + return null; + } } /** @@ -166,15 +162,15 @@ protected function argFqProcess(array &$args): array */ if (isset($args['publishedAt'])) { $tmp = "published_at_dt:"; - if (!is_array($args['publishedAt']) && $args['publishedAt'] instanceof \DateTimeInterface) { + if (!is_array($args['publishedAt']) && $args['publishedAt'] instanceof \DateTime) { $tmp .= $this->formatDateTimeToUTC($args['publishedAt']); } elseif ( isset($args['publishedAt'][0]) && $args['publishedAt'][0] === "BETWEEN" && isset($args['publishedAt'][1]) && - $args['publishedAt'][1] instanceof \DateTimeInterface && + $args['publishedAt'][1] instanceof \DateTime && isset($args['publishedAt'][2]) && - $args['publishedAt'][2] instanceof \DateTimeInterface + $args['publishedAt'][2] instanceof \DateTime ) { $tmp .= "[" . $this->formatDateTimeToUTC($args['publishedAt'][1]) . @@ -184,14 +180,14 @@ protected function argFqProcess(array &$args): array isset($args['publishedAt'][0]) && $args['publishedAt'][0] === "<=" && isset($args['publishedAt'][1]) && - $args['publishedAt'][1] instanceof \DateTimeInterface + $args['publishedAt'][1] instanceof \DateTime ) { $tmp .= "[* TO " . $this->formatDateTimeToUTC($args['publishedAt'][1]) . "]"; } elseif ( isset($args['publishedAt'][0]) && $args['publishedAt'][0] === ">=" && isset($args['publishedAt'][1]) && - $args['publishedAt'][1] instanceof \DateTimeInterface + $args['publishedAt'][1] instanceof \DateTime ) { $tmp .= "[" . $this->formatDateTimeToUTC($args['publishedAt'][1]) . " TO *]"; } diff --git a/src/SearchEngine/SearchHandlerInterface.php b/src/SearchEngine/SearchHandlerInterface.php index 9612829c..373c3a66 100644 --- a/src/SearchEngine/SearchHandlerInterface.php +++ b/src/SearchEngine/SearchHandlerInterface.php @@ -11,6 +11,7 @@ interface SearchHandlerInterface * @param array $args * @param int $rows Results per page * @param bool $searchTags Search in tags/folders too, even if a node don’t match + * @param int $proximity Proximity matching: Lucene supports finding words are a within a specific distance away. Default 10000000 * @param int $page Retrieve a specific page * * @return SearchResultsInterface Return an array of doctrine Entities (Document, NodesSources) @@ -20,6 +21,7 @@ public function search( array $args = [], int $rows = 20, bool $searchTags = false, + int $proximity = 1, int $page = 1 ): SearchResultsInterface; @@ -30,6 +32,7 @@ public function search( * @param array $args * @param int $rows * @param boolean $searchTags Search in tags/folders too, even if a node don’t match + * @param int $proximity Proximity matching: Lucene supports finding words are a within a specific distance away. * @param int $page * * @return SearchResultsInterface Return a SearchResultsInterface iterable object. @@ -39,6 +42,7 @@ public function searchWithHighlight( array $args = [], int $rows = 20, bool $searchTags = false, + int $proximity = 1, int $page = 1 ): SearchResultsInterface; diff --git a/src/SearchEngine/SearchResultsInterface.php b/src/SearchEngine/SearchResultsInterface.php index 8705097a..a4ffb6c2 100644 --- a/src/SearchEngine/SearchResultsInterface.php +++ b/src/SearchEngine/SearchResultsInterface.php @@ -4,15 +4,9 @@ namespace RZ\Roadiz\CoreBundle\SearchEngine; -/** - * @extends \Iterator - */ interface SearchResultsInterface extends \Iterator { public function getResultCount(): int; - /** - * @return array - */ public function getResultItems(): array; public function map(callable $callable): array; } diff --git a/src/SearchEngine/SolariumDocumentTranslation.php b/src/SearchEngine/SolariumDocumentTranslation.php index 8ea8b2fa..9f0fad0a 100644 --- a/src/SearchEngine/SolariumDocumentTranslation.php +++ b/src/SearchEngine/SolariumDocumentTranslation.php @@ -46,16 +46,15 @@ public function getDocumentId(): int|string public function getFieldsAssoc(bool $subResource = false): array { $event = new DocumentTranslationIndexingEvent($this->documentTranslation, [], $this); - /** @var DocumentTranslationIndexingEvent $event */ - $event = $this->dispatcher->dispatch($event); - return $event->getAssociations(); + + return $this->dispatcher->dispatch($event)->getAssociations(); } /** * Remove any document linked to current node-source. * * @param Query $update - * @return bool + * @return boolean */ public function clean(Query $update): bool { diff --git a/src/SearchEngine/SolariumLogger.php b/src/SearchEngine/SolariumLogger.php deleted file mode 100644 index 120b0a67..00000000 --- a/src/SearchEngine/SolariumLogger.php +++ /dev/null @@ -1,187 +0,0 @@ -logger = $searchEngineLogger; - $this->stopwatch = $stopwatch; - } - - public static function getSubscribedEvents(): array - { - return [ - SolariumEvents::PRE_EXECUTE_REQUEST => ['preExecuteRequest', 1000], - SolariumEvents::POST_EXECUTE_REQUEST => ['postExecuteRequest', -1000], - ]; - } - - public function log( - SolariumRequest $request, - ?SolariumResponse $response, - SolariumEndpoint $endpoint, - float $duration - ): void { - $this->queries[] = array( - 'request' => $request, - 'response' => $response, - 'duration' => $duration, - 'base_uri' => $this->getEndpointBaseUrl($endpoint), - ); - } - - public function collect(HttpRequest $request, HttpResponse $response, \Throwable $exception = null): void - { - if (isset($this->currentRequest)) { - $this->failCurrentRequest(); - } - - $time = 0; - foreach ($this->queries as $queryStruct) { - $time += $queryStruct['duration']; - } - $this->data = array( - 'queries' => $this->queries, - 'total_time' => $time, - ); - } - - public function getName(): string - { - return 'solarium'; - } - - public function getQueries(): array - { - return array_key_exists('queries', $this->data) ? $this->data['queries'] : []; - } - - public function getQueryCount(): int - { - return count($this->getQueries()); - } - - public function getTotalTime(): int - { - return array_key_exists('total_time', $this->data) ? $this->data['total_time'] : 0; - } - - public function preExecuteRequest(SolariumPreExecuteRequestEvent $event): void - { - if (isset($this->currentRequest)) { - $this->failCurrentRequest(); - } - - $this->stopwatch->start('solr', 'solr'); - - $this->currentRequest = $event->getRequest(); - $this->currentEndpoint = $event->getEndpoint(); - - $this->logger->debug($this->getEndpointBaseUrl($this->currentEndpoint) . $this->currentRequest->getUri()); - $this->currentStartTime = microtime(true); - } - - public function postExecuteRequest(SolariumPostExecuteRequestEvent $event): void - { - $endTime = microtime(true) - $this->currentStartTime; - if (!isset($this->currentRequest)) { - throw new \RuntimeException('Request not set'); - } - if ($this->currentRequest !== $event->getRequest()) { - throw new \RuntimeException('Requests differ'); - } - - if ($this->stopwatch->isStarted('solr')) { - $this->stopwatch->stop('solr'); - } - - $this->log($event->getRequest(), $event->getResponse(), $event->getEndpoint(), $endTime); - - $this->currentRequest = null; - $this->currentStartTime = null; - $this->currentEndpoint = null; - } - - public function failCurrentRequest(): void - { - $endTime = microtime(true) - $this->currentStartTime; - - if ($this->stopwatch->isStarted('solr')) { - $this->stopwatch->stop('solr'); - } - - $this->log($this->currentRequest, null, $this->currentEndpoint, $endTime); - - $this->currentRequest = null; - $this->currentStartTime = null; - $this->currentEndpoint = null; - } - - public function serialize(): string - { - return serialize($this->data); - } - - public function unserialize($data): void - { - $this->data = unserialize($data); - } - - public function reset(): void - { - $this->data = []; - $this->queries = []; - } - - public function __serialize(): array - { - return $this->data; - } - - public function __unserialize(array $data): void - { - $this->data = $data; - } - - private function getEndpointBaseUrl(SolariumEndpoint $endpoint): string - { - // Support for Solarium v4.2: getBaseUri() has been deprecated in favor of getCoreBaseUri() - return method_exists($endpoint, 'getCoreBaseUri') ? $endpoint->getCoreBaseUri() : $endpoint->getBaseUri(); - } - - public static function getTemplate(): ?string - { - return '@RoadizCore/DataCollector/solarium.html.twig'; - } -} diff --git a/src/SearchEngine/SolariumNodeSource.php b/src/SearchEngine/SolariumNodeSource.php index 0c9ecf23..cf21bdf5 100644 --- a/src/SearchEngine/SolariumNodeSource.php +++ b/src/SearchEngine/SolariumNodeSource.php @@ -51,9 +51,8 @@ public function getDocumentId(): int|string public function getFieldsAssoc(bool $subResource = false): array { $event = new NodesSourcesIndexingEvent($this->nodeSource, [], $this); - /** @var NodesSourcesIndexingEvent $event */ - $event = $this->dispatcher->dispatch($event); - return $event->getAssociations(); + + return $this->dispatcher->dispatch($event)->getAssociations(); } /** diff --git a/src/SearchEngine/SolrSearchResultItem.php b/src/SearchEngine/SolrSearchResultItem.php deleted file mode 100644 index 9f26b97c..00000000 --- a/src/SearchEngine/SolrSearchResultItem.php +++ /dev/null @@ -1,48 +0,0 @@ -> $highlighting - */ - public function __construct( - protected readonly object $item, - protected readonly array $highlighting = [] - ) { - } - - /** - * @return T - */ - #[ApiProperty] - #[Groups(['get'])] - public function getItem(): object - { - return $this->item; - } - - /** - * @return array> - */ - #[ApiProperty] - #[Groups(['get'])] - public function getHighlighting(): array - { - return $this->highlighting; - } -} diff --git a/src/SearchEngine/SolrSearchResults.php b/src/SearchEngine/SolrSearchResults.php index 50d7bc52..fe74fc47 100644 --- a/src/SearchEngine/SolrSearchResults.php +++ b/src/SearchEngine/SolrSearchResults.php @@ -8,41 +8,49 @@ use JMS\Serializer\Annotation as JMS; use RZ\Roadiz\CoreBundle\Entity\DocumentTranslation; use RZ\Roadiz\CoreBundle\Entity\NodesSources; -use Symfony\Component\Serializer\Attribute\Ignore; +use RZ\Roadiz\Documents\Models\DocumentInterface; /** * Wrapper over Solr search results and metas. + * + * @package RZ\Roadiz\CoreBundle\SearchEngine */ class SolrSearchResults implements SearchResultsInterface { - #[JMS\Exclude] - #[Ignore] + /** + * @JMS\Exclude() + */ + protected array $response; + /** + * @JMS\Exclude() + */ + protected ObjectManager $entityManager; + /** + * @JMS\Exclude() + */ protected int $position; - /** - * @var array|null + * @JMS\Exclude() */ - #[JMS\Exclude] - #[Ignore] protected ?array $resultItems; - public function __construct( - #[JMS\Exclude] - #[Ignore] - protected readonly array $response, - #[JMS\Exclude] - #[Ignore] - protected readonly ObjectManager $entityManager - ) { + /** + * @param array $response + * @param ObjectManager $entityManager + */ + public function __construct(array $response, ObjectManager $entityManager) + { + $this->response = $response; + $this->entityManager = $entityManager; $this->position = 0; $this->resultItems = null; } /** * @return int + * @JMS\Groups({"search_results"}) + * @JMS\VirtualProperty() */ - #[JMS\Groups(["search_results"])] - #[JMS\VirtualProperty()] public function getResultCount(): int { if ( @@ -54,10 +62,10 @@ public function getResultCount(): int } /** - * @return array + * @return array + * @JMS\Groups({"search_results"}) + * @JMS\VirtualProperty() */ - #[JMS\Groups(["search_results"])] - #[JMS\VirtualProperty()] public function getResultItems(): array { if (null === $this->resultItems) { @@ -66,16 +74,26 @@ public function getResultItems(): array isset($this->response['response']['docs']) ) { $this->resultItems = array_filter(array_map( - function (array $item) { + function ($item) { $object = $this->getHydratedItem($item); - if (!\is_object($object)) { - return null; + if (isset($this->response["highlighting"])) { + $key = 'object'; + if ($object instanceof NodesSources) { + $key = 'nodeSource'; + } + if ($object instanceof DocumentInterface) { + $key = 'document'; + } + if ($object instanceof DocumentTranslation) { + $key = 'document'; + $object = $object->getDocument(); + } + return [ + $key => $object, + 'highlighting' => $this->getHighlighting($item['id']), + ]; } - $highlighting = $this->getHighlighting($item['id']); - return new SolrSearchResultItem( - $object, - $highlighting - ); + return $object; }, $this->response['response']['docs'] )); @@ -86,18 +104,24 @@ function (array $item) { } /** - * Get highlighting for one field. - * This does not merge highlighting for all fields anymore. + * Merge collection_txt localized fields. * * @param string $id - * @return array + * @return array|array[]|mixed */ - protected function getHighlighting(string $id): array + protected function getHighlighting(string $id): mixed { - if (isset($this->response['highlighting'][$id]) && \is_array($this->response['highlighting'][$id])) { - return $this->response['highlighting'][$id]; + $highlights = $this->response['highlighting'][$id]; + if (!isset($highlights['collection_txt'])) { + $collectionTxt = []; + foreach ($highlights as $field => $value) { + $collectionTxt = array_merge($collectionTxt, $value); + } + $highlights = array_merge($highlights, [ + 'collection_txt' => $collectionTxt + ]); } - return []; + return $highlights; } /** @@ -125,11 +149,10 @@ protected function getHydratedItem(array $item): mixed $item[SolariumNodeSource::IDENTIFIER_KEY] ); case SolariumDocumentTranslation::DOCUMENT_TYPE: - $documentTranslation = $this->entityManager->find( + return $this->entityManager->find( DocumentTranslation::class, $item[SolariumDocumentTranslation::IDENTIFIER_KEY] ); - return $documentTranslation?->getDocument(); } } @@ -140,11 +163,11 @@ protected function getHydratedItem(array $item): mixed * Return the current element * * @link https://php.net/manual/en/iterator.current.php - * @return SolrSearchResultItem + * @return mixed Can return any type. * @since 5.0 */ #[\ReturnTypeWillChange] - public function current(): SolrSearchResultItem + public function current(): mixed { return $this->getResultItems()[$this->position]; } diff --git a/src/SearchEngine/Subscriber/AbstractIndexingSubscriber.php b/src/SearchEngine/Subscriber/AbstractIndexingSubscriber.php index 3bb5c452..12aa92b0 100644 --- a/src/SearchEngine/Subscriber/AbstractIndexingSubscriber.php +++ b/src/SearchEngine/Subscriber/AbstractIndexingSubscriber.php @@ -4,63 +4,14 @@ namespace RZ\Roadiz\CoreBundle\SearchEngine\Subscriber; +use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\String\Slugger\AsciiSlugger; abstract class AbstractIndexingSubscriber implements EventSubscriberInterface { - protected function flattenTextCollection(array $collection): string - { - return trim(implode(PHP_EOL, array_filter(array_unique($collection)))); - } - protected function formatDateTimeToUTC(\DateTimeInterface $dateTime): string { return gmdate('Y-m-d\TH:i:s\Z', $dateTime->getTimestamp()); } - - protected function formatGeoJsonFeature(mixed $geoJson): ?string - { - if (null === $geoJson) { - return null; - } - if (\is_string($geoJson)) { - $geoJson = \json_decode($geoJson, true); - } - if (!\is_array($geoJson)) { - return null; - } - - if ( - isset($geoJson['type']) && - $geoJson['type'] === 'Feature' && - isset($geoJson['geometry']['coordinates']) - ) { - return $geoJson['geometry']['coordinates'][1] . ',' . $geoJson['geometry']['coordinates'][0]; - } - return null; - } - - protected function formatGeoJsonFeatureCollection(mixed $geoJson): ?array - { - if (null === $geoJson) { - return null; - } - if (\is_string($geoJson)) { - $geoJson = \json_decode($geoJson, true); - } - if (!\is_array($geoJson)) { - return null; - } - if ( - isset($geoJson['type']) && - $geoJson['type'] === 'FeatureCollection' && - isset($geoJson['features']) && - \count($geoJson['features']) > 0 - ) { - return array_filter(array_map(function ($feature) { - return $this->formatGeoJsonFeature($feature); - }, $geoJson['features'])); - } - return null; - } } diff --git a/src/SearchEngine/Subscriber/AttributeValueIndexingSubscriber.php b/src/SearchEngine/Subscriber/AttributeValueIndexingSubscriber.php deleted file mode 100644 index 929a85e9..00000000 --- a/src/SearchEngine/Subscriber/AttributeValueIndexingSubscriber.php +++ /dev/null @@ -1,125 +0,0 @@ - ['onIndexing', 900], - ]; - } - - public function onIndexing(NodesSourcesIndexingEvent $event): void - { - if ($event->isSubResource()) { - return; - } - - $associations = $event->getAssociations(); - $attributeValues = $event->getNodeSource() - ->getNode() - ->getAttributesValuesForTranslation($event->getNodeSource()->getTranslation()); - - if ($attributeValues->count() === 0) { - return; - } - - $lang = $event->getNodeSource()->getTranslation()->getLocale(); - if ( - !\in_array( - $lang, - AbstractSolarium::$availableLocalizedTextFields - ) - ) { - $lang = null; - } - - /** @var AttributeValueInterface $attributeValue */ - foreach ($attributeValues as $attributeValue) { - if ($attributeValue->getAttribute()->isSearchable()) { - $data = $attributeValue->getAttributeValueTranslation( - $event->getNodeSource()->getTranslation() - )->getValue(); - if (null === $data) { - $data = $attributeValue->getAttributeValueTranslations()->first() ? - $attributeValue->getAttributeValueTranslations()->first()->getValue() - : null; - } - if (null !== $data) { - $fieldName = (new AsciiSlugger())->slug($attributeValue->getAttribute()->getCode())->snake()->lower()->toString(); - switch ($attributeValue->getType()) { - case AttributeInterface::INTEGER_T: - $fieldName .= '_i'; - $associations[$fieldName] = $data; - break; - case AttributeInterface::DECIMAL_T: - case AttributeInterface::PERCENT_T: - $fieldName .= '_f'; - $associations[$fieldName] = $data; - break; - case AttributeInterface::ENUM_T: - case AttributeInterface::COUNTRY_T: - case AttributeInterface::COLOUR_T: - case AttributeInterface::EMAIL_T: - $fieldName .= '_s'; - $content = $event->getSolariumDocument()->cleanTextContent($data); - $associations[$fieldName] = $content; - $associations['collection_txt'][] = $content; - if (null !== $lang) { - // Compile all text content into a single localized text field. - $associations['collection_txt_' . $lang] = $this->flattenTextCollection($associations['collection_txt']); - } - break; - case AttributeInterface::DATETIME_T: - case AttributeInterface::DATE_T: - if ($data instanceof \DateTimeInterface) { - $fieldName .= '_dt'; - $associations[$fieldName] = $this->formatDateTimeToUTC($data); - } - break; - case AttributeInterface::STRING_T: - /* - * Use locale to create field name - * with right language - */ - if (null !== $lang) { - $fieldName .= '_txt_' . $lang; - } else { - $lang = null; - $fieldName .= '_t'; - } - /* - * Strip Markdown syntax - */ - $content = $event->getSolariumDocument()->cleanTextContent($data); - if (null !== $content) { - $content = trim($content); - $associations[$fieldName] = $content; - $associations['collection_txt'][] = $content; - if (null !== $lang) { - // Compile all text content into a single localized text field. - $associations['collection_txt_' . $lang] = $this->flattenTextCollection($associations['collection_txt']); - } - } - break; - } - } - } - } - - $event->setAssociations($associations); - } -} diff --git a/src/SearchEngine/Subscriber/DefaultDocumentTranslationIndexingSubscriber.php b/src/SearchEngine/Subscriber/DefaultDocumentTranslationIndexingSubscriber.php index a4da9db0..66de4921 100644 --- a/src/SearchEngine/Subscriber/DefaultDocumentTranslationIndexingSubscriber.php +++ b/src/SearchEngine/Subscriber/DefaultDocumentTranslationIndexingSubscriber.php @@ -112,7 +112,7 @@ public function onIndexing(DocumentTranslationIndexingEvent $event): void */ $assoc['collection_txt'] = $collection; // Compile all text content into a single localized text field. - $assoc['collection_txt_' . $lang] = $this->flattenTextCollection($collection); + $assoc['collection_txt_' . $lang] = implode(PHP_EOL, $collection); $event->setAssociations($assoc); } } diff --git a/src/SearchEngine/Subscriber/DefaultNodesSourcesIndexingSubscriber.php b/src/SearchEngine/Subscriber/DefaultNodesSourcesIndexingSubscriber.php index f86bb8e1..0077e7ed 100644 --- a/src/SearchEngine/Subscriber/DefaultNodesSourcesIndexingSubscriber.php +++ b/src/SearchEngine/Subscriber/DefaultNodesSourcesIndexingSubscriber.php @@ -4,11 +4,12 @@ namespace RZ\Roadiz\CoreBundle\SearchEngine\Subscriber; +use Doctrine\Common\Collections\Criteria; +use RZ\Roadiz\Core\AbstractEntities\AbstractField; use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\NodeTypeField; use RZ\Roadiz\CoreBundle\Entity\Tag; use RZ\Roadiz\CoreBundle\Event\NodesSources\NodesSourcesIndexingEvent; -use RZ\Roadiz\CoreBundle\SearchEngine\AbstractSolarium; use RZ\Roadiz\CoreBundle\SearchEngine\SolariumNodeSource; final class DefaultNodesSourcesIndexingSubscriber extends AbstractIndexingSubscriber @@ -31,10 +32,18 @@ public function onIndexing(NodesSourcesIndexingEvent $event): void $collection = []; $node = $nodeSource->getNode(); + if (null === $node) { + throw new \RuntimeException("No node relation found for source: " . $nodeSource->getTitle(), 1); + } + // Need a documentType field - $assoc[AbstractSolarium::TYPE_DISCRIMINATOR] = SolariumNodeSource::DOCUMENT_TYPE; + $assoc[SolariumNodeSource::TYPE_DISCRIMINATOR] = SolariumNodeSource::DOCUMENT_TYPE; // Need a nodeSourceId field $assoc[SolariumNodeSource::IDENTIFIER_KEY] = $nodeSource->getId(); + $assoc['node_type_s'] = $node->getNodeType()->getName(); + $assoc['node_name_s'] = $node->getNodeName(); + $assoc['node_status_i'] = $node->getStatus(); + $assoc['node_visible_b'] = $node->isVisible(); // Need a locale field $locale = $nodeSource->getTranslation()->getLocale(); @@ -48,23 +57,17 @@ public function onIndexing(NodesSourcesIndexingEvent $event): void $assoc['title'] = $title; $assoc['title_txt_' . $lang] = $title; + $assoc['created_at_dt'] = $this->formatDateTimeToUTC($node->getCreatedAt()); + $assoc['updated_at_dt'] = $this->formatDateTimeToUTC($node->getUpdatedAt()); + + if (null !== $nodeSource->getPublishedAt()) { + $assoc['published_at_dt'] = $this->formatDateTimeToUTC($nodeSource->getPublishedAt()); + } + /* * Do not index locale and tags if this is a sub-resource */ if (!$subResource) { - $assoc['node_type_s'] = $nodeSource->getNodeTypeName(); - $assoc['node_name_s'] = $node->getNodeName(); - $assoc['slug_s'] = $node->getNodeName(); - $assoc['node_status_i'] = $node->getStatus(); - $assoc['node_visible_b'] = $node->isVisible(); - $assoc['node_reachable_b'] = $nodeSource->isReachable(); - $assoc['created_at_dt'] = $this->formatDateTimeToUTC($node->getCreatedAt()); - $assoc['updated_at_dt'] = $this->formatDateTimeToUTC($node->getUpdatedAt()); - - if (null !== $nodeSource->getPublishedAt()) { - $assoc['published_at_dt'] = $this->formatDateTimeToUTC($nodeSource->getPublishedAt()); - } - if ($this->canIndexTitleInCollection($nodeSource)) { $collection[] = $title; } @@ -110,61 +113,18 @@ function (Tag $tag) { $allOut = array_filter(array_unique($allOut)); // Use all_tags_slugs_ss to be compatible with other data types $assoc['all_tags_slugs_ss'] = $allOut; + } - $booleanFields = $node->getNodeType()->getFields()->filter(function (NodeTypeField $field) { - return $field->isBoolean(); - }); - $this->indexSuffixedFields($booleanFields, '_b', $nodeSource, $assoc); - - $numberFields = $node->getNodeType()->getFields()->filter(function (NodeTypeField $field) { - return $field->isInteger(); - }); - $this->indexSuffixedFields($numberFields, '_i', $nodeSource, $assoc); - - $decimalFields = $node->getNodeType()->getFields()->filter(function (NodeTypeField $field) { - return $field->isDecimal(); - }); - $this->indexSuffixedFields($decimalFields, '_f', $nodeSource, $assoc); - - $stringFields = $node->getNodeType()->getFields()->filter(function (NodeTypeField $field) { - return $field->isEnum() || $field->isCountry() || $field->isColor() || $field->isEmail(); - }); - $this->indexSuffixedFields($stringFields, '_s', $nodeSource, $assoc); - - $dateTimeFields = $node->getNodeType()->getFields()->filter(function (NodeTypeField $field) { - return $field->isDate() || $field->isDateTime(); - }); - $this->indexSuffixedFields($dateTimeFields, '_dt', $nodeSource, $assoc); + $criteria = new Criteria(); + $criteria->andWhere(Criteria::expr()->eq("type", AbstractField::BOOLEAN_T)); + $booleanFields = $node->getNodeType()->getFields()->matching($criteria); - /* - * Make sure your Solr managed-schema has a field named `*_p` with type `location` singleValued - * - */ - $pointFields = $node->getNodeType()->getFields()->filter(function (NodeTypeField $field) { - return $field->isGeoTag(); - }); - foreach ($pointFields as $field) { - $name = $field->getName(); - $name .= '_p'; - $getter = $field->getGetterName(); - $value = $nodeSource->$getter(); - $assoc[$name] = $this->formatGeoJsonFeature($value); - } - - /* - * Make sure your Solr managed-schema has a field named `*_ps` with type `location` multiValued - * - */ - $multiPointFields = $node->getNodeType()->getFields()->filter(function (NodeTypeField $field) { - return $field->isMultiGeoTag(); - }); - foreach ($multiPointFields as $field) { - $name = $field->getName(); - $name .= '_ps'; - $getter = $field->getGetterName(); - $value = $nodeSource->$getter(); - $assoc[$name] = $this->formatGeoJsonFeatureCollection($value); - } + /** @var NodeTypeField $booleanField */ + foreach ($booleanFields as $booleanField) { + $name = $booleanField->getName(); + $name .= '_b'; + $getter = $booleanField->getGetterName(); + $assoc[$name] = $nodeSource->$getter(); } $searchableFields = $node->getNodeType()->getSearchableFields(); @@ -196,32 +156,10 @@ function (Tag $tag) { */ $assoc['collection_txt'] = $collection; // Compile all text content into a single localized text field. - $assoc['collection_txt_' . $lang] = $this->flattenTextCollection($collection); + $assoc['collection_txt_' . $lang] = implode(PHP_EOL, $collection); $event->setAssociations($assoc); } - /** - * @param iterable $fields - * @param string $suffix - * @param NodesSources $nodeSource - * @param array $assoc - * @return void - */ - protected function indexSuffixedFields(iterable $fields, string $suffix, NodesSources $nodeSource, array &$assoc): void - { - foreach ($fields as $field) { - $name = $field->getName(); - $name .= $suffix; - $getter = $field->getGetterName(); - $value = $nodeSource->$getter(); - if ($value instanceof \DateTimeInterface) { - $assoc[$name] = $this->formatDateTimeToUTC($value); - } elseif (null !== $value) { - $assoc[$name] = $value; - } - } - } - /** * @param NodesSources $source * @return bool @@ -235,6 +173,9 @@ protected function canIndexTitleInCollection(NodesSources $source): bool return ((bool) $source->getShowTitle()); } - return $source->getNode()->getNodeType()->isSearchable(); + if (null !== $source->getNode() && $source->getNode()->getNodeType()) { + return $source->getNode()->getNodeType()->isSearchable(); + } + return true; } } diff --git a/src/SearchEngine/Subscriber/TreeWalkerIndexingEventSubscriber.php b/src/SearchEngine/Subscriber/TreeWalkerIndexingEventSubscriber.php deleted file mode 100644 index 76ccc9ef..00000000 --- a/src/SearchEngine/Subscriber/TreeWalkerIndexingEventSubscriber.php +++ /dev/null @@ -1,100 +0,0 @@ -walkerContext = $walkerContext; - $this->solariumFactory = $solariumFactory; - $this->maxLevel = $maxLevel; - $this->defaultLocale = $defaultLocale; - } - - /** - * @inheritDoc - */ - public static function getSubscribedEvents(): array - { - return [ - NodesSourcesIndexingEvent::class => ['onIndexing', -99], - ]; - } - - public function onIndexing(NodesSourcesIndexingEvent $event): void - { - $nodeSource = $event->getNodeSource(); - if (!$nodeSource->isReachable() || $event->isSubResource()) { - return; - } - - $assoc = $event->getAssociations(); - - $blockWalker = AutoChildrenNodeSourceWalker::build( - $nodeSource, - $this->walkerContext, - $this->maxLevel - ); - - // Need a locale field - $locale = $nodeSource->getTranslation()->getLocale(); - $lang = \Locale::getPrimaryLanguage($locale) ?? $this->defaultLocale; - - try { - foreach ($blockWalker->getChildren() as $subWalker) { - $this->walkAndIndex($subWalker, $assoc, $lang); - } - } catch (\Exception $e) { - } - - $event->setAssociations($assoc); - } - - /** - * @param WalkerInterface $walker - * @param array $assoc - * @param string $locale - * @throws \Exception - */ - protected function walkAndIndex(WalkerInterface $walker, array &$assoc, string $locale): void - { - $item = $walker->getItem(); - if ($item instanceof NodesSources) { - $solarium = $this->solariumFactory->createWithNodesSources($item); - // Fetch all fields array association AS sub-resources (i.e. do not index their title, and relationships) - $childAssoc = $solarium->getFieldsAssoc(true); - $assoc['collection_txt'] = array_filter(array_merge( - $assoc['collection_txt'], - $childAssoc['collection_txt'] - )); - $assoc['collection_txt_' . $locale] = $this->flattenTextCollection($assoc['collection_txt']); - } - if ($walker->count() > 0) { - foreach ($walker->getChildren() as $subWalker) { - $this->walkAndIndex($subWalker, $assoc, $locale); - } - } - } -} diff --git a/src/Security/Authentication/Manager/LoginAttemptManager.php b/src/Security/Authentication/Manager/LoginAttemptManager.php new file mode 100644 index 00000000..5e1e7ad1 --- /dev/null +++ b/src/Security/Authentication/Manager/LoginAttemptManager.php @@ -0,0 +1,160 @@ +requestStack = $requestStack; + $this->logger = $logger; + $this->managerRegistry = $managerRegistry; + } + + /** + * @param string $username + * @throws \Doctrine\ORM\NoResultException + * @throws \Doctrine\ORM\NonUniqueResultException + */ + public function checkLoginAttempts(string $username): void + { + /* + * Checks if there are more than 10 failed attempts + * from same IP address in the last 20 minutes + */ + if ( + $this->getLoginAttemptRepository()->isIpAddressBlocked( + $this->requestStack->getMainRequest()->getClientIp(), + $this->getIpAttemptGraceTime(), + $this->getIpAttemptCount() + ) + ) { + throw new TooManyLoginAttemptsException( + 'Too many login attempts for current IP address, wait before trying again.', + Response::HTTP_TOO_MANY_REQUESTS + ); + } + if ($this->getLoginAttemptRepository()->isUsernameBlocked($username)) { + throw new TooManyLoginAttemptsException( + 'Too many login attempts for this username, wait before trying again.', + Response::HTTP_TOO_MANY_REQUESTS + ); + } + } + + /** + * @param string $username + * + * @return $this + * @throws \Exception + */ + public function onFailedLoginAttempt(string $username): LoginAttemptManager + { + $manager = $this->managerRegistry->getManagerForClass(LoginAttempt::class); + if (null === $manager) { + throw new \RuntimeException('No manager found for class ' . LoginAttempt::class); + } + $loginAttempt = $this->getLoginAttemptRepository()->findOrCreateOneByIpAddressAndUsername( + $this->requestStack->getMainRequest()->getClientIp(), + $username + ); + + $loginAttempt->addAttemptCount(); + $blocksUntil = new \DateTime(); + + if ($loginAttempt->getAttemptCount() >= 9) { + $blocksUntil->add(new \DateInterval('PT30M')); + $loginAttempt->setBlocksLoginUntil($blocksUntil); + $this->logger->info(sprintf( + 'Client has been blocked from login until %s', + $blocksUntil->format('Y-m-d H:i:s') + )); + } elseif ($loginAttempt->getAttemptCount() >= 6) { + $blocksUntil->add(new \DateInterval('PT15M')); + $loginAttempt->setBlocksLoginUntil($blocksUntil); + $this->logger->info(sprintf( + 'Client has been blocked from login until %s', + $blocksUntil->format('Y-m-d H:i:s') + )); + } elseif ($loginAttempt->getAttemptCount() >= 3) { + $blocksUntil->add(new \DateInterval('PT3M')); + $loginAttempt->setBlocksLoginUntil($blocksUntil); + $this->logger->info(sprintf( + 'Client has been blocked from login until %s', + $blocksUntil->format('Y-m-d H:i:s') + )); + } + $manager->flush(); + return $this; + } + + /** + * @return LoginAttemptRepository + */ + public function getLoginAttemptRepository(): LoginAttemptRepository + { + if (null === $this->loginAttemptRepository) { + $this->loginAttemptRepository = $this->managerRegistry->getRepository(LoginAttempt::class); + } + return $this->loginAttemptRepository; + } + + /** + * @param string $username + * + * @return $this + */ + public function onSuccessLoginAttempt(string $username) + { + $this->getLoginAttemptRepository()->resetLoginAttempts( + $this->requestStack->getMainRequest()->getClientIp(), + $username + ); + return $this; + } + + /** + * @return int + */ + public function getIpAttemptGraceTime(): int + { + return $this->ipAttemptGraceTime; + } + + /** + * @return int + */ + public function getIpAttemptCount(): int + { + return $this->ipAttemptCount; + } +} diff --git a/src/Security/Authentication/RoadizAuthenticator.php b/src/Security/Authentication/RoadizAuthenticator.php index 6c22dc08..0d4673d4 100644 --- a/src/Security/Authentication/RoadizAuthenticator.php +++ b/src/Security/Authentication/RoadizAuthenticator.php @@ -16,7 +16,7 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; -use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge; @@ -117,7 +117,7 @@ private function getCredentials(Request $request): array throw new BadRequestHttpException(sprintf('The key "%s" must be a string.', $this->usernamePath)); } - if (\mb_strlen($credentials['username']) > Security::MAX_USERNAME_LENGTH) { + if (\strlen($credentials['username']) > Security::MAX_USERNAME_LENGTH) { throw new BadCredentialsException('Invalid username.'); } } catch (AccessException $e) { diff --git a/src/Security/Authorization/AccessDeniedHandler.php b/src/Security/Authorization/AccessDeniedHandler.php index ff05c9e3..adf23d44 100644 --- a/src/Security/Authorization/AccessDeniedHandler.php +++ b/src/Security/Authorization/AccessDeniedHandler.php @@ -44,7 +44,7 @@ public function __construct( } /** - * Handles access denied failure redirecting to home page + * Handles an access denied failure redirecting to home page * * @param Request $request * @param AccessDeniedException $accessDeniedException diff --git a/src/Security/Authorization/Voter/GroupVoter.php b/src/Security/Authorization/Voter/GroupVoter.php index a6d8a341..10bcfb52 100644 --- a/src/Security/Authorization/Voter/GroupVoter.php +++ b/src/Security/Authorization/Voter/GroupVoter.php @@ -13,8 +13,11 @@ class GroupVoter extends RoleVoter { - public function __construct(private readonly RoleHierarchyInterface $roleHierarchy, string $prefix = 'ROLE_') + private RoleHierarchyInterface $roleHierarchy; + + public function __construct(RoleHierarchyInterface $roleHierarchy, string $prefix = 'ROLE_') { + $this->roleHierarchy = $roleHierarchy; parent::__construct($prefix); } @@ -93,6 +96,11 @@ protected function extractGroupRoles(Group $group): array */ protected function isRoleContained(string $role, array $roles): bool { - return \in_array($role, $roles, true); + foreach ($roles as $singleRole) { + if ($role === $singleRole) { + return true; + } + } + return false; } } diff --git a/src/Security/Authorization/Voter/NodeTypeFieldVoter.php b/src/Security/Authorization/Voter/NodeTypeFieldVoter.php index eb55732c..4deaa5cf 100644 --- a/src/Security/Authorization/Voter/NodeTypeFieldVoter.php +++ b/src/Security/Authorization/Voter/NodeTypeFieldVoter.php @@ -5,9 +5,9 @@ namespace RZ\Roadiz\CoreBundle\Security\Authorization\Voter; use RZ\Roadiz\CoreBundle\Entity\NodeTypeField; -use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; +use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\User\UserInterface; final class NodeTypeFieldVoter extends Voter @@ -15,7 +15,7 @@ final class NodeTypeFieldVoter extends Voter public const VIEW = 'VIEW'; public function __construct( - private readonly Security $security + private Security $security ) { } diff --git a/src/Security/Authorization/Voter/NodeVoter.php b/src/Security/Authorization/Voter/NodeVoter.php index 4cfbe0aa..3655a23c 100644 --- a/src/Security/Authorization/Voter/NodeVoter.php +++ b/src/Security/Authorization/Voter/NodeVoter.php @@ -4,18 +4,17 @@ namespace RZ\Roadiz\CoreBundle\Security\Authorization\Voter; +use Psr\Cache\CacheItemPoolInterface; +use RZ\Roadiz\Core\Handlers\HandlerFactoryInterface; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\NodesSources; -use RZ\Roadiz\CoreBundle\Node\NodeOffspringResolverInterface; +use RZ\Roadiz\CoreBundle\EntityHandler\NodeHandler; use RZ\Roadiz\CoreBundle\Security\Authorization\Chroot\NodeChrootResolver; -use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; +use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\User\UserInterface; -/** - * @extends Voter<'CREATE'|'DUPLICATE'|'CREATE_AT_ROOT'|'SEARCH'|'READ'|'READ_AT_ROOT'|'EMPTY_TRASH'|'READ_LOGS'|'EDIT_CONTENT'|'EDIT_TAGS'|'EDIT_REALMS'|'EDIT_SETTING'|'EDIT_STATUS'|'EDIT_ATTRIBUTE'|'DELETE', Node> - */ final class NodeVoter extends Voter { public const CREATE = 'CREATE'; @@ -35,13 +34,14 @@ final class NodeVoter extends Voter public const DELETE = 'DELETE'; public function __construct( - private readonly NodeChrootResolver $chrootResolver, - private readonly Security $security, - private readonly NodeOffspringResolverInterface $nodeOffspringResolver, + private NodeChrootResolver $chrootResolver, + private Security $security, + private HandlerFactoryInterface $handlerFactory, + private CacheItemPoolInterface $cache ) { } - protected function supports(string $attribute, mixed $subject): bool + protected function supports(string $attribute, $subject): bool { if ( \in_array($attribute, [ @@ -79,7 +79,7 @@ protected function supports(string $attribute, mixed $subject): bool return false; } - protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool { $user = $token->getUser(); @@ -122,7 +122,18 @@ private function isNodeInsideUserChroot(Node $node, Node $chroot, bool $includeC * Test if node is inside user chroot using all Chroot node offspring ids * to be able to cache all results. */ - return \in_array($node->getId(), $this->nodeOffspringResolver->getAllOffspringIds($chroot), true); + $cacheItem = $this->cache->getItem('node_offspring_ids_' . $chroot->getId()); + if (!$cacheItem->isHit()) { + /** @var NodeHandler $nodeHandler */ + $nodeHandler = $this->handlerFactory->getHandler($chroot); + $offspringIds = $nodeHandler->getAllOffspringId(); + $cacheItem->set($offspringIds); + $this->cache->save($cacheItem); + } else { + $offspringIds = $cacheItem->get(); + } + + return \in_array($node->getId(), $offspringIds, true); } private function isGrantedWithUserChroot(Node $node, UserInterface $user, array|string $roles, bool $includeChroot): bool diff --git a/src/Security/Authorization/Voter/RealmVoter.php b/src/Security/Authorization/Voter/RealmVoter.php index 204508da..7e61ec29 100644 --- a/src/Security/Authorization/Voter/RealmVoter.php +++ b/src/Security/Authorization/Voter/RealmVoter.php @@ -4,25 +4,26 @@ namespace RZ\Roadiz\CoreBundle\Security\Authorization\Voter; +use RZ\Roadiz\CoreBundle\Entity\Realm; use RZ\Roadiz\CoreBundle\Model\RealmInterface; -use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; +use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\User\UserInterface; -/** - * @extends Voter<'read'|'password', RealmInterface> - */ final class RealmVoter extends Voter { public const READ = 'read'; public const PASSWORD_QUERY_PARAMETER = 'password'; - public function __construct( - private readonly Security $security, - private readonly RequestStack $requestStack - ) { + private Security $security; + private RequestStack $requestStack; + + public function __construct(Security $security, RequestStack $requestStack) + { + $this->security = $security; + $this->requestStack = $requestStack; } public function supportsAttribute(string $attribute): bool @@ -30,25 +31,28 @@ public function supportsAttribute(string $attribute): bool return $attribute === self::READ; } - protected function supports(string $attribute, mixed $subject): bool + protected function supports(string $attribute, $subject): bool { return $this->supportsAttribute($attribute) && $subject instanceof RealmInterface; } /** * @param string $attribute - * @param RealmInterface $subject + * @param Realm $subject * @param TokenInterface $token * @return bool */ - public function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool + public function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool { - return match ($subject->getType()) { - RealmInterface::TYPE_PLAIN_PASSWORD => $this->voteForPassword($attribute, $subject, $token), - RealmInterface::TYPE_USER => $this->voteForUser($attribute, $subject, $token), - RealmInterface::TYPE_ROLE => $this->voteForRole($attribute, $subject, $token), - default => false, - }; + switch ($subject->getType()) { + case RealmInterface::TYPE_PLAIN_PASSWORD: + return $this->voteForPassword($attribute, $subject, $token); + case RealmInterface::TYPE_USER: + return $this->voteForUser($attribute, $subject, $token); + case RealmInterface::TYPE_ROLE: + return $this->voteForRole($attribute, $subject, $token); + } + return false; } /** @@ -57,7 +61,7 @@ public function voteOnAttribute(string $attribute, mixed $subject, TokenInterfac * @param TokenInterface $token * @return bool */ - private function voteForRole(string $attribute, RealmInterface $subject, TokenInterface $token): bool + private function voteForRole(string $attribute, $subject, TokenInterface $token): bool { if (null === $role = $subject->getRole()) { return false; @@ -71,7 +75,7 @@ private function voteForRole(string $attribute, RealmInterface $subject, TokenIn * @param TokenInterface $token * @return bool */ - private function voteForUser(string $attribute, RealmInterface $subject, TokenInterface $token): bool + private function voteForUser(string $attribute, $subject, TokenInterface $token): bool { if ($subject->getUsers()->count() === 0 || null === $token->getUser()) { return false; @@ -87,7 +91,7 @@ private function voteForUser(string $attribute, RealmInterface $subject, TokenIn * @param TokenInterface $token * @return bool */ - private function voteForPassword(string $attribute, RealmInterface $subject, TokenInterface $token): bool + private function voteForPassword(string $attribute, $subject, TokenInterface $token): bool { $request = $this->requestStack->getCurrentRequest(); if (null === $request || empty($subject->getPlainPassword())) { diff --git a/src/Security/Authorization/Voter/RoleArrayVoter.php b/src/Security/Authorization/Voter/RoleArrayVoter.php index b373a3c2..21ae892f 100644 --- a/src/Security/Authorization/Voter/RoleArrayVoter.php +++ b/src/Security/Authorization/Voter/RoleArrayVoter.php @@ -40,8 +40,10 @@ public function vote(TokenInterface $token, $subject, array $attributes): int } $result = VoterInterface::ACCESS_DENIED; - if (\in_array($singleAttribute, $roles, true)) { - return VoterInterface::ACCESS_GRANTED; + foreach ($roles as $role) { + if ($singleAttribute === $role) { + return VoterInterface::ACCESS_GRANTED; + } } } } diff --git a/src/Security/Authorization/Voter/SuperAdminRoleHierarchyVoter.php b/src/Security/Authorization/Voter/SuperAdminRoleHierarchyVoter.php index 23ccd845..e893fcbb 100644 --- a/src/Security/Authorization/Voter/SuperAdminRoleHierarchyVoter.php +++ b/src/Security/Authorization/Voter/SuperAdminRoleHierarchyVoter.php @@ -10,9 +10,16 @@ final class SuperAdminRoleHierarchyVoter extends RoleArrayVoter { - public function __construct(private readonly ManagerRegistry $managerRegistry, string $prefix = 'ROLE_') + private ManagerRegistry $managerRegistry; + + /** + * @param ManagerRegistry $managerRegistry + * @param string $prefix + */ + public function __construct(ManagerRegistry $managerRegistry, string $prefix = 'ROLE_') { parent::__construct($prefix); + $this->managerRegistry = $managerRegistry; } protected function extractRoles(TokenInterface $token): array @@ -30,10 +37,7 @@ protected function extractRoles(TokenInterface $token): array private function isSuperAdmin(TokenInterface $token): bool { $roleNames = parent::extractRoles($token); - if ( - \in_array('ROLE_SUPER_ADMIN', $roleNames) || - \in_array('ROLE_SUPERADMIN', $roleNames) - ) { + if (\in_array('ROLE_SUPER_ADMIN', $roleNames) || \in_array('ROLE_SUPERADMIN', $roleNames)) { return true; } return false; diff --git a/src/Security/User/UserViewer.php b/src/Security/User/UserViewer.php index c6c07db9..e8681701 100644 --- a/src/Security/User/UserViewer.php +++ b/src/Security/User/UserViewer.php @@ -6,10 +6,11 @@ use Psr\Log\LoggerInterface; use RZ\Roadiz\CoreBundle\Bag\Settings; +use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\User; use RZ\Roadiz\CoreBundle\Mailer\EmailManager; use Symfony\Cmf\Component\Routing\RouteObjectInterface; -use Symfony\Component\Mailer\Exception\TransportExceptionInterface; +use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Contracts\Translation\TranslatorInterface; @@ -20,6 +21,7 @@ class UserViewer protected TranslatorInterface $translator; protected EmailManager $emailManager; protected LoggerInterface $logger; + protected ?User $user = null; public function __construct( Settings $settingsBag, @@ -38,20 +40,21 @@ public function __construct( /** * Send email to reset user password. * - * @param User $user - * @param object|string $route + * @param string|NodesSources $route * @param string $htmlTemplate * @param string $txtTemplate * * @return bool - * @throws TransportExceptionInterface + * @throws \Exception */ public function sendPasswordResetLink( - User $user, - object|string $route = 'loginResetPage', + $route = 'loginResetPage', string $htmlTemplate = '@RoadizCore/email/users/reset_password_email.html.twig', string $txtTemplate = '@RoadizCore/email/users/reset_password_email.txt.twig' ): bool { + if (null === $this->user) { + throw new \InvalidArgumentException('User should be defined before sending email.'); + } $emailContact = $this->getContactEmail(); $siteName = $this->getSiteName(); @@ -59,7 +62,7 @@ public function sendPasswordResetLink( $resetLink = $this->urlGenerator->generate( $route, [ - 'token' => $user->getConfirmationToken(), + 'token' => $this->user->getConfirmationToken(), ], UrlGeneratorInterface::ABSOLUTE_URL ); @@ -68,14 +71,14 @@ public function sendPasswordResetLink( RouteObjectInterface::OBJECT_BASED_ROUTE_NAME, [ RouteObjectInterface::ROUTE_OBJECT => $route, - 'token' => $user->getConfirmationToken(), + 'token' => $this->user->getConfirmationToken(), ], UrlGeneratorInterface::ABSOLUTE_URL ); } $this->emailManager->setAssignation([ 'resetLink' => $resetLink, - 'user' => $user, + 'user' => $this->user, 'site' => $siteName, 'mailContact' => $emailContact, ]); @@ -84,20 +87,18 @@ public function sendPasswordResetLink( $this->emailManager->setSubject($this->translator->trans( 'reset.password.request' )); + $this->emailManager->setReceiver($this->user->getEmail()); + $this->emailManager->setSender([$emailContact => $siteName]); try { - $this->emailManager->setReceiver($user->getEmail()); - $this->emailManager->setSender([$emailContact => $siteName]); - // Send the message $this->emailManager->send(); return true; - } catch (\Exception $e) { + } catch (TransportException $e) { // Silent error not to prevent user creation if mailer is not configured $this->logger->error('Unable to send password reset link', [ 'exception' => get_class($e), 'message' => $e->getMessage(), - 'entity' => $user, ]); return false; } @@ -128,4 +129,22 @@ protected function getSiteName(): string return $siteName; } + + /** + * @return null|User + */ + public function getUser(): ?User + { + return $this->user; + } + + /** + * @param null|User $user + * @return UserViewer + */ + public function setUser(?User $user) + { + $this->user = $user; + return $this; + } } diff --git a/src/Serializer/CircularReferenceHandler.php b/src/Serializer/CircularReferenceHandler.php index 8b5055d7..eaec4ef9 100644 --- a/src/Serializer/CircularReferenceHandler.php +++ b/src/Serializer/CircularReferenceHandler.php @@ -4,24 +4,28 @@ namespace RZ\Roadiz\CoreBundle\Serializer; -use ApiPlatform\Api\IriConverterInterface; -use ApiPlatform\Api\UrlGeneratorInterface; +use ApiPlatform\Core\Api\IriConverterInterface; final class CircularReferenceHandler { - public function __construct(private readonly IriConverterInterface $iriConverter) + private IriConverterInterface $iriConverter; + + /** + * @param IriConverterInterface $iriConverter + */ + public function __construct(IriConverterInterface $iriConverter) { + $this->iriConverter = $iriConverter; } - public function __invoke(mixed $object, string $format, array $context): ?string + /** + * @param mixed $object + * @return string + */ + public function __invoke($object, string $format = null, array $context = []) { try { - return $this->iriConverter->getIriFromResource( - $object, - UrlGeneratorInterface::ABS_PATH, - null, - $context - ); + return $this->iriConverter->getIriFromItem($object); } catch (\InvalidArgumentException $exception) { if (is_object($object) && method_exists($object, 'getId')) { return (string) $object->getId(); diff --git a/src/Serializer/Normalizer/AbstractPathNormalizer.php b/src/Serializer/Normalizer/AbstractPathNormalizer.php index 729e3ae5..e66fb474 100644 --- a/src/Serializer/Normalizer/AbstractPathNormalizer.php +++ b/src/Serializer/Normalizer/AbstractPathNormalizer.php @@ -5,12 +5,13 @@ namespace RZ\Roadiz\CoreBundle\Serializer\Normalizer; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\SerializerAwareInterface; use Symfony\Component\Serializer\SerializerInterface; -abstract class AbstractPathNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface +abstract class AbstractPathNormalizer implements ContextAwareNormalizerInterface, DenormalizerInterface, SerializerAwareInterface { protected UrlGeneratorInterface $urlGenerator; /** @@ -28,27 +29,27 @@ public function __construct(NormalizerInterface $decorated, UrlGeneratorInterfac $this->urlGenerator = $urlGenerator; } - public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool + public function supportsNormalization($data, $format = null, array $context = []): bool { - return $this->decorated->supportsNormalization($data, $format/*, $context*/); + return $this->decorated->supportsNormalization($data, $format); } - public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + public function supportsDenormalization($data, $type, $format = null): bool { - return $this->decorated->supportsDenormalization($data, $type, $format/*, $context*/); + return $this->decorated->supportsDenormalization($data, $type, $format); } /** * @param mixed $data - * @param string $type + * @param string $class * @param string|null $format * @param array $context * @return mixed * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface */ - public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed + public function denormalize($data, $class, $format = null, array $context = []) { - return $this->decorated->denormalize($data, $type, $format, $context); + return $this->decorated->denormalize($data, $class, $format, $context); } public function setSerializer(SerializerInterface $serializer): void @@ -57,11 +58,4 @@ public function setSerializer(SerializerInterface $serializer): void $this->decorated->setSerializer($serializer); } } - - public function getSupportedTypes(?string $format): array - { - return [ - '*' => false, - ]; - } } diff --git a/src/Serializer/Normalizer/AttributeValueNormalizer.php b/src/Serializer/Normalizer/AttributeValueNormalizer.php index b125a1a3..543d865d 100644 --- a/src/Serializer/Normalizer/AttributeValueNormalizer.php +++ b/src/Serializer/Normalizer/AttributeValueNormalizer.php @@ -21,7 +21,7 @@ final class AttributeValueNormalizer extends AbstractPathNormalizer * @return array|\ArrayObject|bool|float|int|mixed|string|null * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface */ - public function normalize(mixed $object, ?string $format = null, array $context = []): mixed + public function normalize($object, $format = null, array $context = []) { $data = $this->decorated->normalize($object, $format, $context); if ($object instanceof AttributeValue && is_array($data)) { @@ -31,25 +31,15 @@ public function normalize(mixed $object, ?string $format = null, array $context $data['type'] = $object->getType(); $data['code'] = $object->getAttribute()->getCode(); $data['color'] = $object->getAttribute()->getColor(); - $data['weight'] = $object->getAttribute()->getWeight(); if (isset($context['translation']) && $context['translation'] instanceof TranslationInterface) { $translatedData = $object->getAttributeValueTranslation($context['translation']); $data['label'] = $object->getAttribute()->getLabelOrCode($context['translation']); - if ( - $translatedData instanceof AttributeValueTranslationInterface && - $translatedData->getValue() !== null - ) { + if ($translatedData instanceof AttributeValueTranslationInterface) { $data['value'] = $translatedData->getValue(); - } else { - $data['value'] = $object->getAttributeValueDefaultTranslation()?->getValue(); } } - if ($data['value'] instanceof \DateTimeInterface) { - $data['value'] = $data['value']->format(\DateTimeInterface::ATOM); - } - if (\in_array('attribute_documents', $serializationGroups, true)) { $documentsContext = $context; $documentsContext['groups'] = ['document_display']; diff --git a/src/Serializer/Normalizer/CustomFormNormalizer.php b/src/Serializer/Normalizer/CustomFormNormalizer.php index 6faf3844..114414ea 100644 --- a/src/Serializer/Normalizer/CustomFormNormalizer.php +++ b/src/Serializer/Normalizer/CustomFormNormalizer.php @@ -5,6 +5,7 @@ namespace RZ\Roadiz\CoreBundle\Serializer\Normalizer; use RZ\Roadiz\CoreBundle\Entity\CustomForm; +use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; use Symfony\Component\String\Slugger\AsciiSlugger; /** @@ -19,7 +20,7 @@ final class CustomFormNormalizer extends AbstractPathNormalizer * @return array|\ArrayObject|bool|float|int|mixed|string|null * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface */ - public function normalize(mixed $object, ?string $format = null, array $context = []): mixed + public function normalize($object, $format = null, array $context = []) { $data = $this->decorated->normalize($object, $format, $context); if ($object instanceof CustomForm && is_array($data)) { diff --git a/src/Serializer/Normalizer/DocumentNormalizer.php b/src/Serializer/Normalizer/DocumentNormalizer.php index b11d4d95..918e6aee 100644 --- a/src/Serializer/Normalizer/DocumentNormalizer.php +++ b/src/Serializer/Normalizer/DocumentNormalizer.php @@ -8,6 +8,7 @@ use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; use RZ\Roadiz\CoreBundle\Entity\Document; use RZ\Roadiz\CoreBundle\Entity\DocumentTranslation; +use RZ\Roadiz\CoreBundle\Entity\Folder; use RZ\Roadiz\Documents\MediaFinders\EmbedFinderFactory; use RZ\Roadiz\Documents\Models\FolderInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -18,13 +19,18 @@ */ final class DocumentNormalizer extends AbstractPathNormalizer { + private FilesystemOperator $documentsStorage; + private EmbedFinderFactory $embedFinderFactory; + public function __construct( + FilesystemOperator $documentsStorage, NormalizerInterface $decorated, UrlGeneratorInterface $urlGenerator, - private readonly FilesystemOperator $documentsStorage, - private readonly EmbedFinderFactory $embedFinderFactory + EmbedFinderFactory $embedFinderFactory ) { parent::__construct($decorated, $urlGenerator); + $this->documentsStorage = $documentsStorage; + $this->embedFinderFactory = $embedFinderFactory; } /** @@ -34,7 +40,7 @@ public function __construct( * @return array|\ArrayObject|bool|float|int|string|null * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface */ - public function normalize(mixed $object, ?string $format = null, array $context = []): mixed + public function normalize($object, $format = null, array $context = []) { $data = $this->decorated->normalize($object, $format, $context); if ( @@ -55,14 +61,6 @@ public function normalize(mixed $object, ?string $format = null, array $context } } - if ( - !$object->isPrivate() && - $object->isProcessable() && - null !== $alignment = $object->getImageCropAlignment() - ) { - $data['imageCropAlignment'] = $alignment; - } - if ( \in_array('document_folders_all', $serializationGroups, true) ) { diff --git a/src/Serializer/Normalizer/DocumentSourcesNormalizer.php b/src/Serializer/Normalizer/DocumentSourcesNormalizer.php index e52f4004..e61ef468 100644 --- a/src/Serializer/Normalizer/DocumentSourcesNormalizer.php +++ b/src/Serializer/Normalizer/DocumentSourcesNormalizer.php @@ -11,12 +11,15 @@ final class DocumentSourcesNormalizer extends AbstractPathNormalizer { + protected DocumentFinderInterface $documentFinder; + public function __construct( NormalizerInterface $decorated, UrlGeneratorInterface $urlGenerator, - private readonly DocumentFinderInterface $documentFinder + DocumentFinderInterface $documentFinder ) { parent::__construct($decorated, $urlGenerator); + $this->documentFinder = $documentFinder; } /** @@ -26,7 +29,7 @@ public function __construct( * @return array|\ArrayObject|bool|float|int|mixed|string|null * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface */ - public function normalize(mixed $object, ?string $format = null, array $context = []): mixed + public function normalize($object, $format = null, array $context = []) { $data = $this->decorated->normalize($object, $format, $context); if ($object instanceof Document && is_array($data)) { diff --git a/src/Serializer/Normalizer/FolderNormalizer.php b/src/Serializer/Normalizer/FolderNormalizer.php index a3f86112..b50a7642 100644 --- a/src/Serializer/Normalizer/FolderNormalizer.php +++ b/src/Serializer/Normalizer/FolderNormalizer.php @@ -20,7 +20,7 @@ final class FolderNormalizer extends AbstractPathNormalizer * @return array|\ArrayObject|bool|float|int|mixed|string|null * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface */ - public function normalize(mixed $object, ?string $format = null, array $context = []): mixed + public function normalize($object, $format = null, array $context = []) { $data = $this->decorated->normalize($object, $format, $context); if ($object instanceof Folder && is_array($data)) { diff --git a/src/Serializer/Normalizer/NodesSourcesPathNormalizer.php b/src/Serializer/Normalizer/NodesSourcesPathNormalizer.php index 650ff2e0..e6c63040 100644 --- a/src/Serializer/Normalizer/NodesSourcesPathNormalizer.php +++ b/src/Serializer/Normalizer/NodesSourcesPathNormalizer.php @@ -17,7 +17,7 @@ final class NodesSourcesPathNormalizer extends AbstractPathNormalizer * @return array|\ArrayObject|bool|float|int|mixed|string|null * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface */ - public function normalize(mixed $object, ?string $format = null, array $context = []): mixed + public function normalize($object, $format = null, array $context = []) { $data = $this->decorated->normalize($object, $format, $context); if ( diff --git a/src/Serializer/Normalizer/RealmSerializationGroupNormalizer.php b/src/Serializer/Normalizer/RealmSerializationGroupNormalizer.php index a470c7eb..f51583b7 100644 --- a/src/Serializer/Normalizer/RealmSerializationGroupNormalizer.php +++ b/src/Serializer/Normalizer/RealmSerializationGroupNormalizer.php @@ -4,59 +4,52 @@ namespace RZ\Roadiz\CoreBundle\Serializer\Normalizer; +use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\CoreBundle\Entity\NodesSources; -use RZ\Roadiz\CoreBundle\Model\RealmInterface; -use RZ\Roadiz\CoreBundle\Realm\RealmResolver; +use RZ\Roadiz\CoreBundle\Entity\Realm; use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\RealmVoter; -use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Security; +use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Stopwatch\Stopwatch; -final class RealmSerializationGroupNormalizer implements NormalizerInterface, NormalizerAwareInterface +final class RealmSerializationGroupNormalizer implements ContextAwareNormalizerInterface, NormalizerAwareInterface { use NormalizerAwareTrait; private const ALREADY_CALLED = 'REALM_SERIALIZER_NORMALIZER_ALREADY_CALLED'; + private Security $security; + private ManagerRegistry $managerRegistry; - public function __construct( - private readonly Security $security, - private readonly RealmResolver $realmResolver, - private readonly Stopwatch $stopwatch - ) { + /** + * @param Security $security + * @param ManagerRegistry $managerRegistry + */ + public function __construct(Security $security, ManagerRegistry $managerRegistry) + { + $this->security = $security; + $this->managerRegistry = $managerRegistry; } /** * @inheritDoc */ - public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool + public function supportsNormalization($data, string $format = null, array $context = []): bool { - if (!($data instanceof NodesSources)) { - return false; - } // Make sure we're not called twice if (isset($context[self::ALREADY_CALLED])) { return false; } - return $this->realmResolver->hasRealmsWithSerializationGroup(); - } - - public function getSupportedTypes(?string $format): array - { - return [ - '*' => false, - ]; + return $data instanceof NodesSources; } /** * @inheritDoc * @return array|string|int|float|bool|\ArrayObject|null */ - public function normalize(mixed $object, ?string $format = null, array $context = []): mixed + public function normalize($object, string $format = null, array $context = []) { - $this->stopwatch->start('realm-serialization-group-normalizer', 'serializer'); $realms = $this->getAuthorizedRealmsForObject($object); foreach ($realms as $realm) { @@ -66,19 +59,18 @@ public function normalize(mixed $object, ?string $format = null, array $context } $context[self::ALREADY_CALLED] = true; - $this->stopwatch->stop('realm-serialization-group-normalizer'); return $this->normalizer->normalize($object, $format, $context); } /** - * @return RealmInterface[] + * @return Realm[] */ private function getAuthorizedRealmsForObject(NodesSources $object): array { - $realms = $this->realmResolver->getRealmsWithSerializationGroup($object->getNode()); + $realms = $this->managerRegistry->getRepository(Realm::class)->findByNode($object->getNode()); - return array_filter($realms, function (RealmInterface $realm) { + return array_filter($realms, function (Realm $realm) { return $this->security->isGranted(RealmVoter::READ, $realm); }); } diff --git a/src/Serializer/Normalizer/TagNormalizer.php b/src/Serializer/Normalizer/TagNormalizer.php index da9a11c7..aa6aa71a 100644 --- a/src/Serializer/Normalizer/TagNormalizer.php +++ b/src/Serializer/Normalizer/TagNormalizer.php @@ -21,28 +21,27 @@ final class TagNormalizer extends AbstractPathNormalizer * @return array|\ArrayObject|bool|float|int|mixed|string|null * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface */ - public function normalize(mixed $object, ?string $format = null, array $context = []): mixed + public function normalize($object, $format = null, array $context = []) { $data = $this->decorated->normalize($object, $format, $context); - if ( - $object instanceof Tag && - is_array($data) && - isset($context['translation']) && - $context['translation'] instanceof TranslationInterface - ) { + if ($object instanceof Tag && is_array($data)) { + $data['slug'] = $object->getTagName(); /** @var array $serializationGroups */ $serializationGroups = isset($context['groups']) && is_array($context['groups']) ? $context['groups'] : []; - $translatedData = $object->getTranslatedTagsByTranslation($context['translation'])->first() ?: null; - if ($translatedData instanceof TagTranslation) { - $data['name'] = $translatedData->getName(); - $data['description'] = $translatedData->getDescription(); - if (\in_array('tag_documents', $serializationGroups, true)) { - $documentsContext = $context; - $documentsContext['groups'] = ['document_display']; - $data['documents'] = array_map(function (DocumentInterface $document) use ($format, $documentsContext) { - return $this->decorated->normalize($document, $format, $documentsContext); - }, $translatedData->getDocuments()); + if (isset($context['translation']) && $context['translation'] instanceof TranslationInterface) { + $documentsContext = $context; + $documentsContext['groups'] = ['document_display']; + $translatedData = $object->getTranslatedTagsByTranslation($context['translation'])->first() ?: null; + if ($translatedData instanceof TagTranslation) { + $data['name'] = $translatedData->getName(); + $data['description'] = $translatedData->getDescription(); + + if (\in_array('tag_documents', $serializationGroups, true)) { + $data['documents'] = array_map(function (DocumentInterface $document) use ($format, $documentsContext) { + return $this->decorated->normalize($document, $format, $documentsContext); + }, $translatedData->getDocuments()); + } } } } diff --git a/src/Serializer/Normalizer/TranslationAwareNormalizer.php b/src/Serializer/Normalizer/TranslationAwareNormalizer.php index eb1c3df2..a0441cc9 100644 --- a/src/Serializer/Normalizer/TranslationAwareNormalizer.php +++ b/src/Serializer/Normalizer/TranslationAwareNormalizer.php @@ -12,21 +12,28 @@ use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; use RZ\Roadiz\CoreBundle\Repository\TranslationRepository; use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; -final class TranslationAwareNormalizer implements NormalizerInterface, NormalizerAwareInterface +final class TranslationAwareNormalizer implements ContextAwareNormalizerInterface, NormalizerAwareInterface { use NormalizerAwareTrait; + private RequestStack $requestStack; + private ManagerRegistry $managerRegistry; + private PreviewResolverInterface $previewResolver; + private const ALREADY_CALLED = 'TRANSLATION_AWARE_NORMALIZER_ALREADY_CALLED'; public function __construct( - private readonly RequestStack $requestStack, - private readonly ManagerRegistry $managerRegistry, - private readonly PreviewResolverInterface $previewResolver + RequestStack $requestStack, + ManagerRegistry $managerRegistry, + PreviewResolverInterface $previewResolver ) { + $this->requestStack = $requestStack; + $this->managerRegistry = $managerRegistry; + $this->previewResolver = $previewResolver; } /** @@ -36,7 +43,7 @@ public function __construct( * @return array|\ArrayObject|bool|float|int|string|null * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface */ - public function normalize(mixed $object, ?string $format = null, array $context = []): mixed + public function normalize($object, $format = null, array $context = []) { if ($object instanceof WebResponseInterface) { $item = $object->getItem(); @@ -75,33 +82,21 @@ private function getTranslationFromLocale(string $locale): ?TranslationInterface private function getTranslationFromRequest(): ?TranslationInterface { $request = $this->requestStack->getMainRequest(); - - if (null === $request) { - return $this->managerRegistry - ->getRepository(Translation::class) - ->findDefault(); - } - - /* - * Try to get translation resolved from LocaleSubscriber before - */ - $requestTranslation = $request->attributes->get('_translation'); - if ($requestTranslation instanceof TranslationInterface) { - return $requestTranslation; - } - - $locale = $request->query->get('_locale', $request->getLocale()); if ( - \is_string($locale) && - null !== $translation = $this->getTranslationFromLocale($locale) + null !== $request && + null !== $translation = $this->getTranslationFromLocale( + $request->query->get('_locale', $request->getLocale()) + ) ) { return $translation; } - return null; + return $this->managerRegistry + ->getRepository(Translation::class) + ->findDefault(); } - public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool + public function supportsNormalization($data, $format = null, array $context = []): bool { // Make sure we're not called twice if (isset($context[self::ALREADY_CALLED])) { @@ -110,11 +105,4 @@ public function supportsNormalization(mixed $data, string $format = null, array return true; } - - public function getSupportedTypes(?string $format): array - { - return [ - '*' => false, - ]; - } } diff --git a/src/Serializer/ObjectConstructor/AbstractTypedObjectConstructor.php b/src/Serializer/ObjectConstructor/AbstractTypedObjectConstructor.php index 476a75dd..9fb1f1ea 100644 --- a/src/Serializer/ObjectConstructor/AbstractTypedObjectConstructor.php +++ b/src/Serializer/ObjectConstructor/AbstractTypedObjectConstructor.php @@ -13,10 +13,17 @@ abstract class AbstractTypedObjectConstructor implements TypedObjectConstructorInterface { - public function __construct( - protected readonly ObjectManager $entityManager, - protected readonly ObjectConstructorInterface $fallbackConstructor - ) { + protected ObjectManager $entityManager; + protected ObjectConstructorInterface $fallbackConstructor; + + /** + * @param ObjectManager $entityManager + * @param ObjectConstructorInterface $fallbackConstructor + */ + public function __construct(ObjectManager $entityManager, ObjectConstructorInterface $fallbackConstructor) + { + $this->entityManager = $entityManager; + $this->fallbackConstructor = $fallbackConstructor; } /** @@ -25,7 +32,7 @@ public function __construct( * * @return object|null */ - abstract protected function findObject(mixed $data, DeserializationContext $context): ?object; + abstract protected function findObject($data, DeserializationContext $context): ?object; /** * @param object $object diff --git a/src/Serializer/ObjectConstructor/AttributeObjectConstructor.php b/src/Serializer/ObjectConstructor/AttributeObjectConstructor.php deleted file mode 100644 index ba437452..00000000 --- a/src/Serializer/ObjectConstructor/AttributeObjectConstructor.php +++ /dev/null @@ -1,55 +0,0 @@ -entityManager - ->getRepository(AttributeInterface::class) - ->findOneByCode($data['code']); - - if ( - null !== $tag && - $context->hasAttribute(self::EXCEPTION_ON_EXISTING) && - true === $context->hasAttribute(self::EXCEPTION_ON_EXISTING) - ) { - throw new EntityAlreadyExistsException('Attribute already exists in database.'); - } - - return $tag; - } - - protected function fillIdentifier(object $object, array $data): void - { - if ($object instanceof AttributeInterface) { - $object->setCode($data['code']); - } - } -} diff --git a/src/Serializer/ObjectConstructor/ChainDoctrineObjectConstructor.php b/src/Serializer/ObjectConstructor/ChainDoctrineObjectConstructor.php index eeff57cc..5e3fc28a 100644 --- a/src/Serializer/ObjectConstructor/ChainDoctrineObjectConstructor.php +++ b/src/Serializer/ObjectConstructor/ChainDoctrineObjectConstructor.php @@ -11,13 +11,28 @@ use JMS\Serializer\Visitor\DeserializationVisitorInterface; use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; -final class ChainDoctrineObjectConstructor implements ObjectConstructorInterface +class ChainDoctrineObjectConstructor implements ObjectConstructorInterface { + protected ?ObjectManager $entityManager; + /** + * @var array + */ + protected array $typedObjectConstructors; + protected ObjectConstructorInterface $fallbackConstructor; + + /** + * @param ObjectManager|null $entityManager + * @param ObjectConstructorInterface $fallbackConstructor + * @param array $typedObjectConstructors + */ public function __construct( - private readonly ?ObjectManager $entityManager, - private readonly ObjectConstructorInterface $fallbackConstructor, - private readonly array $typedObjectConstructors + ?ObjectManager $entityManager, + ObjectConstructorInterface $fallbackConstructor, + array $typedObjectConstructors ) { + $this->entityManager = $entityManager; + $this->typedObjectConstructors = $typedObjectConstructors; + $this->fallbackConstructor = $fallbackConstructor; } /** @@ -42,11 +57,9 @@ public function construct( // Locate possible ClassMetadata $classMetadataFactory = $this->entityManager->getMetadataFactory(); - /** @var class-string $className */ - $className = $metadata->name; try { - $doctrineMetadata = $classMetadataFactory->getMetadataFor($className); - if ($doctrineMetadata->getName() !== $className) { + $doctrineMetadata = $classMetadataFactory->getMetadataFor($metadata->name); + if ($doctrineMetadata->getName() !== $metadata->name) { /* * Doctrine resolveTargetEntity has found an alternative class */ @@ -56,9 +69,7 @@ public function construct( // Object class is not a valid doctrine entity } - /** @var class-string $className */ - $className = $metadata->name; - if ($classMetadataFactory->isTransient($className)) { + if ($classMetadataFactory->isTransient($metadata->name)) { // No ClassMetadata found, proceed with normal deserialization return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context); } @@ -66,12 +77,12 @@ public function construct( // Managed entity, check for proxy load if (!\is_array($data)) { // Single identifier, load proxy - return $this->entityManager->getReference($className, $data); + return $this->entityManager->getReference($metadata->name, $data); } /** @var TypedObjectConstructorInterface $typedObjectConstructor */ foreach ($this->typedObjectConstructors as $typedObjectConstructor) { - if ($typedObjectConstructor->supports($className, $data)) { + if ($typedObjectConstructor->supports($metadata->name, $data)) { return $typedObjectConstructor->construct( $visitor, $metadata, @@ -82,12 +93,17 @@ public function construct( } } + // PHPStan need to explicit classname + /** @var class-string $className */ + $className = $metadata->name; + // Fallback to default constructor if missing identifier(s) $classMetadata = $this->entityManager->getClassMetadata($className); $identifierList = []; foreach ($classMetadata->getIdentifierFieldNames() as $name) { if ( + isset($metadata->propertyMetadata[$name]) && isset($metadata->propertyMetadata[$name]->serializedName) ) { $dataName = $metadata->propertyMetadata[$name]->serializedName; diff --git a/src/Serializer/ObjectConstructor/GroupObjectConstructor.php b/src/Serializer/ObjectConstructor/GroupObjectConstructor.php index 9cd71848..8d0fbf1e 100644 --- a/src/Serializer/ObjectConstructor/GroupObjectConstructor.php +++ b/src/Serializer/ObjectConstructor/GroupObjectConstructor.php @@ -8,7 +8,7 @@ use JMS\Serializer\Exception\ObjectConstructionException; use RZ\Roadiz\CoreBundle\Entity\Group; -final class GroupObjectConstructor extends AbstractTypedObjectConstructor +class GroupObjectConstructor extends AbstractTypedObjectConstructor { /** * @inheritDoc @@ -21,7 +21,7 @@ public function supports(string $className, array $data): bool /** * @inheritDoc */ - protected function findObject(mixed $data, DeserializationContext $context): ?object + protected function findObject($data, DeserializationContext $context): ?object { if (null === $data['name'] || $data['name'] === '') { throw new ObjectConstructionException('Group name can not be empty'); diff --git a/src/Serializer/ObjectConstructor/NodeObjectConstructor.php b/src/Serializer/ObjectConstructor/NodeObjectConstructor.php index ec0cc75c..4a9412dc 100644 --- a/src/Serializer/ObjectConstructor/NodeObjectConstructor.php +++ b/src/Serializer/ObjectConstructor/NodeObjectConstructor.php @@ -9,7 +9,7 @@ use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Repository\NodeRepository; -final class NodeObjectConstructor extends AbstractTypedObjectConstructor +class NodeObjectConstructor extends AbstractTypedObjectConstructor { /** * @inheritDoc @@ -22,7 +22,7 @@ public function supports(string $className, array $data): bool /** * @inheritDoc */ - protected function findObject(mixed $data, DeserializationContext $context): ?object + protected function findObject($data, DeserializationContext $context): ?object { if (empty($data['nodeName']) && empty($data['node_name'])) { throw new ObjectConstructionException('Node name can not be empty'); diff --git a/src/Serializer/ObjectConstructor/NodeTypeFieldObjectConstructor.php b/src/Serializer/ObjectConstructor/NodeTypeFieldObjectConstructor.php new file mode 100644 index 00000000..71fddc76 --- /dev/null +++ b/src/Serializer/ObjectConstructor/NodeTypeFieldObjectConstructor.php @@ -0,0 +1,64 @@ +entityManager + ->getRepository(NodeType::class) + ->findOneByName($data['nodeTypeName'] ?? $data['node_type_name']); + + if (null === $nodeType) { + /* + * Do not look for existing fields if node-type does not exist either. + */ + return null; + } + return $this->entityManager + ->getRepository(NodeTypeField::class) + ->findOneBy([ + 'name' => $data['name'], + 'nodeType' => $nodeType, + ]); + } + + protected function fillIdentifier(object $object, array $data): void + { + trigger_error('Cannot call fillIdentifier on NodeTypeField', E_USER_WARNING); + } + + /** + * @return bool + */ + protected function canBeFlushed(): bool + { + return false; + } +} diff --git a/src/Serializer/ObjectConstructor/NodeTypeObjectConstructor.php b/src/Serializer/ObjectConstructor/NodeTypeObjectConstructor.php index 4d51fccd..71d96815 100644 --- a/src/Serializer/ObjectConstructor/NodeTypeObjectConstructor.php +++ b/src/Serializer/ObjectConstructor/NodeTypeObjectConstructor.php @@ -6,24 +6,22 @@ use JMS\Serializer\DeserializationContext; use JMS\Serializer\Exception\ObjectConstructionException; -use RZ\Roadiz\Contracts\NodeType\NodeTypeInterface; use RZ\Roadiz\CoreBundle\Entity\NodeType; -final class NodeTypeObjectConstructor extends AbstractTypedObjectConstructor +class NodeTypeObjectConstructor extends AbstractTypedObjectConstructor { /** * @inheritDoc */ public function supports(string $className, array $data): bool { - return \is_subclass_of($className, NodeTypeInterface::class) && - array_key_exists('name', $data); + return $className === NodeType::class && array_key_exists('name', $data); } /** * @inheritDoc */ - protected function findObject(mixed $data, DeserializationContext $context): ?object + protected function findObject($data, DeserializationContext $context): ?object { if (null === $data['name'] || $data['name'] === '') { throw new ObjectConstructionException('NodeType name can not be empty'); diff --git a/src/Serializer/ObjectConstructor/RoleObjectConstructor.php b/src/Serializer/ObjectConstructor/RoleObjectConstructor.php index 00698c86..492348be 100644 --- a/src/Serializer/ObjectConstructor/RoleObjectConstructor.php +++ b/src/Serializer/ObjectConstructor/RoleObjectConstructor.php @@ -8,7 +8,7 @@ use JMS\Serializer\Exception\ObjectConstructionException; use RZ\Roadiz\CoreBundle\Entity\Role; -final class RoleObjectConstructor extends AbstractTypedObjectConstructor +class RoleObjectConstructor extends AbstractTypedObjectConstructor { /** * @inheritDoc @@ -21,7 +21,7 @@ public function supports(string $className, array $data): bool /** * @inheritDoc */ - protected function findObject(mixed $data, DeserializationContext $context): ?object + protected function findObject($data, DeserializationContext $context): ?object { if (null === $data['name'] || $data['name'] === '') { throw new ObjectConstructionException('Role name can not be empty'); diff --git a/src/Serializer/ObjectConstructor/SettingGroupObjectConstructor.php b/src/Serializer/ObjectConstructor/SettingGroupObjectConstructor.php index 19834d39..0b6a8adb 100644 --- a/src/Serializer/ObjectConstructor/SettingGroupObjectConstructor.php +++ b/src/Serializer/ObjectConstructor/SettingGroupObjectConstructor.php @@ -8,7 +8,7 @@ use JMS\Serializer\Exception\ObjectConstructionException; use RZ\Roadiz\CoreBundle\Entity\SettingGroup; -final class SettingGroupObjectConstructor extends AbstractTypedObjectConstructor +class SettingGroupObjectConstructor extends AbstractTypedObjectConstructor { /** * @inheritDoc @@ -21,7 +21,7 @@ public function supports(string $className, array $data): bool /** * @inheritDoc */ - protected function findObject(mixed $data, DeserializationContext $context): ?object + protected function findObject($data, DeserializationContext $context): ?object { if (null === $data['name'] || $data['name'] === '') { throw new ObjectConstructionException('SettingGroup name can not be empty'); diff --git a/src/Serializer/ObjectConstructor/SettingObjectConstructor.php b/src/Serializer/ObjectConstructor/SettingObjectConstructor.php index c43a7384..7db87eb5 100644 --- a/src/Serializer/ObjectConstructor/SettingObjectConstructor.php +++ b/src/Serializer/ObjectConstructor/SettingObjectConstructor.php @@ -8,7 +8,7 @@ use JMS\Serializer\Exception\ObjectConstructionException; use RZ\Roadiz\CoreBundle\Entity\Setting; -final class SettingObjectConstructor extends AbstractTypedObjectConstructor +class SettingObjectConstructor extends AbstractTypedObjectConstructor { /** * @inheritDoc @@ -21,7 +21,7 @@ public function supports(string $className, array $data): bool /** * @inheritDoc */ - protected function findObject(mixed $data, DeserializationContext $context): ?object + protected function findObject($data, DeserializationContext $context): ?object { if (null === $data['name'] || $data['name'] === '') { throw new ObjectConstructionException('Setting name can not be empty'); diff --git a/src/Serializer/ObjectConstructor/TagObjectConstructor.php b/src/Serializer/ObjectConstructor/TagObjectConstructor.php index cc984c9a..126ed722 100644 --- a/src/Serializer/ObjectConstructor/TagObjectConstructor.php +++ b/src/Serializer/ObjectConstructor/TagObjectConstructor.php @@ -9,7 +9,7 @@ use RZ\Roadiz\CoreBundle\Entity\Tag; use RZ\Roadiz\CoreBundle\Exception\EntityAlreadyExistsException; -final class TagObjectConstructor extends AbstractTypedObjectConstructor +class TagObjectConstructor extends AbstractTypedObjectConstructor { public const EXCEPTION_ON_EXISTING_TAG = 'exception_on_existing_tag'; @@ -27,7 +27,7 @@ public function supports(string $className, array $data): bool /** * @inheritDoc */ - protected function findObject(mixed $data, DeserializationContext $context): ?object + protected function findObject($data, DeserializationContext $context): ?object { if (empty($data['tagName']) && empty($data['tag_name'])) { throw new ObjectConstructionException('Tag name can not be empty'); diff --git a/src/Serializer/ObjectConstructor/TranslationObjectConstructor.php b/src/Serializer/ObjectConstructor/TranslationObjectConstructor.php index 4bc72788..40309d06 100644 --- a/src/Serializer/ObjectConstructor/TranslationObjectConstructor.php +++ b/src/Serializer/ObjectConstructor/TranslationObjectConstructor.php @@ -6,17 +6,16 @@ use JMS\Serializer\DeserializationContext; use JMS\Serializer\Exception\ObjectConstructionException; -use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; +use RZ\Roadiz\CoreBundle\Entity\Translation; -final class TranslationObjectConstructor extends AbstractTypedObjectConstructor +class TranslationObjectConstructor extends AbstractTypedObjectConstructor { /** * @inheritDoc */ public function supports(string $className, array $data): bool { - return \is_subclass_of($className, TranslationInterface::class) && - array_key_exists('locale', $data); + return $className === Translation::class && array_key_exists('locale', $data); } /** @@ -29,13 +28,13 @@ protected function findObject($data, DeserializationContext $context): ?object } return $this->entityManager - ->getRepository(TranslationInterface::class) + ->getRepository(Translation::class) ->findOneByLocale($data['locale']); } protected function fillIdentifier(object $object, array $data): void { - if ($object instanceof TranslationInterface) { + if ($object instanceof Translation) { $object->setLocale($data['locale']); $object->setName($data['locale']); } diff --git a/src/Serializer/TranslationAwareContextBuilder.php b/src/Serializer/TranslationAwareContextBuilder.php index 66867a64..e12cc612 100644 --- a/src/Serializer/TranslationAwareContextBuilder.php +++ b/src/Serializer/TranslationAwareContextBuilder.php @@ -4,7 +4,7 @@ namespace RZ\Roadiz\CoreBundle\Serializer; -use ApiPlatform\Serializer\SerializerContextBuilderInterface; +use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface; use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; @@ -13,11 +13,18 @@ final class TranslationAwareContextBuilder implements SerializerContextBuilderInterface { + private ManagerRegistry $managerRegistry; + private SerializerContextBuilderInterface $decorated; + private PreviewResolverInterface $previewResolver; + public function __construct( - private readonly SerializerContextBuilderInterface $decorated, - private readonly ManagerRegistry $managerRegistry, - private readonly PreviewResolverInterface $previewResolver + SerializerContextBuilderInterface $decorated, + ManagerRegistry $managerRegistry, + PreviewResolverInterface $previewResolver ) { + $this->decorated = $decorated; + $this->managerRegistry = $managerRegistry; + $this->previewResolver = $previewResolver; } /** * @inheritDoc @@ -26,38 +33,25 @@ public function createFromRequest(Request $request, bool $normalization, array $ { $context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes); - if (isset($context['translation']) && $context['translation'] instanceof TranslationInterface) { - return $context; - } - - /* - * Try to get translation resolved from LocaleSubscriber before - */ - $requestTranslation = $request->attributes->get('_translation'); - if ($requestTranslation instanceof TranslationInterface) { - $context['translation'] = $requestTranslation; - return $context; - } - - /** @var TranslationRepository $repository */ - $repository = $this->managerRegistry - ->getRepository(TranslationInterface::class); - $locale = $request->query->get('_locale', $request->getLocale()); - - if (!\is_string($locale)) { - return $context; + if (!isset($context['translation']) || !($context['translation'] instanceof TranslationInterface)) { + /** @var TranslationRepository $repository */ + $repository = $this->managerRegistry + ->getRepository(TranslationInterface::class); + + if ($this->previewResolver->isPreview()) { + $translation = $repository->findOneByLocaleOrOverrideLocale( + $request->query->get('_locale', $request->getLocale()) + ); + } else { + $translation = $repository->findOneAvailableByLocaleOrOverrideLocale( + $request->query->get('_locale', $request->getLocale()) + ); + } + + if ($translation instanceof TranslationInterface) { + $context['translation'] = $translation; + } } - - if ($this->previewResolver->isPreview()) { - $translation = $repository->findOneByLocaleOrOverrideLocale($locale); - } else { - $translation = $repository->findOneAvailableByLocaleOrOverrideLocale($locale); - } - - if ($translation instanceof TranslationInterface) { - $context['translation'] = $translation; - } - return $context; } } diff --git a/src/Test/NodeType/ApiResourceGeneratorTest.php b/src/Test/NodeType/ApiResourceGeneratorTest.php deleted file mode 100644 index 138a8efc..00000000 --- a/src/Test/NodeType/ApiResourceGeneratorTest.php +++ /dev/null @@ -1,152 +0,0 @@ -getContainer()->get(ApiResourceOperationNameGenerator::class); - - return new ApiResourceGenerator( - $apiResourceOperationNameGenerator, - static::getGeneratedPath(), - new NullLogger(), - WebResponse::class - ); - } - - public function testGenerate(): void - { - $apiResourceGenerator = $this->getApiResourceGenerator(); - - $nodeType = new NodeType(); - $nodeType->setName('Test'); - - $apiResourceGenerator->generate($nodeType); - $resourcePath = $apiResourceGenerator->getResourcePath($nodeType); - $this->assertFileExists($resourcePath); - $this->assertFileEquals( - dirname(__DIR__) . '/../../tests/expected_api_resources/nstest.yml', - $resourcePath - ); - } - - public function testReachableGenerate(): void - { - $apiResourceGenerator = $this->getApiResourceGenerator(); - - $nodeType = new NodeType(); - $nodeType->setName('Test'); - $nodeType->setReachable(true); - - $apiResourceGenerator->generate($nodeType); - $resourcePath = $apiResourceGenerator->getResourcePath($nodeType); - $this->assertFileExists($resourcePath); - $this->assertFileExists(dirname(__DIR__) . '/../../tests/generated_api_resources/web_response.yml'); - $this->assertFileEquals( - dirname(__DIR__) . '/../../tests/expected_api_resources/nstest.yml', - $resourcePath - ); - $this->assertFileEquals( - dirname(__DIR__) . '/../../tests/expected_api_resources/web_response.yml', - dirname(__DIR__) . '/../../tests/generated_api_resources/web_response.yml', - ); - } - - public function testMultipleGenerate(): void - { - $apiResourceGenerator = $this->getApiResourceGenerator(); - - $nodeType = new NodeType(); - $nodeType->setName('Test'); - $nodeType->setReachable(true); - - $nodeType2 = new NodeType(); - $nodeType2->setName('SecondTest'); - $nodeType2->setReachable(true); - - $apiResourceGenerator->generate($nodeType); - $resourcePath = $apiResourceGenerator->getResourcePath($nodeType); - $this->assertFileExists($resourcePath); - $this->assertFileEquals( - dirname(__DIR__) . '/../../tests/expected_api_resources/nstest.yml', - $resourcePath - ); - - $apiResourceGenerator->generate($nodeType2); - $resourcePath2 = $apiResourceGenerator->getResourcePath($nodeType2); - $this->assertFileExists($resourcePath2); - $this->assertFileEquals( - dirname(__DIR__) . '/../../tests/expected_api_resources/nssecondtest.yml', - $resourcePath2 - ); - - $this->assertFileExists(dirname(__DIR__) . '/../../tests/generated_api_resources/web_response.yml'); - $this->assertFileEquals( - dirname(__DIR__) . '/../../tests/expected_api_resources/web_response_multiple.yml', - dirname(__DIR__) . '/../../tests/generated_api_resources/web_response.yml', - ); - } - - public function testRemoveGenerate(): void - { - $apiResourceGenerator = $this->getApiResourceGenerator(); - - $nodeType = new NodeType(); - $nodeType->setName('Test'); - $nodeType->setReachable(true); - - $nodeType2 = new NodeType(); - $nodeType2->setName('SecondTest'); - $nodeType2->setReachable(true); - - $apiResourceGenerator->generate($nodeType); - $apiResourceGenerator->generate($nodeType2); - - // Remove second node-type to check if operation - // is removed from web_response - $apiResourceGenerator->remove($nodeType2); - $resourcePath2 = $apiResourceGenerator->getResourcePath($nodeType2); - $this->assertFileDoesNotExist($resourcePath2); - - $this->assertFileExists(dirname(__DIR__) . '/../../tests/generated_api_resources/web_response.yml'); - $this->assertFileEquals( - dirname(__DIR__) . '/../../tests/expected_api_resources/web_response.yml', - dirname(__DIR__) . '/../../tests/generated_api_resources/web_response.yml', - ); - } - - protected function setUp(): void - { - parent::setUp(); - - $filesystem = new Filesystem(); - $filesystem->mkdir(static::getGeneratedPath()); - } - - - protected function tearDown(): void - { - parent::tearDown(); - - $filesystem = new Filesystem(); - $filesystem->remove(static::getGeneratedPath()); - } -} diff --git a/src/Traits/LoginRequestTrait.php b/src/Traits/LoginRequestTrait.php index 19ea1b16..d804a62d 100644 --- a/src/Traits/LoginRequestTrait.php +++ b/src/Traits/LoginRequestTrait.php @@ -23,11 +23,11 @@ trait LoginRequestTrait abstract protected function getUserViewer(): UserViewer; /** - * @param FormInterface $form - * @param ObjectManager $entityManager - * @param LoggerInterface $logger + * @param FormInterface $form + * @param ObjectManager $entityManager + * @param LoggerInterface $logger * @param UrlGeneratorInterface $urlGenerator - * @param string $resetRoute + * @param string $resetRoute * * @return bool TRUE if confirmation has been sent. FALSE if errors * @throws \Doctrine\ORM\ORMException @@ -39,7 +39,7 @@ public function sendConfirmationEmail( LoggerInterface $logger, UrlGeneratorInterface $urlGenerator, string $resetRoute = 'loginResetPage' - ): bool { + ) { $email = $form->get('email')->getData(); /** @var User $user */ $user = $entityManager->getRepository(User::class)->findOneByEmail($email); @@ -52,9 +52,10 @@ public function sendConfirmationEmail( $user->setConfirmationToken($tokenGenerator->generateToken()); $entityManager->flush(); $userViewer = $this->getUserViewer(); - $userViewer->sendPasswordResetLink($user, $resetRoute); + $userViewer->setUser($user); + $userViewer->sendPasswordResetLink($resetRoute); return true; - } catch (\Throwable $e) { + } catch (\Exception $e) { $user->setPasswordRequestedAt(null); $user->setConfirmationToken(null); $entityManager->flush(); diff --git a/src/TwigExtension/AttributesExtension.php b/src/TwigExtension/AttributesExtension.php index 92755704..890cfa49 100644 --- a/src/TwigExtension/AttributesExtension.php +++ b/src/TwigExtension/AttributesExtension.php @@ -19,10 +19,16 @@ use Twig\TwigFunction; use Twig\TwigTest; -final class AttributesExtension extends AbstractExtension +class AttributesExtension extends AbstractExtension { - public function __construct(private readonly EntityManagerInterface $entityManager) + protected EntityManagerInterface $entityManager; + + /** + * @param EntityManagerInterface $entityManager + */ + public function __construct(EntityManagerInterface $entityManager) { + $this->entityManager = $entityManager; } public function getFunctions(): array @@ -207,7 +213,7 @@ public function getNodeSourceGroupedAttributeValues(?NodesSources $nodesSources, * * @return string|null */ - public function getAttributeLabelOrCode(mixed $mixed, TranslationInterface $translation = null): ?string + public function getAttributeLabelOrCode($mixed, TranslationInterface $translation = null): ?string { if (null === $mixed) { return null; @@ -234,7 +240,7 @@ public function getAttributeLabelOrCode(mixed $mixed, TranslationInterface $tran * @param TranslationInterface|null $translation * @return string|null */ - public function getAttributeGroupLabelOrCode(mixed $mixed, TranslationInterface $translation = null): ?string + public function getAttributeGroupLabelOrCode($mixed, TranslationInterface $translation = null): ?string { if (null === $mixed) { return null; diff --git a/src/TwigExtension/BlockRenderExtension.php b/src/TwigExtension/BlockRenderExtension.php index 5e028743..e4944307 100644 --- a/src/TwigExtension/BlockRenderExtension.php +++ b/src/TwigExtension/BlockRenderExtension.php @@ -15,10 +15,16 @@ * Extension that allow render inner page part calling directly their * controller response instead of doing a simple include. */ -final class BlockRenderExtension extends AbstractExtension +class BlockRenderExtension extends AbstractExtension { - public function __construct(private readonly FragmentHandler $handler) + protected FragmentHandler $handler; + + /** + * @param FragmentHandler $handler + */ + public function __construct(FragmentHandler $handler) { + $this->handler = $handler; } public function getFilters(): array @@ -36,7 +42,7 @@ public function getFilters(): array * @return string * @throws RuntimeError */ - public function blockRender(NodesSources $nodeSource = null, string $themeName = "DefaultTheme", array $assignation = []): string + public function blockRender(NodesSources $nodeSource = null, string $themeName = "DefaultTheme", array $assignation = []) { if (null !== $nodeSource) { if (!empty($themeName)) { diff --git a/src/TwigExtension/CentralTruncateExtension.php b/src/TwigExtension/CentralTruncateExtension.php index 077e73e9..8c3c8851 100644 --- a/src/TwigExtension/CentralTruncateExtension.php +++ b/src/TwigExtension/CentralTruncateExtension.php @@ -9,7 +9,7 @@ use function Symfony\Component\String\u; -final class CentralTruncateExtension extends AbstractExtension +class CentralTruncateExtension extends AbstractExtension { public function getFilters(): array { diff --git a/src/TwigExtension/DocumentUrlExtension.php b/src/TwigExtension/DocumentUrlExtension.php index af68dd7d..945ffda6 100644 --- a/src/TwigExtension/DocumentUrlExtension.php +++ b/src/TwigExtension/DocumentUrlExtension.php @@ -15,12 +15,21 @@ /** * Extension that allow render documents Url */ -final class DocumentUrlExtension extends AbstractExtension +class DocumentUrlExtension extends AbstractExtension { + protected DocumentUrlGeneratorInterface $documentUrlGenerator; + protected bool $throwExceptions; + + /** + * @param DocumentUrlGeneratorInterface $documentUrlGenerator + * @param bool $throwExceptions Trigger exception if using filter on NULL values (default: false) + */ public function __construct( - private readonly DocumentUrlGeneratorInterface $documentUrlGenerator, - private readonly bool $throwExceptions = false + DocumentUrlGeneratorInterface $documentUrlGenerator, + bool $throwExceptions = false ) { + $this->throwExceptions = $throwExceptions; + $this->documentUrlGenerator = $documentUrlGenerator; } /** @@ -45,7 +54,7 @@ public function getFilters(): array * @return string * @throws RuntimeError */ - public function getUrl(PersistableInterface $mixed = null, array $criteria = []): string + public function getUrl(PersistableInterface $mixed = null, array $criteria = []) { if (null === $mixed) { if ($this->throwExceptions) { diff --git a/src/TwigExtension/HandlerExtension.php b/src/TwigExtension/HandlerExtension.php index 46bd6cd0..186355d6 100644 --- a/src/TwigExtension/HandlerExtension.php +++ b/src/TwigExtension/HandlerExtension.php @@ -4,8 +4,6 @@ namespace RZ\Roadiz\CoreBundle\TwigExtension; -use Psr\Container\ContainerExceptionInterface; -use Psr\Container\NotFoundExceptionInterface; use RZ\Roadiz\Core\AbstractEntities\AbstractEntity; use RZ\Roadiz\Core\Handlers\AbstractHandler; use RZ\Roadiz\CoreBundle\EntityHandler\HandlerFactory; @@ -15,8 +13,14 @@ final class HandlerExtension extends AbstractExtension { - public function __construct(private readonly HandlerFactory $handlerFactory) + private HandlerFactory $handlerFactory; + + /** + * @param HandlerFactory $handlerFactory + */ + public function __construct(HandlerFactory $handlerFactory) { + $this->handlerFactory = $handlerFactory; } public function getFilters(): array @@ -30,10 +34,8 @@ public function getFilters(): array * @param mixed $mixed * @return AbstractHandler|null * @throws RuntimeError - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface */ - public function getHandler(mixed $mixed): ?AbstractHandler + public function getHandler($mixed) { if (null === $mixed) { return null; diff --git a/src/TwigExtension/JwtExtension.php b/src/TwigExtension/JwtExtension.php index 246e20d8..c8d2452b 100644 --- a/src/TwigExtension/JwtExtension.php +++ b/src/TwigExtension/JwtExtension.php @@ -9,16 +9,24 @@ use Psr\Log\LoggerInterface; use RZ\Roadiz\CoreBundle\Preview\User\PreviewUserProviderInterface; use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Component\Security\Core\User\UserInterface; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; final class JwtExtension extends AbstractExtension { + private PreviewUserProviderInterface $previewUserProvider; + private JWTTokenManagerInterface $tokenManager; + private LoggerInterface $logger; + public function __construct( - private readonly JWTTokenManagerInterface $tokenManager, - private readonly LoggerInterface $logger, - private readonly PreviewUserProviderInterface $previewUserProvider + JWTTokenManagerInterface $tokenManager, + LoggerInterface $logger, + PreviewUserProviderInterface $previewUserProvider ) { + $this->tokenManager = $tokenManager; + $this->logger = $logger; + $this->previewUserProvider = $previewUserProvider; } public function getFunctions(): array diff --git a/src/TwigExtension/LogExtension.php b/src/TwigExtension/LogExtension.php deleted file mode 100644 index cfaa612e..00000000 --- a/src/TwigExtension/LogExtension.php +++ /dev/null @@ -1,114 +0,0 @@ - [$this, 'isUrlGenerationSafe']]), - ]; - } - - public function getEditPath(?object $log): ?string - { - if (!($log instanceof Log) || null === $log->getEntityId()) { - return null; - } - - switch ($log->getEntityClass()) { - case Node::class: - case NodesSources::class: - if ( - $this->security->isGranted('ROLE_ACCESS_NODES') && - isset($log->getAdditionalData()['node_id']) && - isset($log->getAdditionalData()['translation_id']) - ) { - return $this->urlGenerator->generate('nodesEditSourcePage', [ - 'nodeId' => $log->getAdditionalData()['node_id'], - 'translationId' => $log->getAdditionalData()['translation_id'], - ]); - } - break; - case Tag::class: - if ( - $this->security->isGranted('ROLE_ACCESS_TAGS') - ) { - return $this->urlGenerator->generate('tagsEditPage', [ - 'tagId' => $log->getEntityId(), - ]); - } - break; - case Document::class: - if ( - $this->security->isGranted('ROLE_ACCESS_DOCUMENTS') - ) { - return $this->urlGenerator->generate('documentsEditPage', [ - 'documentId' => $log->getEntityId(), - ]); - } - break; - case User::class: - if ( - $this->security->isGranted('ROLE_ACCESS_USERS') - ) { - return $this->urlGenerator->generate('usersEditPage', [ - 'id' => $log->getEntityId(), - ]); - } - break; - case CustomForm::class: - if ( - $this->security->isGranted('ROLE_ACCESS_CUSTOMFORMS') - ) { - return $this->urlGenerator->generate('customFormsEditPage', [ - 'id' => $log->getEntityId(), - ]); - } - break; - case Translation::class: - if ( - $this->security->isGranted('ROLE_ACCESS_TRANSLATIONS') - ) { - return $this->urlGenerator->generate('translationsEditPage', [ - 'translationId' => $log->getEntityId(), - ]); - } - break; - case Setting::class: - if ( - $this->security->isGranted('ROLE_ACCESS_SETTINGS') - ) { - return $this->urlGenerator->generate('settingsEditPage', [ - 'settingId' => $log->getEntityId(), - ]); - } - break; - } - - return null; - } -} diff --git a/src/TwigExtension/NodesSourcesExtension.php b/src/TwigExtension/NodesSourcesExtension.php index 55d8fcc8..59a2b90e 100644 --- a/src/TwigExtension/NodesSourcesExtension.php +++ b/src/TwigExtension/NodesSourcesExtension.php @@ -4,13 +4,11 @@ namespace RZ\Roadiz\CoreBundle\TwigExtension; -use Doctrine\ORM\NonUniqueResultException; -use Doctrine\Persistence\ManagerRegistry; -use Psr\Container\ContainerExceptionInterface; -use Psr\Container\NotFoundExceptionInterface; use RZ\Roadiz\CoreBundle\Bag\NodeTypes; use RZ\Roadiz\CoreBundle\Entity\NodesSources; -use RZ\Roadiz\CoreBundle\Entity\Tag; +use RZ\Roadiz\CoreBundle\EntityApi\NodeSourceApi; +use RZ\Roadiz\CoreBundle\EntityHandler\HandlerFactory; +use RZ\Roadiz\CoreBundle\EntityHandler\NodesSourcesHandler; use Twig\Error\RuntimeError; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; @@ -21,11 +19,27 @@ */ final class NodesSourcesExtension extends AbstractExtension { + protected NodeSourceApi $nodeSourceApi; + protected HandlerFactory $handlerFactory; + private bool $throwExceptions; + private NodeTypes $nodeTypesBag; + + /** + * @param NodeSourceApi $nodeSourceApi + * @param HandlerFactory $handlerFactory + * @param NodeTypes $nodeTypesBag + * @param bool $throwExceptions + */ public function __construct( - private readonly ManagerRegistry $managerRegistry, - private readonly NodeTypes $nodeTypesBag, - private readonly bool $throwExceptions = false + NodeSourceApi $nodeSourceApi, + HandlerFactory $handlerFactory, + NodeTypes $nodeTypesBag, + bool $throwExceptions = false ) { + $this->throwExceptions = $throwExceptions; + $this->handlerFactory = $handlerFactory; + $this->nodeTypesBag = $nodeTypesBag; + $this->nodeSourceApi = $nodeSourceApi; } public function getFilters(): array @@ -62,10 +76,10 @@ public function getTests(): array * @param NodesSources|null $ns * @param array|null $criteria * @param array|null $order - * @return iterable + * @return array * @throws RuntimeError */ - public function getChildren(NodesSources $ns = null, array $criteria = null, array $order = null): iterable + public function getChildren(NodesSources $ns = null, array $criteria = null, array $order = null) { if (null === $ns) { if ($this->throwExceptions) { @@ -74,10 +88,24 @@ public function getChildren(NodesSources $ns = null, array $criteria = null, arr return []; } } + $defaultCrit = [ + 'node.parent' => $ns->getNode(), + 'translation' => $ns->getTranslation(), + ]; + + if (null !== $order) { + $defaultOrder = $order; + } else { + $defaultOrder = [ + 'node.position' => 'ASC', + ]; + } + + if (null !== $criteria) { + $defaultCrit = array_merge($defaultCrit, $criteria); + } - return $this->managerRegistry - ->getRepository(NodesSources::class) - ->findChildren($ns, $criteria, $order); + return $this->nodeSourceApi->getBy($defaultCrit, $defaultOrder); } /** @@ -87,7 +115,7 @@ public function getChildren(NodesSources $ns = null, array $criteria = null, arr * @return NodesSources|null * @throws RuntimeError */ - public function getNext(NodesSources $ns = null, array $criteria = null, array $order = null): ?NodesSources + public function getNext(NodesSources $ns = null, array $criteria = null, array $order = null) { if (null === $ns) { if ($this->throwExceptions) { @@ -96,10 +124,9 @@ public function getNext(NodesSources $ns = null, array $criteria = null, array $ return null; } } - - return $this->managerRegistry - ->getRepository(NodesSources::class) - ->findNext($ns, $criteria, $order); + /** @var NodesSourcesHandler $nodeSourceHandler */ + $nodeSourceHandler = $this->handlerFactory->getHandler($ns); + return $nodeSourceHandler->getNext($criteria, $order); } /** @@ -109,7 +136,7 @@ public function getNext(NodesSources $ns = null, array $criteria = null, array $ * @return NodesSources|null * @throws RuntimeError */ - public function getPrevious(NodesSources $ns = null, array $criteria = null, array $order = null): ?NodesSources + public function getPrevious(NodesSources $ns = null, array $criteria = null, array $order = null) { if (null === $ns) { if ($this->throwExceptions) { @@ -118,10 +145,9 @@ public function getPrevious(NodesSources $ns = null, array $criteria = null, arr return null; } } - - return $this->managerRegistry - ->getRepository(NodesSources::class) - ->findPrevious($ns, $criteria, $order); + /** @var NodesSourcesHandler $nodeSourceHandler */ + $nodeSourceHandler = $this->handlerFactory->getHandler($ns); + return $nodeSourceHandler->getPrevious($criteria, $order); } /** @@ -131,7 +157,7 @@ public function getPrevious(NodesSources $ns = null, array $criteria = null, arr * @return NodesSources|null * @throws RuntimeError */ - public function getLastSibling(NodesSources $ns = null, array $criteria = null, array $order = null): ?NodesSources + public function getLastSibling(NodesSources $ns = null, array $criteria = null, array $order = null) { if (null === $ns) { if ($this->throwExceptions) { @@ -140,10 +166,9 @@ public function getLastSibling(NodesSources $ns = null, array $criteria = null, return null; } } - - return $this->managerRegistry - ->getRepository(NodesSources::class) - ->findLastSibling($ns, $criteria, $order); + /** @var NodesSourcesHandler $nodeSourceHandler */ + $nodeSourceHandler = $this->handlerFactory->getHandler($ns); + return $nodeSourceHandler->getLastSibling($criteria, $order); } /** @@ -153,7 +178,7 @@ public function getLastSibling(NodesSources $ns = null, array $criteria = null, * @return NodesSources|null * @throws RuntimeError */ - public function getFirstSibling(NodesSources $ns = null, array $criteria = null, array $order = null): ?NodesSources + public function getFirstSibling(NodesSources $ns = null, array $criteria = null, array $order = null) { if (null === $ns) { if ($this->throwExceptions) { @@ -162,10 +187,9 @@ public function getFirstSibling(NodesSources $ns = null, array $criteria = null, return null; } } - - return $this->managerRegistry - ->getRepository(NodesSources::class) - ->findFirstSibling($ns, $criteria, $order); + /** @var NodesSourcesHandler $nodeSourceHandler */ + $nodeSourceHandler = $this->handlerFactory->getHandler($ns); + return $nodeSourceHandler->getFirstSibling($criteria, $order); } /** @@ -173,7 +197,7 @@ public function getFirstSibling(NodesSources $ns = null, array $criteria = null, * @return NodesSources|null * @throws RuntimeError */ - public function getParent(NodesSources $ns = null): ?NodesSources + public function getParent(NodesSources $ns = null) { if (null === $ns) { if ($this->throwExceptions) { @@ -190,12 +214,9 @@ public function getParent(NodesSources $ns = null): ?NodesSources * @param NodesSources|null $ns * @param array|null $criteria * @return array - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface * @throws RuntimeError - * @throws NonUniqueResultException */ - public function getParents(NodesSources $ns = null, array $criteria = null): array + public function getParents(NodesSources $ns = null, array $criteria = null) { if (null === $ns) { if ($this->throwExceptions) { @@ -204,20 +225,17 @@ public function getParents(NodesSources $ns = null, array $criteria = null): arr return []; } } - - return $this->managerRegistry - ->getRepository(NodesSources::class) - ->findParents($ns, $criteria); + /** @var NodesSourcesHandler $nodeSourceHandler */ + $nodeSourceHandler = $this->handlerFactory->getHandler($ns); + return $nodeSourceHandler->getParents($criteria); } /** * @param NodesSources|null $ns - * @return iterable + * @return array * @throws RuntimeError - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface */ - public function getTags(NodesSources $ns = null): iterable + public function getTags(NodesSources $ns = null) { if (null === $ns) { if ($this->throwExceptions) { @@ -226,9 +244,8 @@ public function getTags(NodesSources $ns = null): iterable return []; } } - - return $this->managerRegistry - ->getRepository(Tag::class) - ->findByNodesSources($ns); + /** @var NodesSourcesHandler $nodeSourceHandler */ + $nodeSourceHandler = $this->handlerFactory->getHandler($ns); + return $nodeSourceHandler->getTags(); } } diff --git a/src/TwigExtension/RoadizExtension.php b/src/TwigExtension/RoadizExtension.php index c5d4d333..62b7fd10 100644 --- a/src/TwigExtension/RoadizExtension.php +++ b/src/TwigExtension/RoadizExtension.php @@ -11,18 +11,35 @@ use Twig\Extension\AbstractExtension; use Twig\Extension\GlobalsInterface; -final class RoadizExtension extends AbstractExtension implements GlobalsInterface +class RoadizExtension extends AbstractExtension implements GlobalsInterface { + protected Settings $settingsBag; + protected NodeTypes $nodeTypesBag; + protected PreviewResolverInterface $previewResolver; + protected NodeChrootResolver $chrootResolver; + protected string $cmsVersion; + protected string $cmsVersionPrefix; + protected bool $hideRoadizVersion; + protected int $maxVersionsShowed; + public function __construct( - private readonly Settings $settingsBag, - private readonly NodeTypes $nodeTypesBag, - private readonly PreviewResolverInterface $previewResolver, - private readonly NodeChrootResolver $chrootResolver, - private readonly string $cmsVersion, - private readonly string $cmsVersionPrefix, - private readonly bool $hideRoadizVersion, - private readonly int $maxVersionsShowed + Settings $settingsBag, + NodeTypes $nodeTypesBag, + PreviewResolverInterface $previewResolver, + NodeChrootResolver $chrootResolver, + string $cmsVersion, + string $cmsVersionPrefix, + bool $hideRoadizVersion, + int $maxVersionsShowed ) { + $this->settingsBag = $settingsBag; + $this->nodeTypesBag = $nodeTypesBag; + $this->previewResolver = $previewResolver; + $this->chrootResolver = $chrootResolver; + $this->cmsVersion = $cmsVersion; + $this->cmsVersionPrefix = $cmsVersionPrefix; + $this->hideRoadizVersion = $hideRoadizVersion; + $this->maxVersionsShowed = $maxVersionsShowed; } /** diff --git a/src/TwigExtension/RoutingExtension.php b/src/TwigExtension/RoutingExtension.php index f78d3dd3..bfdd578d 100644 --- a/src/TwigExtension/RoutingExtension.php +++ b/src/TwigExtension/RoutingExtension.php @@ -14,12 +14,21 @@ /** * Override Symfony RoutingExtension to support object url generation. */ -final class RoutingExtension extends AbstractExtension +class RoutingExtension extends AbstractExtension { + private UrlGeneratorInterface $generator; + private \Symfony\Bridge\Twig\Extension\RoutingExtension $decorated; + + /** + * @param UrlGeneratorInterface $generator + * @param \Symfony\Bridge\Twig\Extension\RoutingExtension $decorated + */ public function __construct( - private readonly \Symfony\Bridge\Twig\Extension\RoutingExtension $decorated, - private readonly UrlGeneratorInterface $generator + \Symfony\Bridge\Twig\Extension\RoutingExtension $decorated, + UrlGeneratorInterface $generator ) { + $this->generator = $generator; + $this->decorated = $decorated; } /** @@ -40,7 +49,7 @@ public function getFunctions(): array * @return string * @throws RuntimeError */ - public function getPath(string|object|null $name, array $parameters = [], bool $relative = false): string + public function getPath($name, array $parameters = [], bool $relative = false): string { if (is_string($name)) { return $this->decorated->getPath( @@ -66,7 +75,7 @@ public function getPath(string|object|null $name, array $parameters = [], bool $ * @return string * @throws RuntimeError */ - public function getUrl(string|object|null $name, array $parameters = [], bool $schemeRelative = false): string + public function getUrl($name, array $parameters = [], bool $schemeRelative = false): string { if (is_string($name)) { return $this->decorated->getUrl( diff --git a/src/TwigExtension/TransChoiceExtension.php b/src/TwigExtension/TransChoiceExtension.php index 96c19b15..949b0243 100644 --- a/src/TwigExtension/TransChoiceExtension.php +++ b/src/TwigExtension/TransChoiceExtension.php @@ -15,8 +15,14 @@ */ final class TransChoiceExtension extends AbstractExtension { - public function __construct(private readonly TranslatorInterface $translator) + private TranslatorInterface $translator; + + /** + * @param TranslatorInterface $translator + */ + public function __construct(TranslatorInterface $translator) { + $this->translator = $translator; } public function getFilters(): array diff --git a/src/TwigExtension/TranslationExtension.php b/src/TwigExtension/TranslationExtension.php index cd248352..807a2fc7 100644 --- a/src/TwigExtension/TranslationExtension.php +++ b/src/TwigExtension/TranslationExtension.php @@ -34,7 +34,7 @@ public function getTests(): array * * @return bool */ - public function isLocaleRtl(mixed $mixed): bool + public function isLocaleRtl($mixed) { if ($mixed instanceof TranslationInterface) { return $mixed->isRtl(); diff --git a/src/TwigExtension/TranslationMenuExtension.php b/src/TwigExtension/TranslationMenuExtension.php index 2c0bccd8..b1fdb2fd 100644 --- a/src/TwigExtension/TranslationMenuExtension.php +++ b/src/TwigExtension/TranslationMenuExtension.php @@ -13,10 +13,17 @@ final class TranslationMenuExtension extends AbstractExtension { - public function __construct( - private readonly RequestStack $requestStack, - private readonly TranslationViewer $translationViewer - ) { + private RequestStack $requestStack; + private TranslationViewer $translationViewer; + + /** + * @param RequestStack $requestStack + * @param TranslationViewer $translationViewer + */ + public function __construct(RequestStack $requestStack, TranslationViewer $translationViewer) + { + $this->requestStack = $requestStack; + $this->translationViewer = $translationViewer; } public function getFilters(): array @@ -33,7 +40,7 @@ public function getFilters(): array * @return array * @throws ORMException */ - public function getMenuAssignation(TranslationInterface $translation = null, bool $absolute = false): array + public function getMenuAssignation(TranslationInterface $translation = null, bool $absolute = false) { if (null !== $translation) { $this->translationViewer->setTranslation($translation); diff --git a/src/Workflow/Event/NodeStatusGuardListener.php b/src/Workflow/Event/NodeStatusGuardListener.php index edc6b66e..b9550d93 100644 --- a/src/Workflow/Event/NodeStatusGuardListener.php +++ b/src/Workflow/Event/NodeStatusGuardListener.php @@ -5,9 +5,8 @@ namespace RZ\Roadiz\CoreBundle\Workflow\Event; use RZ\Roadiz\CoreBundle\Entity\Node; -use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Security; use Symfony\Component\Workflow\Event\GuardEvent; use Symfony\Component\Workflow\TransitionBlocker; @@ -38,7 +37,7 @@ public static function getSubscribedEvents(): array public function guard(GuardEvent $event): void { - if (!$this->security->isGranted(NodeVoter::EDIT_CONTENT, $event->getSubject())) { + if (!$this->security->isGranted('ROLE_ACCESS_NODES')) { $event->addTransitionBlocker(new TransitionBlocker( 'User is not allowed to edit this node.', '1' @@ -48,7 +47,7 @@ public function guard(GuardEvent $event): void public function guardPublish(GuardEvent $event): void { - if (!$this->security->isGranted(NodeVoter::EDIT_STATUS, $event->getSubject())) { + if (!$this->security->isGranted('ROLE_ACCESS_NODES_STATUS')) { $event->addTransitionBlocker(new TransitionBlocker( 'User is not allowed to publish this node.', '1' @@ -66,7 +65,7 @@ public function guardArchive(GuardEvent $event): void '1' )); } - if (!$this->security->isGranted(NodeVoter::EDIT_STATUS, $event->getSubject())) { + if (!$this->security->isGranted('ROLE_ACCESS_NODES_STATUS')) { $event->addTransitionBlocker(new TransitionBlocker( 'User is not allowed to archive this node.', '1' @@ -84,7 +83,7 @@ public function guardDelete(GuardEvent $event): void '1' )); } - if (!$this->security->isGranted(NodeVoter::DELETE, $event->getSubject())) { + if (!$this->security->isGranted('ROLE_ACCESS_NODES_DELETE')) { $event->addTransitionBlocker(new TransitionBlocker( 'User is not allowed to delete this node.', '1' diff --git a/src/Xlsx/NodeSourceXlsxSerializer.php b/src/Xlsx/NodeSourceXlsxSerializer.php index 56e19597..b2579415 100644 --- a/src/Xlsx/NodeSourceXlsxSerializer.php +++ b/src/Xlsx/NodeSourceXlsxSerializer.php @@ -68,6 +68,7 @@ public function toArray($nodeSource): array $data['title'] = $nodeSource->getTitle(); $data['published_at'] = $nodeSource->getPublishedAt(); $data['meta_title'] = $nodeSource->getMetaTitle(); + $data['meta_keywords'] = $nodeSource->getMetaKeywords(); $data['meta_description'] = $nodeSource->getMetaDescription(); $data = array_merge($data, $this->getSourceFields($nodeSource)); diff --git a/src/Xlsx/XlsxExporter.php b/src/Xlsx/XlsxExporter.php index 633d7017..20988aa7 100644 --- a/src/Xlsx/XlsxExporter.php +++ b/src/Xlsx/XlsxExporter.php @@ -85,10 +85,7 @@ public function exportXlsx($data, $keys = []) foreach ($headerkeys as $key => $value) { $columnAlpha = Coordinate::stringFromColumnIndex($key + 1); $activeSheet->getStyle($columnAlpha . $activeRow)->applyFromArray($headerStyles); - if (\is_string($value)) { - $value = $this->translator->trans($value); - } - $activeSheet->setCellValueByColumnAndRow($key + 1, $activeRow, $value); + $activeSheet->setCellValueByColumnAndRow($key + 1, $activeRow, $this->translator->trans($value)); } $activeRow++; } @@ -107,7 +104,7 @@ public function exportXlsx($data, $keys = []) continue; } - if ($value instanceof \DateTimeInterface) { + if ($value instanceof \DateTime) { $value = Date::PHPToExcel($value); $activeSheet->getStyle($columnAlpha . ($activeRow)) ->getNumberFormat() @@ -137,11 +134,6 @@ public function exportXlsx($data, $keys = []) $writer = new Xlsx($spreadsheet); ob_start(); $writer->save('php://output'); - $output = ob_get_clean(); - - if (!\is_string($output)) { - throw new \RuntimeException('Output is not a string.'); - } - return $output; + return ob_get_clean(); } } diff --git a/templates/DataCollector/solarium.html.twig b/templates/DataCollector/solarium.html.twig deleted file mode 100644 index 5a26a650..00000000 --- a/templates/DataCollector/solarium.html.twig +++ /dev/null @@ -1,81 +0,0 @@ -{# @see https://github.com/nelmio/NelmioSolariumBundle #} -{% extends '@WebProfiler/Profiler/layout.html.twig' %} - -{% block toolbar %} - {% set icon %} - {{ include('@RoadizCore/DataCollector/solr.svg') }} - {{ collector.querycount }} - - in - {{ '%0.2f'|format(collector.totaltime * 1000) }} - ms - - {% endset %} - {% set text %} -
- Solr Queries - {{ collector.querycount }} -
-
- Query time - {{ '%0.2f'|format(collector.totaltime * 1000) }} ms -
- {% endset %} - {% include '@WebProfiler/Profiler/toolbar_item.html.twig' with { 'link': true } %} -{% endblock %} - -{% block menu %} - - {{ include('@RoadizCore/DataCollector/solr.svg') }} - Solr - -{% endblock %} - -{% block panel %} - {% if collector.queries is empty %} -

- No queries. -

- {% else %} -
    - {% for i, query in collector.queries %} -
  • -

    Request {{ loop.index }} ({{ query.request.uri }})

    -
    -

    Params

    - - - - - - - - - {% for key, value in query.request.params %} - - - {% if value is iterable %} - - {% else %} - - {% endif %} - - {% endfor %} - -
    KeyValue
    {{ key }}{{ value|join('
    ')|raw }}
    {{ value }}
    - -

    Response

    - - {% if query.response %} - HTTP-Result: {{ query.response.statuscode }} ({{ '%0.2f'|format(query.duration * 1000) }} ms)
    - {{ query.response.body }} - {% else %} - Request failed, no response logged - {% endif %} -
    -
    -
  • - {% endfor %} -
- {% endif %} -{% endblock %} diff --git a/templates/DataCollector/solr.svg b/templates/DataCollector/solr.svg deleted file mode 100644 index 2f16b911..00000000 --- a/templates/DataCollector/solr.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/templates/customForm/customForm.html.twig b/templates/customForm/customForm.html.twig index 86d9b857..445238db 100644 --- a/templates/customForm/customForm.html.twig +++ b/templates/customForm/customForm.html.twig @@ -16,7 +16,7 @@ {{ form_widget(form) }}
{% apply spaceless %} - diff --git a/templates/email/forms/answerForm.html.twig b/templates/email/forms/answerForm.html.twig index a6cf6428..2fc13c63 100644 --- a/templates/email/forms/answerForm.html.twig +++ b/templates/email/forms/answerForm.html.twig @@ -3,9 +3,6 @@ {% block title %}{{ title }}{% endblock %} {% block content_table %} -{% if requestLocale is not defined %} - {% set requestLocale = app.request.locale %} -{% endif %}
@@ -25,10 +22,10 @@ {% for field in fields %} - +
{{ field.label|default(field.name|trans(locale=requestLocale)) }}{{ field.label|default(field.name|trans) }} {% if field.name == "submittedAt" %} - {{ field.value|format_datetime("medium", "short", locale=requestLocale) }} + {{ field.value|format_datetime("medium", "short", locale=app.request.locale) }} {% else %} {{ field.value }} {% endif %} diff --git a/templates/email/forms/answerForm.txt.twig b/templates/email/forms/answerForm.txt.twig index 3d9eba19..762450bc 100644 --- a/templates/email/forms/answerForm.txt.twig +++ b/templates/email/forms/answerForm.txt.twig @@ -3,15 +3,11 @@ {% block title %}{{ title }}{% endblock %} {% block content_table %} -{% if requestLocale is not defined %} - {% set requestLocale = app.request.locale %} -{% endif %} {% trans %}answer.form{% endtrans %} --- {% for field in fields %} - -{{ field.label|default(field.name|trans(locale=requestLocale)) }}: {% if field.name == "submittedAt" %}{{ field.value|format_datetime("medium", "short", locale=requestLocale) }}{% else %}{{ field.value }}{% endif %} +{{ field.label|default(field.name|trans) }}: {% if field.name == "submittedAt" %}{{ field.value|format_datetime("medium", "short", locale=app.request.locale) }}{% else %}{{ field.value }}{% endif %} {% endfor %} {% endblock %} diff --git a/tests/bootstrap.php b/tests/bootstrap.php deleted file mode 100644 index 469dccee..00000000 --- a/tests/bootstrap.php +++ /dev/null @@ -1,11 +0,0 @@ -bootEnv(dirname(__DIR__).'/.env'); -} diff --git a/tests/expected_api_resources/nssecondtest.yml b/tests/expected_api_resources/nssecondtest.yml deleted file mode 100644 index 50af807a..00000000 --- a/tests/expected_api_resources/nssecondtest.yml +++ /dev/null @@ -1,35 +0,0 @@ -resources: - App\GeneratedEntity\NSSecondTest: - shortName: SecondTest - types: - - SecondTest - operations: - secondtest_get_collection: - method: GET - class: ApiPlatform\Metadata\GetCollection - shortName: SecondTest - normalizationContext: - enable_max_depth: true - groups: - - nodes_sources_base - - nodes_sources_default - - urls - - tag_base - - translation_base - - document_display - - document_thumbnails - - document_display_sources - secondtest_get: - method: GET - class: ApiPlatform\Metadata\Get - shortName: SecondTest - normalizationContext: - groups: - - nodes_sources - - node_listing - - urls - - tag_base - - translation_base - - document_display - - document_thumbnails - - document_display_sources diff --git a/tests/expected_api_resources/nstest.yml b/tests/expected_api_resources/nstest.yml deleted file mode 100644 index cc8c952f..00000000 --- a/tests/expected_api_resources/nstest.yml +++ /dev/null @@ -1,35 +0,0 @@ -resources: - App\GeneratedEntity\NSTest: - shortName: Test - types: - - Test - operations: - test_get_collection: - method: GET - class: ApiPlatform\Metadata\GetCollection - shortName: Test - normalizationContext: - enable_max_depth: true - groups: - - nodes_sources_base - - nodes_sources_default - - urls - - tag_base - - translation_base - - document_display - - document_thumbnails - - document_display_sources - test_get: - method: GET - class: ApiPlatform\Metadata\Get - shortName: Test - normalizationContext: - groups: - - nodes_sources - - node_listing - - urls - - tag_base - - translation_base - - document_display - - document_thumbnails - - document_display_sources diff --git a/tests/expected_api_resources/web_response.yml b/tests/expected_api_resources/web_response.yml deleted file mode 100644 index 176c0a5d..00000000 --- a/tests/expected_api_resources/web_response.yml +++ /dev/null @@ -1,32 +0,0 @@ -resources: - RZ\Roadiz\CoreBundle\Api\Model\WebResponse: - operations: - test_get_by_path: - method: GET - class: ApiPlatform\Metadata\Get - uriTemplate: /web_response_by_path - read: false - controller: RZ\Roadiz\CoreBundle\Api\Controller\GetWebResponseByPathController - normalizationContext: - pagination_enabled: false - enable_max_depth: true - groups: - - test_get_by_path - - nodes_sources - - node_listing - - urls - - tag_base - - translation_base - - document_display - - document_thumbnails - - document_display_sources - - web_response - - walker - - children - openapiContext: - tags: - - WebResponse - summary: 'Get a Test by its path wrapped in a WebResponse object' - description: 'Get a Test by its path wrapped in a WebResponse' - parameters: - - { type: string, name: path, in: query, required: true, description: 'Resource path, or `/` for home page', schema: { type: string } } diff --git a/tests/expected_api_resources/web_response_multiple.yml b/tests/expected_api_resources/web_response_multiple.yml deleted file mode 100644 index eff72beb..00000000 --- a/tests/expected_api_resources/web_response_multiple.yml +++ /dev/null @@ -1,61 +0,0 @@ -resources: - RZ\Roadiz\CoreBundle\Api\Model\WebResponse: - operations: - test_get_by_path: - method: GET - class: ApiPlatform\Metadata\Get - uriTemplate: /web_response_by_path - read: false - controller: RZ\Roadiz\CoreBundle\Api\Controller\GetWebResponseByPathController - normalizationContext: - pagination_enabled: false - enable_max_depth: true - groups: - - test_get_by_path - - nodes_sources - - node_listing - - urls - - tag_base - - translation_base - - document_display - - document_thumbnails - - document_display_sources - - web_response - - walker - - children - openapiContext: - tags: - - WebResponse - summary: 'Get a Test by its path wrapped in a WebResponse object' - description: 'Get a Test by its path wrapped in a WebResponse' - parameters: - - { type: string, name: path, in: query, required: true, description: 'Resource path, or `/` for home page', schema: { type: string } } - secondtest_get_by_path: - method: GET - class: ApiPlatform\Metadata\Get - uriTemplate: /web_response_by_path - read: false - controller: RZ\Roadiz\CoreBundle\Api\Controller\GetWebResponseByPathController - normalizationContext: - pagination_enabled: false - enable_max_depth: true - groups: - - secondtest_get_by_path - - nodes_sources - - node_listing - - urls - - tag_base - - translation_base - - document_display - - document_thumbnails - - document_display_sources - - web_response - - walker - - children - openapiContext: - tags: - - WebResponse - summary: 'Get a SecondTest by its path wrapped in a WebResponse object' - description: 'Get a SecondTest by its path wrapped in a WebResponse' - parameters: - - { type: string, name: path, in: query, required: true, description: 'Resource path, or `/` for home page', schema: { type: string } } diff --git a/tests/object-manager.php b/tests/object-manager.php deleted file mode 100644 index c15dbe95..00000000 --- a/tests/object-manager.php +++ /dev/null @@ -1,11 +0,0 @@ -bootEnv(__DIR__ . '/../.env'); - -$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); -$kernel->boot(); -return $kernel->getContainer()->get('doctrine')->getManager(); diff --git a/translations/core/messages.en.xlf b/translations/core/messages.en.xlf index 760ae856..186ea829 100644 --- a/translations/core/messages.en.xlf +++ b/translations/core/messages.en.xlf @@ -157,24 +157,6 @@ password_should_be_at_least_{{length}}_characters_long Password should be at least {{length}} characters long - - attributes.defaultRealm - Default realm - - - attributes.defaultRealm.help - Choose a default realm to secure any attribute value within API. This will affect only new attribute values. - - - attributes.defaultRealm.placeholder - - -- No default realm -- - Default text when no realm is attached to an attribute - - attributeValue.realm.placeholder - - -- No realm -- - Default text when no realm is attached to an attribute-value diff --git a/translations/core/messages.fr.xlf b/translations/core/messages.fr.xlf index 0210b7a4..c71a735b 100644 --- a/translations/core/messages.fr.xlf +++ b/translations/core/messages.fr.xlf @@ -157,24 +157,6 @@ password_should_be_at_least_{{length}}_characters_long Le mot de passe doit posséder au minimum {{length}} caractères - - attributes.defaultRealm - Domaine sécurisé par défaut - - - attributes.defaultRealm.help - Choisissez un domaine sécurisé pour que tous les attributs créés soient protégés dans l'API. Cela n'affectera que les nouveaux attributs. - - - attributes.defaultRealm.placeholder - - -- Aucun domaine sécurisé par défaut -- - Default text when no realm is attached to an attribute - - attributeValue.realm.placeholder - - -- Aucun domaine sécurisé -- - Default text when no realm is attached to an attribute-value diff --git a/translations/core/messages.xlf b/translations/core/messages.xlf index ecf881ff..586b3624 100644 --- a/translations/core/messages.xlf +++ b/translations/core/messages.xlf @@ -166,24 +166,6 @@ - - attributes.defaultRealm - - - - attributes.defaultRealm.help - - - - attributes.defaultRealm.placeholder - Default text when no realm is attached to an attribute - - - - attributeValue.realm.placeholder - Default text when no realm is attached to an attribute-value - - diff --git a/translations/security.ar.xlf b/translations/security.ar.xlf deleted file mode 100644 index 972f39e0..00000000 --- a/translations/security.ar.xlf +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/translations/security.de.xlf b/translations/security.de.xlf deleted file mode 100644 index 5d46fc4b..00000000 --- a/translations/security.de.xlf +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/translations/security.en.xlf b/translations/security.en.xlf index 92492801..76189574 100644 --- a/translations/security.en.xlf +++ b/translations/security.en.xlf @@ -1,23 +1,23 @@ - - - + + + Your user account is not enabled. Contact an administrator. Your user account is not enabled. Contact an administrator. - + Your credentials have expired. Please request a new password. Your credentials have expired. Please request a new password. - + Your account has expired. Contact an administrator. Your account has expired. Contact an administrator. - + Your user account is locked. Contact an administrator. Your user account is locked. Contact an administrator. - - + + diff --git a/translations/security.es.xlf b/translations/security.es.xlf deleted file mode 100644 index 73274f66..00000000 --- a/translations/security.es.xlf +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/translations/security.fr.xlf b/translations/security.fr.xlf index bc8f9ad4..1b6f7a74 100644 --- a/translations/security.fr.xlf +++ b/translations/security.fr.xlf @@ -1,23 +1,23 @@ - - - + + + Your user account is not enabled. Contact an administrator. Votre compte utilisateur n'est pas activé. Contactez un administrateur. - + Your credentials have expired. Please request a new password. - Vos informations de connexion ont expiré. Veuillez demander un nouveau mot de passe. + Vos informations d'identification ont expiré. Veuillez demander un nouveau mot de passe. - + Your account has expired. Contact an administrator. Votre compte a expiré. Contactez un administrateur. - + Your user account is locked. Contact an administrator. Votre compte utilisateur est verrouillé. Contactez un administrateur. - - + + diff --git a/translations/security.id.xlf b/translations/security.id.xlf deleted file mode 100644 index 10b490eb..00000000 --- a/translations/security.id.xlf +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/translations/security.it.xlf b/translations/security.it.xlf deleted file mode 100644 index 1c087246..00000000 --- a/translations/security.it.xlf +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/translations/security.ru.xlf b/translations/security.ru.xlf deleted file mode 100644 index bdafc723..00000000 --- a/translations/security.ru.xlf +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/translations/security.sr.xlf b/translations/security.sr.xlf deleted file mode 100644 index a24d74fa..00000000 --- a/translations/security.sr.xlf +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/translations/security.tr.xlf b/translations/security.tr.xlf deleted file mode 100644 index fe017043..00000000 --- a/translations/security.tr.xlf +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/translations/security.uk.xlf b/translations/security.uk.xlf deleted file mode 100644 index f37323e8..00000000 --- a/translations/security.uk.xlf +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/translations/security.zh.xlf b/translations/security.zh.xlf deleted file mode 100644 index 6b49359a..00000000 --- a/translations/security.zh.xlf +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/translations/validators.ar.xlf b/translations/validators.ar.xlf index f4fb0597..7fcf2644 100644 --- a/translations/validators.ar.xlf +++ b/translations/validators.ar.xlf @@ -4,7 +4,6 @@ - diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index 1abb71e8..62c27ea0 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -10,10 +10,6 @@ tagName.%name%.alreadyExists Tag %name% already exists. - - field_with_same_name_already_exists_but_with_different_doctrine_type - This field already exists with the same name in another node type, but with a different Doctrine data type. - diff --git a/translations/validators.es.xlf b/translations/validators.es.xlf index bf1185dd..b98ab19c 100644 --- a/translations/validators.es.xlf +++ b/translations/validators.es.xlf @@ -4,7 +4,6 @@ - diff --git a/translations/validators.fr.xlf b/translations/validators.fr.xlf index 6de39b55..0daaf454 100644 --- a/translations/validators.fr.xlf +++ b/translations/validators.fr.xlf @@ -10,10 +10,6 @@ tagName.%name%.alreadyExists L'étiquette «%name%» existe déjà. - - field_with_same_name_already_exists_but_with_different_doctrine_type - Ce champ existe déjà avec le même nom dans un autre type de nœud, mais avec un type de donnée Doctrine différent. - diff --git a/translations/validators.id.xlf b/translations/validators.id.xlf index 707d35fe..8d22e146 100644 --- a/translations/validators.id.xlf +++ b/translations/validators.id.xlf @@ -4,7 +4,6 @@ - diff --git a/translations/validators.it.xlf b/translations/validators.it.xlf index 0e106d9c..aa25ca67 100644 --- a/translations/validators.it.xlf +++ b/translations/validators.it.xlf @@ -4,7 +4,6 @@ - diff --git a/translations/validators.ru.xlf b/translations/validators.ru.xlf index 12dceea9..2871bce1 100644 --- a/translations/validators.ru.xlf +++ b/translations/validators.ru.xlf @@ -4,7 +4,6 @@ - diff --git a/translations/validators.sr.xlf b/translations/validators.sr.xlf index 11661084..d39ab964 100644 --- a/translations/validators.sr.xlf +++ b/translations/validators.sr.xlf @@ -4,7 +4,6 @@ - diff --git a/translations/validators.tr.xlf b/translations/validators.tr.xlf index 1241fabb..7d3ddf7b 100644 --- a/translations/validators.tr.xlf +++ b/translations/validators.tr.xlf @@ -4,7 +4,6 @@ - diff --git a/translations/validators.xlf b/translations/validators.xlf index 413ebfd4..82fbfc32 100644 --- a/translations/validators.xlf +++ b/translations/validators.xlf @@ -10,10 +10,6 @@ tagName.%name%.alreadyExists - - field_with_same_name_already_exists_but_with_different_doctrine_type - -