diff --git a/.github/workflows/run-test.yml b/.github/workflows/run-test.yml index 3b17d56e..189e3a55 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.0', '8.1'] + php-version: ['8.1', '8.2', '8.3'] steps: - uses: shivammathur/setup-php@v2 with: diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 316f0e9e..00000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -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/composer.json b/composer.json index f98a3974..e9dcb335 100644 --- a/composer.json +++ b/composer.json @@ -4,6 +4,7 @@ "keywords": [ "cms", "backoffice", + "roadiz", "rezo zero" ], "authors": [ @@ -15,8 +16,9 @@ } ], "type": "symfony-bundle", + "prefer-stable": true, "require": { - "php": ">=8.0", + "php": ">=8.1", "ext-ctype": "*", "ext-iconv": "*", "ext-zip": "*", @@ -25,7 +27,7 @@ "doctrine/annotations": "^1.0", "doctrine/doctrine-bundle": "^2.8.1", "doctrine/doctrine-migrations-bundle": "^3.1", - "doctrine/orm": "<2.17", + "doctrine/orm": "~2.17.0", "gedmo/doctrine-extensions": "^3.10.0", "inlinestyle/inlinestyle": "~1.2.7", "james-heinrich/getid3": "^1.9", @@ -33,23 +35,23 @@ "jms/serializer-bundle": "^3.10.0", "league/flysystem": "^3.0", "league/flysystem-bundle": "^3.0", - "lexik/jwt-authentication-bundle": "^2.13", + "lexik/jwt-authentication-bundle": "^2.19", "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.18.1", + "rezozero/liform-bundle": "^0.19", "rezozero/tree-walker": "^1.3.0", - "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/doc-generator": "2.2.*", + "roadiz/documents": "2.2.*", + "roadiz/dts-generator": "2.2.*", + "roadiz/entity-generator": "2.2.*", + "roadiz/jwt": "2.2.*", + "roadiz/markdown": "2.2.*", + "roadiz/models": "2.2.*", "roadiz/nodetype-contracts": "~1.1.2", - "roadiz/random": "2.1.*", + "roadiz/random": "2.2.*", "rollerworks/password-common-list": "^0.2.0", "rollerworks/password-strength-bundle": "^2.2", "scienta/doctrine-json-functions": "^4.2", @@ -62,7 +64,7 @@ "symfony/console": "5.4.*", "symfony/dotenv": "5.4.*", "symfony/expression-language": "5.4.*", - "symfony/flex": "^v1.19.4 || ^2.2.3", + "symfony/flex": "^2.2.3", "symfony/form": "5.4.*", "symfony/framework-bundle": "5.4.*", "symfony/http-client": "5.4.*", @@ -87,7 +89,7 @@ "symfony/web-link": "5.4.*", "symfony/workflow": "5.4.*", "symfony/yaml": "5.4.*", - "twig/extra-bundle": "^2.12|^3.0", + "twig/extra-bundle": "^3.0", "twig/intl-extra": "*", "twig/string-extra": "*", "twig/twig": "^3.1" @@ -130,8 +132,8 @@ }, "extra": { "branch-alias": { - "dev-main": "2.1.x-dev", - "dev-develop": "2.2.x-dev" + "dev-main": "2.2.x-dev", + "dev-develop": "2.3.x-dev" } } } diff --git a/config/api_resources/attribute.yml b/config/api_resources/attribute.yml index 11a3c2d6..1b48bcc9 100644 --- a/config/api_resources/attribute.yml +++ b/config/api_resources/attribute.yml @@ -1,5 +1,4 @@ --- RZ\Roadiz\CoreBundle\Entity\Attribute: - collectionOperations: [] - itemOperations: [] + operations: [] diff --git a/config/api_resources/attribute_value.yml b/config/api_resources/attribute_value.yml index aba402ae..23eee57d 100644 --- a/config/api_resources/attribute_value.yml +++ b/config/api_resources/attribute_value.yml @@ -1,15 +1,14 @@ --- RZ\Roadiz\CoreBundle\Entity\AttributeValue: - collectionOperations: - get: + operations: + ApiPlatform\Metadata\GetCollection: method: "GET" - normalization_context: + normalizationContext: groups: ["urls", "attribute", "document_display", "attribute_node", "attribute_documents"] enable_max_depth: true - itemOperations: - get: + ApiPlatform\Metadata\Get: method: 'GET' - normalization_context: + normalizationContext: 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 new file mode 100644 index 00000000..cb452fe8 --- /dev/null +++ b/config/api_resources/common_content.yml @@ -0,0 +1,28 @@ +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 a1db8c54..fbb79c5f 100644 --- a/config/api_resources/custom_form.yml +++ b/config/api_resources/custom_form.yml @@ -1,23 +1,23 @@ --- RZ\Roadiz\CoreBundle\Entity\CustomForm: - collectionOperations: - get: + operations: + ApiPlatform\Metadata\GetCollection: method: "GET" - normalization_context: + normalizationContext: enable_max_depth: true - itemOperations: - get: + ApiPlatform\Metadata\Get: method: 'GET' - normalization_context: + normalizationContext: enable_max_depth: true api_custom_forms_item_post: method: 'POST' - route_name: api_custom_forms_item_post - normalization_context: + class: ApiPlatform\Metadata\Post + routeName: api_custom_forms_item_post + normalizationContext: enable_max_depth: true - openapi_context: + openapiContext: summary: Post a user custom form description: | Post a user custom form @@ -59,10 +59,11 @@ RZ\Roadiz\CoreBundle\Entity\CustomForm: api_custom_forms_item_definition: method: 'GET' - route_name: api_custom_forms_item_definition - normalization_context: + class: ApiPlatform\Metadata\Get + routeName: api_custom_forms_item_definition + normalizationContext: enable_max_depth: true - openapi_context: + openapiContext: summary: Get a custom form definition for frontend description: | Get a custom form definition for frontend @@ -107,99 +108,3 @@ RZ\Roadiz\CoreBundle\Entity\CustomForm: 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 9f3f075c..d875d9f0 100644 --- a/config/api_resources/document.yml +++ b/config/api_resources/document.yml @@ -1,15 +1,15 @@ --- RZ\Roadiz\CoreBundle\Entity\Document: - collectionOperations: - get: + operations: + ApiPlatform\Metadata\GetCollection: method: "GET" - normalization_context: - groups: ["urls", "document_display", "document_display_sources", "position"] + normalizationContext: + groups: ["urls", "document_display", "document_folders", "document_folders_all", "document_display_sources"] enable_max_depth: true - itemOperations: - get: + + ApiPlatform\Metadata\Get: method: 'GET' - normalization_context: - groups: ["urls", "document", "document_display", "document_folders", "document_display_sources", "position"] + normalizationContext: + groups: ["urls", "document", "document_display", "document_folders", "document_folders_all", "document_display_sources"] enable_max_depth: true diff --git a/config/api_resources/folder.yml b/config/api_resources/folder.yml index c29268fd..38dafc52 100644 --- a/config/api_resources/folder.yml +++ b/config/api_resources/folder.yml @@ -1,11 +1,13 @@ --- RZ\Roadiz\CoreBundle\Entity\Folder: - iri: Folder - shortName: Folder - collectionOperations: {} - itemOperations: - get: + operations: + ApiPlatform\Metadata\GetCollection: method: "GET" - normalization_context: - groups: [ "folder", "position" ] + normalizationContext: + groups: [ "folder" ] + enable_max_depth: true + ApiPlatform\Metadata\Get: + method: "GET" + normalizationContext: + groups: [ "folder" ] enable_max_depth: true diff --git a/config/api_resources/node.yml b/config/api_resources/node.yml index 4a3deb0b..94e6cf1b 100644 --- a/config/api_resources/node.yml +++ b/config/api_resources/node.yml @@ -1,10 +1,8 @@ - +--- RZ\Roadiz\CoreBundle\Entity\Node: - shortName: Node - collectionOperations: {} - itemOperations: - get: + operations: + ApiPlatform\Metadata\Get: method: 'GET' - normalization_context: + normalizationContext: + groups: ["node", "document_display"] 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 d6f10d0f..602794b7 100644 --- a/config/api_resources/nodes_sources.yml +++ b/config/api_resources/nodes_sources.yml @@ -1,55 +1,66 @@ --- RZ\Roadiz\CoreBundle\Entity\NodesSources: - iri: NodesSources - shortName: NodesSources - collectionOperations: - # Get operation is needed for sitemap generation - get: + operations: + ApiPlatform\Metadata\GetCollection: method: "GET" - normalization_context: - enable_max_depth: true + normalizationContext: groups: - nodes_sources_base - nodes_sources_default + - user - 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: + api_nodes_sources_archives: + class: ApiPlatform\Metadata\GetCollection method: 'GET' - normalization_context: - enable_max_depth: true + uriTemplate: '/nodes_sources/archives' + pagination_enabled: false + pagination_client_enabled: false + extraProperties: + archive_enabled: true + archive_publication_field_name: publishedAt + normalizationContext: groups: - - nodes_sources + - 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"] diff --git a/config/api_resources/realm.yml b/config/api_resources/realm.yml index 45da623a..50dbdd8b 100644 --- a/config/api_resources/realm.yml +++ b/config/api_resources/realm.yml @@ -1,11 +1,14 @@ --- RZ\Roadiz\CoreBundle\Entity\Realm: - iri: Realm - shortName: Realm - collectionOperations: {} - itemOperations: - get: + operations: + ApiPlatform\Metadata\GetCollection: method: "GET" - normalization_context: - groups: [ "get", "realm" ] + normalizationContext: + groups: [ "realm" ] + enable_max_depth: true + + ApiPlatform\Metadata\Get: + method: "GET" + normalizationContext: + groups: [ "realm" ] enable_max_depth: true diff --git a/config/api_resources/tag.yml b/config/api_resources/tag.yml index dc516bb0..3f35d12b 100644 --- a/config/api_resources/tag.yml +++ b/config/api_resources/tag.yml @@ -1,20 +1,18 @@ - +--- RZ\Roadiz\CoreBundle\Entity\Tag: - iri: Tag - shortName: Tag - collectionOperations: - get: + operations: + ApiPlatform\Metadata\GetCollection: method: "GET" - normalization_context: + normalizationContext: enable_max_depth: true groups: - - tag - tag_base - itemOperations: - get: + + ApiPlatform\Metadata\Get: method: 'GET' - normalization_context: + normalizationContext: enable_max_depth: true groups: - tag - tag_base + - tag_parent diff --git a/config/api_resources/translation.yml b/config/api_resources/translation.yml index 304ffa9e..ad19c01c 100644 --- a/config/api_resources/translation.yml +++ b/config/api_resources/translation.yml @@ -1,9 +1,17 @@ - +--- RZ\Roadiz\CoreBundle\Entity\Translation: - collectionOperations: - get: + operations: + ApiPlatform\Metadata\GetCollection: method: "GET" - itemOperations: - get: + normalizationContext: + enable_max_depth: true + groups: + - translation_base + + ApiPlatform\Metadata\Get: method: 'GET' + normalizationContext: + enable_max_depth: true + groups: + - translation_base diff --git a/config/api_resources/web_response.yml b/config/api_resources/web_response.yml index 2353a4c1..cf9513c9 100644 --- a/config/api_resources/web_response.yml +++ b/config/api_resources/web_response.yml @@ -1,13 +1,13 @@ RZ\Roadiz\CoreBundle\Api\Model\WebResponse: - collectionOperations: {} - itemOperations: + operations: getByPath: + class: ApiPlatform\Metadata\Get method: 'GET' - path: '/web_response_by_path' + uriTemplate: '/web_response_by_path' read: false controller: RZ\Roadiz\CoreBundle\Api\Controller\GetWebResponseByPathController pagination_enabled: false - normalization_context: + normalizationContext: enable_max_depth: true pagination_enabled: false groups: @@ -20,12 +20,14 @@ RZ\Roadiz\CoreBundle\Api\Model\WebResponse: - children - children_count - nodes_sources + - node_listing - urls - tag_base - translation_base - document_display - node_attributes - openapi_context: + - document_display_sources + openapiContext: summary: Get a resource by its path wrapped in a WebResponse object description: | Get a resource by its path wrapped in a WebResponse diff --git a/config/packages/api_platform.yaml b/config/packages/api_platform.yaml index 0fc9e246..f9551bd7 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: "My Roadiz website API" - description: "My Roadiz website API" + title: "%env(string:APP_TITLE)%" + description: "%env(string:APP_DESCRIPTION)%" version: '%env(string:APP_VERSION)%' mapping: paths: @@ -19,32 +19,27 @@ 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' -# 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)%'] + + collection: + pagination: + page_parameter_name: page + items_per_page_parameter_name: itemsPerPage + + http_cache: + 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'] - collection: - pagination: - items_per_page: 15 # Default value - maximum_items_per_page: 50 - client_items_per_page: true - items_per_page_parameter_name: itemsPerPage + 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' ] diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index 092fadc1..8165a234 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -5,43 +5,67 @@ 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 - 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 + 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 resolve_target_entities: Symfony\Component\Security\Core\User\UserInterface: RZ\Roadiz\CoreBundle\Entity\User diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 4c2ce00a..840b2f1d 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -8,6 +8,8 @@ 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 @@ -20,12 +22,12 @@ security: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false - # JWT for API - api: - pattern: ^/api + + # https://symfony.com/bundles/LexikJWTAuthenticationBundle/current/index.html#configure-application-routing + api_login: + pattern: ^/api/token stateless: true provider: all_users - user_checker: RZ\Roadiz\CoreBundle\Security\UserChecker login_throttling: max_attempts: 3 json_login: @@ -34,7 +36,21 @@ 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 @@ -63,18 +79,16 @@ 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: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: ^/rz-admin/login, roles: PUBLIC_ACCESS } + - { path: ^/rz-admin/logout, roles: PUBLIC_ACCESS } - { path: ^/rz-admin, roles: ROLE_BACKEND_USER } - - { path: ^/api/token, roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: "^/api/custom_forms/(?:[0-9]+)/post", methods: [ POST ], roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: ^/api/token, roles: PUBLIC_ACCESS } + - { path: "^/api/custom_forms/(?:[0-9]+)/post", methods: [ POST ], roles: PUBLIC_ACCESS } - { path: ^/api, roles: ROLE_BACKEND_USER, methods: [ POST, PUT, PATCH, DELETE ] } # - { path: ^/profile, roles: ROLE_USER } diff --git a/config/services.yaml b/config/services.yaml index d66b984e..36148d3d 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -1,6 +1,6 @@ --- parameters: - roadiz_core.cms_version: '2.1.62' + roadiz_core.cms_version: '2.2.21' roadiz_core.cms_version_prefix: 'main' env(APP_NAMESPACE): "roadiz" env(APP_VERSION): "0.1.0" @@ -42,6 +42,7 @@ services: $apiResourcesDir: '%kernel.project_dir%/config/api_resources' $debug: '%kernel.debug%' $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%' @@ -113,6 +114,21 @@ 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 @@ -370,11 +386,16 @@ services: RZ\Roadiz\CoreBundle\Preview\RequestPreviewRevolver: arguments: - '@Symfony\Component\HttpFoundation\RequestStack' - - 'ROLE_BACKEND_USER' + - '%roadiz_core.preview_required_role_name%' 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'] @@ -444,7 +465,6 @@ 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' diff --git a/migrations/Version20201203004857.php b/migrations/Version20201203004857.php index 746639af..48ccc417 100644 --- a/migrations/Version20201203004857.php +++ b/migrations/Version20201203004857.php @@ -7,7 +7,7 @@ use Doctrine\Migrations\AbstractMigration; /** - * Database initialization migration for MySQL/MariaDB. + * Database initialization migration. * * @package RZ\Roadiz\Migrations */ @@ -15,17 +15,22 @@ final class Version20201203004857 extends AbstractMigration { public function getDescription() : string { - return 'Database initialization migration for MySQL/MariaDB.'; + return 'Database initialization migration.'; } 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'); @@ -133,6 +138,348 @@ public function up(Schema $schema) : 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 5c22be4b..3e099267 100644 --- a/migrations/Version20201225181256.php +++ b/migrations/Version20201225181256.php @@ -9,361 +9,19 @@ /** * 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 'Database initialization migration for PostgreSQL.'; + return '[deprecated] Database initialization migration for PostgreSQL.'; } public function up(Schema $schema) : void { - $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'); + $this->write('Nothing to do with RZ\Roadiz\Migrations\Version20201225181256.'); } public function down(Schema $schema) : void diff --git a/migrations/Version20230607134403.php b/migrations/Version20230607134403.php new file mode 100644 index 00000000..7b50e986 --- /dev/null +++ b/migrations/Version20230607134403.php @@ -0,0 +1,80 @@ +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 new file mode 100644 index 00000000..abed8b48 --- /dev/null +++ b/migrations/Version20230615122615.php @@ -0,0 +1,38 @@ +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 new file mode 100644 index 00000000..98f4c214 --- /dev/null +++ b/migrations/Version20230628143106.php @@ -0,0 +1,74 @@ +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 new file mode 100644 index 00000000..84dc01b3 --- /dev/null +++ b/migrations/Version20230628170203.php @@ -0,0 +1,40 @@ +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 new file mode 100644 index 00000000..cf03d449 --- /dev/null +++ b/migrations/Version20230712171650.php @@ -0,0 +1,36 @@ +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 new file mode 100644 index 00000000..5eebad1f --- /dev/null +++ b/migrations/Version20230828092821.php @@ -0,0 +1,46 @@ +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 new file mode 100644 index 00000000..fc6b3521 --- /dev/null +++ b/migrations/Version20230829082257.php @@ -0,0 +1,38 @@ +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 new file mode 100644 index 00000000..823029ea --- /dev/null +++ b/migrations/Version20230905140844.php @@ -0,0 +1,46 @@ +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 new file mode 100644 index 00000000..cc0bd784 --- /dev/null +++ b/migrations/Version20230915134833.php @@ -0,0 +1,36 @@ +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 new file mode 100644 index 00000000..5b71054d --- /dev/null +++ b/migrations/Version20231012154717.php @@ -0,0 +1,36 @@ +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 new file mode 100644 index 00000000..b4b0829a --- /dev/null +++ b/migrations/Version20231013132932.php @@ -0,0 +1,38 @@ +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/Version20240603210209.php b/migrations/Version20240603210209.php new file mode 100644 index 00000000..7aa4ccd3 --- /dev/null +++ b/migrations/Version20240603210209.php @@ -0,0 +1,33 @@ +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 new file mode 100644 index 00000000..da201acc --- /dev/null +++ b/migrations/Version20240604143759.php @@ -0,0 +1,33 @@ +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 8785b98d..a032d3d1 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -26,6 +26,8 @@ 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 checkGenericClassInNonGenericObjectType: false diff --git a/src/Api/Breadcrumbs/NodesSourcesBreadcrumbsFactory.php b/src/Api/Breadcrumbs/NodesSourcesBreadcrumbsFactory.php index 5a5e3fe9..07fcf193 100644 --- a/src/Api/Breadcrumbs/NodesSourcesBreadcrumbsFactory.php +++ b/src/Api/Breadcrumbs/NodesSourcesBreadcrumbsFactory.php @@ -20,7 +20,6 @@ public function create(?PersistableInterface $entity): ?BreadcrumbsInterface } if ( - null === $entity->getNode() || null === $entity->getNode()->getNodeType() || !$entity->isReachable() ) { diff --git a/src/Api/Controller/GetWebResponseByPathController.php b/src/Api/Controller/GetWebResponseByPathController.php index 6441d6a4..8d9905bd 100644 --- a/src/Api/Controller/GetWebResponseByPathController.php +++ b/src/Api/Controller/GetWebResponseByPathController.php @@ -4,8 +4,8 @@ namespace RZ\Roadiz\CoreBundle\Api\Controller; -use ApiPlatform\Core\Api\IriConverterInterface; -use ApiPlatform\Core\Exception\InvalidArgumentException; +use ApiPlatform\Api\IriConverterInterface; +use ApiPlatform\Exception\InvalidArgumentException; use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; use RZ\Roadiz\CoreBundle\Api\DataTransformer\WebResponseDataTransformerInterface; use RZ\Roadiz\CoreBundle\Api\Model\WebResponseInterface; @@ -120,8 +120,8 @@ protected function addResourceToCacheTags(PersistableInterface $resource): void { $request = $this->requestStack->getMainRequest(); if (null !== $request) { - $iri = $this->iriConverter->getIriFromItem($resource); - $request->attributes->set('_resources', $request->attributes->get('_resources', []) + [$iri]); + $iri = $this->iriConverter->getIriFromResource($resource); + $request->attributes->set('_resources', $request->attributes->get('_resources', []) + [ $iri => $iri ]); } } } diff --git a/src/Api/Controller/TranslationAwareControllerTrait.php b/src/Api/Controller/TranslationAwareControllerTrait.php index f291948d..de21fd9e 100644 --- a/src/Api/Controller/TranslationAwareControllerTrait.php +++ b/src/Api/Controller/TranslationAwareControllerTrait.php @@ -4,6 +4,7 @@ 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; @@ -16,12 +17,15 @@ 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'); /** @var TranslationRepository $repository */ $repository = $this->getManagerRegistry()->getRepository(TranslationInterface::class); - if (null === $locale) { + if (!\is_string($locale) || $locale === '') { return $repository->findDefault(); } diff --git a/src/Api/DataTransformer/AttributeOutputDataTransformer.php b/src/Api/DataTransformer/AttributeOutputDataTransformer.php deleted file mode 100644 index 15bdea15..00000000 --- a/src/Api/DataTransformer/AttributeOutputDataTransformer.php +++ /dev/null @@ -1,45 +0,0 @@ -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 deleted file mode 100644 index 9421721b..00000000 --- a/src/Api/DataTransformer/AttributeValueOutputDataTransformer.php +++ /dev/null @@ -1,48 +0,0 @@ -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 deleted file mode 100644 index 2465864f..00000000 --- a/src/Api/DataTransformer/BaseNodesSourcesOutputDataTransformer.php +++ /dev/null @@ -1,35 +0,0 @@ -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 b5be9688..2e89b12e 100644 --- a/src/Api/DataTransformer/BlocksAwareWebResponseOutputDataTransformerTrait.php +++ b/src/Api/DataTransformer/BlocksAwareWebResponseOutputDataTransformerTrait.php @@ -8,6 +8,7 @@ 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; @@ -16,6 +17,7 @@ trait BlocksAwareWebResponseOutputDataTransformerTrait { abstract protected function getWalkerContext(): WalkerContextInterface; abstract protected function getCacheItemPool(): CacheItemPoolInterface; + abstract protected function getTreeWalkerGenerator(): TreeWalkerGenerator; abstract protected function getChildrenNodeSourceWalkerMaxLevel(): int; /** @@ -23,22 +25,17 @@ 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()) { - /** @var class-string $childrenNodeSourceWalkerClassname */ - $childrenNodeSourceWalkerClassname = $this->getChildrenNodeSourceWalkerClassname(); - $output->setBlocks($childrenNodeSourceWalkerClassname::build( + $walker = $this->getTreeWalkerGenerator()->buildForRoot( $data, + $this->getChildrenNodeSourceWalkerClassname(), $this->getWalkerContext(), $this->getChildrenNodeSourceWalkerMaxLevel(), $this->getCacheItemPool() - )->getChildren()); + ); + $output->setBlocks($walker->getChildren()); } return $output; diff --git a/src/Api/DataTransformer/CustomFormOutputDataTransformer.php b/src/Api/DataTransformer/CustomFormOutputDataTransformer.php deleted file mode 100644 index c4411bca..00000000 --- a/src/Api/DataTransformer/CustomFormOutputDataTransformer.php +++ /dev/null @@ -1,59 +0,0 @@ -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 deleted file mode 100644 index 7455219f..00000000 --- a/src/Api/DataTransformer/DocumentOutputDataTransformer.php +++ /dev/null @@ -1,96 +0,0 @@ -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 deleted file mode 100644 index 63d8b79b..00000000 --- a/src/Api/DataTransformer/FolderOutputDataTransformer.php +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index 3dec987d..00000000 --- a/src/Api/DataTransformer/NodeOutputDataTransformer.php +++ /dev/null @@ -1,39 +0,0 @@ -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 deleted file mode 100644 index 03c85bbd..00000000 --- a/src/Api/DataTransformer/NodesSourcesOutputDataTransformer.php +++ /dev/null @@ -1,52 +0,0 @@ -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 deleted file mode 100644 index 947a0acc..00000000 --- a/src/Api/DataTransformer/TagOutputDataTransformer.php +++ /dev/null @@ -1,53 +0,0 @@ -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 deleted file mode 100644 index 41f6d702..00000000 --- a/src/Api/DataTransformer/TranslationOutputDataTransformer.php +++ /dev/null @@ -1,36 +0,0 @@ -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/WebResponseOutputDataTransformer.php b/src/Api/DataTransformer/WebResponseOutputDataTransformer.php index 950eeef1..a91c4b04 100644 --- a/src/Api/DataTransformer/WebResponseOutputDataTransformer.php +++ b/src/Api/DataTransformer/WebResponseOutputDataTransformer.php @@ -12,8 +12,10 @@ 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; @@ -29,6 +31,7 @@ class WebResponseOutputDataTransformer implements WebResponseDataTransformerInte private CacheItemPoolInterface $cacheItemPool; private UrlGeneratorInterface $urlGenerator; private RealmResolverInterface $realmResolver; + private TreeWalkerGenerator $treeWalkerGenerator; public function __construct( NodesSourcesHeadFactoryInterface $nodesSourcesHeadFactory, @@ -36,7 +39,8 @@ public function __construct( WalkerContextInterface $walkerContext, CacheItemPoolInterface $cacheItemPool, UrlGeneratorInterface $urlGenerator, - RealmResolverInterface $realmResolver + RealmResolverInterface $realmResolver, + TreeWalkerGenerator $treeWalkerGenerator ) { $this->nodesSourcesHeadFactory = $nodesSourcesHeadFactory; $this->breadcrumbsFactory = $breadcrumbsFactory; @@ -44,6 +48,7 @@ public function __construct( $this->cacheItemPool = $cacheItemPool; $this->urlGenerator = $urlGenerator; $this->realmResolver = $realmResolver; + $this->treeWalkerGenerator = $treeWalkerGenerator; } protected function getWalkerContext(): WalkerContextInterface @@ -61,6 +66,14 @@ protected function getChildrenNodeSourceWalkerMaxLevel(): int return 5; } + public function getTreeWalkerGenerator(): TreeWalkerGenerator + { + return $this->treeWalkerGenerator; + } + + /** + * @return class-string + */ protected function getChildrenNodeSourceWalkerClassname(): string { return AutoChildrenNodeSourceWalker::class; diff --git a/src/Api/Dto/AttributeOutput.php b/src/Api/Dto/AttributeOutput.php deleted file mode 100644 index 4ed39205..00000000 --- a/src/Api/Dto/AttributeOutput.php +++ /dev/null @@ -1,51 +0,0 @@ - - */ - #[Groups(['attribute'])] - public array $documents = []; -} diff --git a/src/Api/Dto/AttributeValueOutput.php b/src/Api/Dto/AttributeValueOutput.php deleted file mode 100644 index 9c1457eb..00000000 --- a/src/Api/Dto/AttributeValueOutput.php +++ /dev/null @@ -1,41 +0,0 @@ - - */ - #[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 deleted file mode 100644 index cd152ed1..00000000 --- a/src/Api/Dto/FolderOutput.php +++ /dev/null @@ -1,34 +0,0 @@ - - */ - #[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 deleted file mode 100644 index 0abd4630..00000000 --- a/src/Api/Dto/TranslationOutput.php +++ /dev/null @@ -1,34 +0,0 @@ -resourceMetadataFactory = $resourceMetadataFactory; $this->requestStack = $requestStack; $this->defaultPublicationFieldName = $defaultPublicationFieldName; } @@ -63,9 +58,10 @@ public function applyToCollection( QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, - string $operationName = null + ?Operation $operation = null, + array $context = [] ): void { - if (!$this->supportsResult($resourceClass, $operationName)) { + if (!$this->supportsResult($resourceClass, $operation)) { return; } if (null === $request = $this->requestStack->getCurrentRequest()) { @@ -73,7 +69,7 @@ public function applyToCollection( } $aliases = $queryBuilder->getRootAliases(); $alias = reset($aliases); - $publicationFieldName = $this->getPublicationFieldName($request, $this->resourceMetadataFactory->create($resourceClass), $operationName); + $publicationFieldName = $this->getPublicationFieldName($operation); $publicationField = $alias . '.' . $publicationFieldName; $queryBuilder->select($publicationField) @@ -81,17 +77,21 @@ public function applyToCollection( ->orderBy($publicationField, 'DESC'); } - public function supportsResult(string $resourceClass, string $operationName = null): bool + public function supportsResult(string $resourceClass, ?Operation $operation = null, array $context = []): bool { if (null === $request = $this->requestStack->getCurrentRequest()) { return false; } - return $this->isArchiveEnabled($request, $this->resourceMetadataFactory->create($resourceClass), $operationName); + return $this->isArchiveEnabled($operation); } - public function getResult(QueryBuilder $queryBuilder): iterable - { + public function getResult( + QueryBuilder $queryBuilder, + ?string $resourceClass = null, + ?Operation $operation = null, + array $context = [] + ): iterable { $entities = []; $dates = []; $paginator = new Paginator($queryBuilder, false); @@ -104,7 +104,7 @@ public function getResult(QueryBuilder $queryBuilder): iterable foreach ($paginator as $result) { $dateTimeField = reset($result); - if ($dateTimeField instanceof \DateTime) { + if ($dateTimeField instanceof \DateTimeInterface) { $year = $dateTimeField->format('Y'); $month = $dateTimeField->format('Y-m'); @@ -128,28 +128,14 @@ public function getResult(QueryBuilder $queryBuilder): iterable } private function isArchiveEnabled( - Request $request, - ResourceMetadata $resourceMetadata, - string $operationName = null + ?Operation $operation = null ): bool { - return $resourceMetadata->getCollectionOperationAttribute( - $operationName, - 'archive_enabled', - false, - true - ); + return $operation->getExtraProperties()['archive_enabled'] ?? false; } private function getPublicationFieldName( - Request $request, - ResourceMetadata $resourceMetadata, - string $operationName = null + ?Operation $operation = null ): string { - return $resourceMetadata->getCollectionOperationAttribute( - $operationName, - 'archive_publication_field_name', - $this->defaultPublicationFieldName, - true - ); + return $operation->getExtraProperties()['archive_publication_field_name'] ?? $this->defaultPublicationFieldName; } } diff --git a/src/Api/Extension/AttributeValueQueryExtension.php b/src/Api/Extension/AttributeValueQueryExtension.php new file mode 100644 index 00000000..3a095af7 --- /dev/null +++ b/src/Api/Extension/AttributeValueQueryExtension.php @@ -0,0 +1,84 @@ +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 new file mode 100644 index 00000000..a43c17fd --- /dev/null +++ b/src/Api/Extension/AttributeValueRealmExtension.php @@ -0,0 +1,69 @@ +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 8b4b0611..7d3894da 100644 --- a/src/Api/Extension/DocumentQueryExtension.php +++ b/src/Api/Extension/DocumentQueryExtension.php @@ -4,9 +4,10 @@ namespace RZ\Roadiz\CoreBundle\Api\Extension; -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 ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface; +use ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\Operation; use Doctrine\ORM\QueryBuilder; use RZ\Roadiz\CoreBundle\Entity\Document; @@ -15,8 +16,7 @@ final class DocumentQueryExtension implements QueryItemExtensionInterface, Query private function apply( QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, - string $resourceClass, - string $operationName = null + string $resourceClass ): void { if ($resourceClass !== Document::class) { return; @@ -32,18 +32,19 @@ public function applyToItem( QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, - string $operationName = null, + Operation $operation = null, array $context = [] ): void { - $this->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName); + $this->apply($queryBuilder, $queryNameGenerator, $resourceClass); } public function applyToCollection( QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, - string $operationName = null + Operation $operation = null, + array $context = [] ): void { - $this->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName); + $this->apply($queryBuilder, $queryNameGenerator, $resourceClass); } } diff --git a/src/Api/Extension/NodeQueryExtension.php b/src/Api/Extension/NodeQueryExtension.php index 7b259f71..0b15784e 100644 --- a/src/Api/Extension/NodeQueryExtension.php +++ b/src/Api/Extension/NodeQueryExtension.php @@ -4,10 +4,11 @@ namespace RZ\Roadiz\CoreBundle\Api\Extension; -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 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 Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; use RZ\Roadiz\CoreBundle\Entity\Node; @@ -28,17 +29,16 @@ public function applyToItem( QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, - string $operationName = null, + Operation $operation = null, array $context = [] ): void { - $this->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName); + $this->apply($queryBuilder, $queryNameGenerator, $resourceClass); } private function apply( QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, - string $resourceClass, - string $operationName = null + string $resourceClass ): void { if ($resourceClass !== Node::class) { return; @@ -70,8 +70,9 @@ public function applyToCollection( QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, - string $operationName = null + Operation $operation = null, + array $context = [] ): void { - $this->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName); + $this->apply($queryBuilder, $queryNameGenerator, $resourceClass); } } diff --git a/src/Api/Extension/NodesSourcesQueryExtension.php b/src/Api/Extension/NodesSourcesQueryExtension.php index 2239df4f..2e5b3960 100644 --- a/src/Api/Extension/NodesSourcesQueryExtension.php +++ b/src/Api/Extension/NodesSourcesQueryExtension.php @@ -4,10 +4,11 @@ namespace RZ\Roadiz\CoreBundle\Api\Extension; -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 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 Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; use RZ\Roadiz\CoreBundle\Entity\Node; @@ -32,26 +33,26 @@ public function applyToItem( QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, - string $operationName = null, + ?Operation $operation = null, array $context = [] ): void { - $this->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName); + $this->apply($queryBuilder, $queryNameGenerator, $resourceClass); } public function applyToCollection( QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, - string $operationName = null + ?Operation $operation = null, + array $context = [] ): void { - $this->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName); + $this->apply($queryBuilder, $queryNameGenerator, $resourceClass); } private function apply( QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, - string $resourceClass, - string $operationName = null + string $resourceClass ): void { if ( $resourceClass !== NodesSources::class && diff --git a/src/Api/Extension/NodesTagsQueryExtension.php b/src/Api/Extension/NodesTagsQueryExtension.php new file mode 100644 index 00000000..a5ea3cb3 --- /dev/null +++ b/src/Api/Extension/NodesTagsQueryExtension.php @@ -0,0 +1,89 @@ +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 a7cf3fc6..c6d5d995 100644 --- a/src/Api/Filter/ArchiveFilter.php +++ b/src/Api/Filter/ArchiveFilter.php @@ -4,14 +4,16 @@ namespace RZ\Roadiz\CoreBundle\Api\Filter; -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\Doctrine\Common\PropertyHelperTrait; +use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter; +use ApiPlatform\Doctrine\Orm\Filter\DateFilter; +use ApiPlatform\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 AbstractContextAwareFilter +final class ArchiveFilter extends AbstractFilter { use PropertyHelperTrait; @@ -37,18 +39,19 @@ protected function isDateField(string $property, string $resourceClass): bool */ protected function filterProperty( string $property, - $values, + $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, - string $operationName = null + ?Operation $operation = null, + array $context = [] ): 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($values[self::PARAMETER_ARCHIVE]) + !isset($value[self::PARAMETER_ARCHIVE]) ) { return; } @@ -57,17 +60,24 @@ protected function filterProperty( $field = $property; if ($this->isPropertyNested($property, $resourceClass)) { - [$alias, $field] = $this->addJoinsForNestedProperty($property, $alias, $queryBuilder, $queryNameGenerator, $resourceClass); + [$alias, $field] = $this->addJoinsForNestedProperty( + $property, + $alias, + $queryBuilder, + $queryNameGenerator, + $resourceClass, + Join::INNER_JOIN + ); } - if (!is_string($values[self::PARAMETER_ARCHIVE])) { + if (!is_string($value[self::PARAMETER_ARCHIVE])) { throw new FilterValidationException([sprintf( '“%s” filter must be only used with a string value.', self::PARAMETER_ARCHIVE )]); } - $range = $this->normalizeFilteringDates($values[self::PARAMETER_ARCHIVE]); + $range = $this->normalizeFilteringDates($value[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 513ba4fd..3d649566 100644 --- a/src/Api/Filter/CopyrightValidFilter.php +++ b/src/Api/Filter/CopyrightValidFilter.php @@ -4,12 +4,13 @@ namespace RZ\Roadiz\CoreBundle\Api\Filter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\Operation; use Doctrine\ORM\QueryBuilder; use RZ\Roadiz\CoreBundle\Entity\Document; -final class CopyrightValidFilter extends AbstractContextAwareFilter +final class CopyrightValidFilter extends AbstractFilter { public const PARAMETER = 'copyrightValid'; public const TRUE_VALUES = [1, '1', 'true', true, 'on', 'yes']; @@ -21,7 +22,8 @@ protected function filterProperty( QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, - string $operationName = null + ?Operation $operation = null, + array $context = [] ): void { if ($property !== self::PARAMETER) { return; diff --git a/src/Api/Filter/GeneratedEntityFilter.php b/src/Api/Filter/GeneratedEntityFilter.php index d34cb712..255be66d 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\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter; +use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter; use Doctrine\Persistence\ManagerRegistry; use Psr\Log\LoggerInterface; -use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; -abstract class GeneratedEntityFilter extends AbstractContextAwareFilter +abstract class GeneratedEntityFilter extends AbstractFilter { 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, - ?RequestStack $requestStack = null, - string $generatedEntityNamespacePattern = '#^App\\\GeneratedEntity\\\NS(?:[a-zA-Z]+)$#', LoggerInterface $logger = null, - array $properties = null + array $properties = null, + NameConverterInterface $nameConverter = null, + string $generatedEntityNamespacePattern = '#^App\\\GeneratedEntity\\\NS(?:[a-zA-Z]+)$#', ) { - parent::__construct($managerRegistry, $requestStack, $logger, $properties); + parent::__construct($managerRegistry, $logger, $properties, $nameConverter); $this->generatedEntityNamespacePattern = $generatedEntityNamespacePattern; } diff --git a/src/Api/Filter/IntersectionFilter.php b/src/Api/Filter/IntersectionFilter.php index 10fb2d06..fc7cf395 100644 --- a/src/Api/Filter/IntersectionFilter.php +++ b/src/Api/Filter/IntersectionFilter.php @@ -4,16 +4,17 @@ namespace RZ\Roadiz\CoreBundle\Api\Filter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter; 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 AbstractContextAwareFilter +final class IntersectionFilter extends AbstractFilter { public const PARAMETER = 'intersect'; @@ -26,7 +27,8 @@ protected function filterProperty( QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, - string $operationName = null + ?Operation $operation = null, + array $context = [] ): void { if ($property !== IntersectionFilter::PARAMETER || !is_array($value)) { return; diff --git a/src/Api/Filter/LocaleFilter.php b/src/Api/Filter/LocaleFilter.php index 95da674d..c6518139 100644 --- a/src/Api/Filter/LocaleFilter.php +++ b/src/Api/Filter/LocaleFilter.php @@ -4,15 +4,16 @@ namespace RZ\Roadiz\CoreBundle\Api\Filter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\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\HttpFoundation\RequestStack; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; final class LocaleFilter extends GeneratedEntityFilter { @@ -23,34 +24,23 @@ 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 + array $properties = null, + NameConverterInterface $nameConverter = null, + string $generatedEntityNamespacePattern = '#^App\\\GeneratedEntity\\\NS(?:[a-zA-Z]+)$#' ) { - parent::__construct($managerRegistry, $requestStack, $generatedEntityNamespacePattern, $logger, $properties); + parent::__construct($managerRegistry, $logger, $properties, $nameConverter, $generatedEntityNamespacePattern); $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, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, - string $operationName = null + ?Operation $operation = null, + array $context = [] ): void { if ($property !== self::PROPERTY) { return; diff --git a/src/Api/Filter/NotFilter.php b/src/Api/Filter/NotFilter.php index 22e37e2a..2961bc99 100644 --- a/src/Api/Filter/NotFilter.php +++ b/src/Api/Filter/NotFilter.php @@ -4,33 +4,25 @@ namespace RZ\Roadiz\CoreBundle\Api\Filter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter; +use ApiPlatform\Metadata\Operation; +use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; use Symfony\Component\String\Slugger\AsciiSlugger; -final class NotFilter extends AbstractContextAwareFilter +final class NotFilter extends AbstractFilter { 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, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, - string $operationName = null + ?Operation $operation = null, + array $context = [] ): void { if ($property !== self::PARAMETER || !\is_array($value)) { return; @@ -41,7 +33,14 @@ protected function filterProperty( $field = $property; if ($this->isPropertyNested($property, $resourceClass)) { - list($alias, $field) = $this->addJoinsForNestedProperty($property, $alias, $queryBuilder, $queryNameGenerator); + list($alias, $field) = $this->addJoinsForNestedProperty( + $property, + $alias, + $queryBuilder, + $queryNameGenerator, + $resourceClass, + Join::INNER_JOIN + ); } $placeholder = ':' . (new AsciiSlugger())->slug($alias . '_' . $field, '_')->toString(); diff --git a/src/Api/ListManager/SolrPaginator.php b/src/Api/ListManager/SolrPaginator.php index 99d467be..8eedf602 100644 --- a/src/Api/ListManager/SolrPaginator.php +++ b/src/Api/ListManager/SolrPaginator.php @@ -62,6 +62,10 @@ public function getItemsPerPage(): float public function getIterator(): \Traversable { $this->handleOnce(); - return new ArrayCollection($this->listManager->getEntities()); + $entities = $this->listManager->getEntities(); + if (\is_array($entities)) { + return new \ArrayIterator($entities); + } + return $entities->getIterator(); } } diff --git a/src/Api/ListManager/SolrSearchListManager.php b/src/Api/ListManager/SolrSearchListManager.php index 2185a575..a79c575a 100644 --- a/src/Api/ListManager/SolrSearchListManager.php +++ b/src/Api/ListManager/SolrSearchListManager.php @@ -15,6 +15,7 @@ final class SolrSearchListManager extends AbstractEntityListManager protected ?SearchResultsInterface $searchResults; private array $criteria; private bool $searchInTags; + private ?string $query = null; public function __construct( ?Request $request, @@ -34,31 +35,18 @@ public function handle(bool $disabled = false) throw new \InvalidArgumentException('Cannot handle a NULL request.'); } - $query = trim($this->request->query->get('search') ?? ''); + $this->handleRequestQuery($disabled); - 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')); + if (null === $this->query) { + throw new \InvalidArgumentException('Cannot handle a NULL query.'); } - /* * Query must be longer than 3 chars or Solr might crash * on highlighting fields. */ - if (\mb_strlen($query) > 3) { + if (\mb_strlen($this->query) > 3) { $this->searchResults = $this->searchHandler->searchWithHighlight( - $query, # Use ?q query parameter to search with + $this->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, @@ -67,7 +55,7 @@ public function handle(bool $disabled = false) ); } else { $this->searchResults = $this->searchHandler->search( - $query, # Use ?q query parameter to search with + $this->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, @@ -77,6 +65,12 @@ public function handle(bool $disabled = false) } } + protected function handleSearchParam(string $search): void + { + parent::handleSearchParam($search); + $this->query = trim($search); + } + /** * @inheritDoc */ diff --git a/src/Api/Model/WebResponse.php b/src/Api/Model/WebResponse.php index 2cf0b7e4..4d78feab 100644 --- a/src/Api/Model/WebResponse.php +++ b/src/Api/Model/WebResponse.php @@ -4,7 +4,7 @@ namespace RZ\Roadiz\CoreBundle\Api\Model; -use ApiPlatform\Core\Annotation\ApiProperty; +use ApiPlatform\Metadata\ApiProperty; use Doctrine\Common\Collections\Collection; use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; use RZ\Roadiz\CoreBundle\Api\Breadcrumbs\BreadcrumbsInterface; diff --git a/src/Api/TreeWalker/Definition/DefinitionFactoryConfiguration.php b/src/Api/TreeWalker/Definition/DefinitionFactoryConfiguration.php new file mode 100644 index 00000000..a63ca9c1 --- /dev/null +++ b/src/Api/TreeWalker/Definition/DefinitionFactoryConfiguration.php @@ -0,0 +1,27 @@ +classname = $classname; + $this->onlyVisible = $onlyVisible; + $this->definitionFactory = $definitionFactory; + } +} diff --git a/src/Api/TreeWalker/Definition/DefinitionFactoryInterface.php b/src/Api/TreeWalker/Definition/DefinitionFactoryInterface.php new file mode 100644 index 00000000..85e75536 --- /dev/null +++ b/src/Api/TreeWalker/Definition/DefinitionFactoryInterface.php @@ -0,0 +1,12 @@ +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 4ad2f99b..22fbc6ac 100644 --- a/src/Api/TreeWalker/Definition/ReachableNodeSourceDefinition.php +++ b/src/Api/TreeWalker/Definition/ReachableNodeSourceDefinition.php @@ -47,6 +47,8 @@ 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 1e357445..7a8bff01 100644 --- a/src/Api/TreeWalker/TreeWalkerGenerator.php +++ b/src/Api/TreeWalker/TreeWalkerGenerator.php @@ -6,9 +6,12 @@ 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; @@ -20,6 +23,11 @@ final class TreeWalkerGenerator private WalkerContextInterface $walkerContext; private CacheItemPoolInterface $cacheItemPool; + /** + * @var array + */ + private array $walkerDefinitionFactories = []; + public function __construct( NodeSourceApi $nodeSourceApi, NodeTypes $nodeTypesBag, @@ -34,7 +42,7 @@ public function __construct( /** * @param string $nodeType - * @param class-string $walkerClass + * @param class-string $walkerClass * @param TranslationInterface $translation * @param int $maxLevel * @return array @@ -54,14 +62,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] = call_user_func( - [$walkerClass, 'build'], + $walkers[$walkerName] = $this->buildForRoot( $root, + $walkerClass, $this->walkerContext, $maxLevel, $this->cacheItemPool @@ -70,4 +78,62 @@ 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/Settings.php b/src/Bag/Settings.php index 128d9b89..4586ae26 100644 --- a/src/Bag/Settings.php +++ b/src/Bag/Settings.php @@ -9,19 +9,19 @@ 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; + private Stopwatch $stopwatch; - /** - * @param ManagerRegistry $managerRegistry - */ - public function __construct(ManagerRegistry $managerRegistry) + public function __construct(ManagerRegistry $managerRegistry, Stopwatch $stopwatch) { parent::__construct(); $this->managerRegistry = $managerRegistry; + $this->stopwatch = $stopwatch; } /** @@ -37,6 +37,7 @@ public function getRepository(): SettingRepository protected function populateParameters(): void { + $this->stopwatch->start('settings'); try { $settings = $this->getRepository()->findAll(); $this->parameters = []; @@ -48,6 +49,7 @@ protected function populateParameters(): void $this->parameters = []; } $this->ready = true; + $this->stopwatch->stop('settings'); } /** diff --git a/src/Console/AppInstallCommand.php b/src/Console/AppInstallCommand.php new file mode 100644 index 00000000..11354079 --- /dev/null +++ b/src/Console/AppInstallCommand.php @@ -0,0 +1,191 @@ +projectDir = $projectDir; + $this->themeGenerator = $themeGenerator; + $this->nodeTypesImporter = $nodeTypesImporter; + $this->tagsImporter = $tagsImporter; + $this->settingsImporter = $settingsImporter; + $this->rolesImporter = $rolesImporter; + $this->groupsImporter = $groupsImporter; + $this->attributeImporter = $attributeImporter; + $this->managerRegistry = $managerRegistry; + } + + protected function configure(): void + { + $this->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 new file mode 100644 index 00000000..9aef6a7a --- /dev/null +++ b/src/Console/AppMigrateCommand.php @@ -0,0 +1,129 @@ +projectDir = $projectDir; + $this->schemaUpdater = $schemaUpdater; + } + + protected function configure(): void + { + $this->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 deleted file mode 100644 index 758ac145..00000000 --- a/src/Console/CleanLoginAttemptCommand.php +++ /dev/null @@ -1,43 +0,0 @@ -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/FilesImportCommand.php b/src/Console/FilesImportCommand.php index 283cadc3..55c0b07f 100644 --- a/src/Console/FilesImportCommand.php +++ b/src/Console/FilesImportCommand.php @@ -57,6 +57,9 @@ 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/InstallCommand.php b/src/Console/InstallCommand.php index 0e21d59e..79291b84 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -70,26 +70,48 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io->askQuestion($question) ) { $fixturesRoot = dirname(__DIR__) . '/../config'; - $data = Yaml::parse(file_get_contents($fixturesRoot . "/fixtures.yaml")); + $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); if (isset($data["importFiles"]['roles'])) { foreach ($data["importFiles"]['roles'] as $filename) { $filePath = $fixturesRoot . "/" . $filename; - $this->rolesImporter->import(file_get_contents($filePath)); + $fileContents = file_get_contents($filePath); + if (false === $fileContents) { + $io->error('No file found in ' . $filePath); + return 1; + } + $this->rolesImporter->import($fileContents); $io->success('Theme file “' . $filePath . '” has been imported.'); } } if (isset($data["importFiles"]['groups'])) { foreach ($data["importFiles"]['groups'] as $filename) { $filePath = $fixturesRoot . "/" . $filename; - $this->groupsImporter->import(file_get_contents($filePath)); + $fileContents = file_get_contents($filePath); + if (false === $fileContents) { + $io->error('No file found in ' . $filePath); + return 1; + } + $this->groupsImporter->import($fileContents); $io->success('Theme file “' . $filePath . '” has been imported.'); } } if (isset($data["importFiles"]['settings'])) { foreach ($data["importFiles"]['settings'] as $filename) { $filePath = $fixturesRoot . "/" . $filename; - $this->settingsImporter->import(file_get_contents($filePath)); + $fileContents = file_get_contents($filePath); + if (false === $fileContents) { + $io->error('No file found in ' . $filePath); + return 1; + } + $this->settingsImporter->import($fileContents); $io->success('Theme files “' . $filePath . '” has been imported.'); } } diff --git a/src/Console/LogsCleanupCommand.php b/src/Console/LogsCleanupCommand.php index 6e00af9a..d9b94f3c 100644 --- a/src/Console/LogsCleanupCommand.php +++ b/src/Console/LogsCleanupCommand.php @@ -6,9 +6,10 @@ use Doctrine\ORM\NoResultException; use Doctrine\Persistence\ManagerRegistry; -use RZ\Roadiz\CoreBundle\Entity\Log; +use RZ\Roadiz\CoreBundle\Logger\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; @@ -47,7 +48,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (\is_string($input->getOption('since'))) { $since = '-' . $input->getOption('since'); } - $now->add(\DateInterval::createFromDateString($since)); + $interval = \DateInterval::createFromDateString($since); + if (false === $interval) { + throw new InvalidArgumentException('Invalid since option format.'); + } + $now->add($interval); $io = new SymfonyStyle($input, $output); /** @var LogRepository $logRepository */ diff --git a/src/Console/NodeTypesCreationCommand.php b/src/Console/NodeTypesCreationCommand.php index 81c11ba2..812e9d0d 100644 --- a/src/Console/NodeTypesCreationCommand.php +++ b/src/Console/NodeTypesCreationCommand.php @@ -108,7 +108,8 @@ private function executeCreation(InputInterface $input, OutputInterface $output) protected function addNodeTypeField(NodeType $nodeType, int|float|string $position, SymfonyStyle $io): void { $field = new NodeTypeField(); - $field->setPosition((float) $position); + $position = floatval($position); + $field->setPosition($position); $questionfName = new Question('[Field ' . $position . '] Enter field name', 'content'); $fName = $io->askQuestion($questionfName); diff --git a/src/Console/NodesDetailsCommand.php b/src/Console/NodesDetailsCommand.php index d5095807..8b373a43 100644 --- a/src/Console/NodesDetailsCommand.php +++ b/src/Console/NodesDetailsCommand.php @@ -67,7 +67,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (is_array($data)) { $data = implode(', ', $data); } - if ($data instanceof \DateTime) { + if ($data instanceof \DateTimeInterface) { $data = $data->format('c'); } if ($data instanceof \stdClass) { diff --git a/src/Console/PurgeLoginAttemptCommand.php b/src/Console/PurgeLoginAttemptCommand.php deleted file mode 100644 index 95985673..00000000 --- a/src/Console/PurgeLoginAttemptCommand.php +++ /dev/null @@ -1,54 +0,0 @@ -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/UsersCommand.php b/src/Console/UsersCommand.php index f86c7100..f61d19ef 100644 --- a/src/Console/UsersCommand.php +++ b/src/Console/UsersCommand.php @@ -21,12 +21,9 @@ class UsersCommand extends Command { protected ManagerRegistry $managerRegistry; - /** - * @param ManagerRegistry $managerRegistry - */ - public function __construct(ManagerRegistry $managerRegistry) + public function __construct(ManagerRegistry $managerRegistry, string $name = null) { - parent::__construct(); + parent::__construct($name); $this->managerRegistry = $managerRegistry; } @@ -68,7 +65,9 @@ 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 @@ -120,10 +119,9 @@ 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) + public function getRole(string $roleName = Role::ROLE_SUPERADMIN): Role { $role = $this->managerRegistry ->getRepository(Role::class) diff --git a/src/Console/UsersCreationCommand.php b/src/Console/UsersCreationCommand.php index cd97c713..bb33ff51 100644 --- a/src/Console/UsersCreationCommand.php +++ b/src/Console/UsersCreationCommand.php @@ -38,29 +38,32 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { + $io = new SymfonyStyle($input, $output); $name = $input->getArgument('username'); - 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.'); - } + if (!\is_string($name) || empty($name)) { + throw new \InvalidArgumentException('Username argument is required.'); } - return 0; + + /** @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; + } + + $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); } /** diff --git a/src/Console/UsersDeleteCommand.php b/src/Console/UsersDeleteCommand.php index 6fb754da..9e98959f 100644 --- a/src/Console/UsersDeleteCommand.php +++ b/src/Console/UsersDeleteCommand.php @@ -31,33 +31,24 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $name = $input->getArgument('username'); + $user = $this->getUserForInput($input); - 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.'); - } + $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; } - return 0; } } diff --git a/src/Console/UsersDisableCommand.php b/src/Console/UsersDisableCommand.php index a5d3c5f7..11cf7db2 100644 --- a/src/Console/UsersDisableCommand.php +++ b/src/Console/UsersDisableCommand.php @@ -31,33 +31,24 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $name = $input->getArgument('username'); + $user = $this->getUserForInput($input); - 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.'); - } + $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; } - return 0; } } diff --git a/src/Console/UsersEnableCommand.php b/src/Console/UsersEnableCommand.php index f166e74b..f277a779 100644 --- a/src/Console/UsersEnableCommand.php +++ b/src/Console/UsersEnableCommand.php @@ -31,32 +31,24 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $name = $input->getArgument('username'); + $user = $this->getUserForInput($input); - 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.'); - } + $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; } - return 0; } } diff --git a/src/Console/UsersPasswordCommand.php b/src/Console/UsersPasswordCommand.php index 6472dfd7..ccab60ae 100644 --- a/src/Console/UsersPasswordCommand.php +++ b/src/Console/UsersPasswordCommand.php @@ -45,33 +45,24 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $name = $input->getArgument('username'); + $user = $this->getUserForInput($input); - 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.'); - } + $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; } - return 0; } } diff --git a/src/Console/UsersRolesCommand.php b/src/Console/UsersRolesCommand.php index 40b725b9..0c9fc369 100644 --- a/src/Console/UsersRolesCommand.php +++ b/src/Console/UsersRolesCommand.php @@ -58,53 +58,43 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - $name = $input->getArgument('username'); + $user = $this->getUserForInput($input); - if ($name) { - /** @var User|null $user */ - $user = $this->managerRegistry - ->getRepository(User::class) - ->findOneBy(['username' => $name]); + if ($input->getOption('add')) { + $roles = $this->managerRegistry + ->getRepository(Role::class) + ->getAllRoleName(); - if (null !== $user) { - if ($input->getOption('add')) { - $roles = $this->managerRegistry - ->getRepository(Role::class) - ->getAllRoleName(); - - $question = new Question( - 'Enter the role name to add' - ); - $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); + 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.'); - } - } while ($role != ""); + $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.'); } - } else { - throw new \InvalidArgumentException('User “' . $name . '” does not exist.'); - } + } while ($role != ""); } + return 0; } } diff --git a/src/Controller/CustomFormController.php b/src/Controller/CustomFormController.php index 78957bce..c22ac946 100644 --- a/src/Controller/CustomFormController.php +++ b/src/Controller/CustomFormController.php @@ -7,20 +7,17 @@ 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; @@ -32,19 +29,14 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException; -use Symfony\Component\Mailer\Exception\TransportExceptionInterface; -use Symfony\Component\Mime\Address; +use Symfony\Component\Messenger\MessageBusInterface; 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; @@ -54,11 +46,10 @@ final class CustomFormController extends AbstractController private FormErrorSerializerInterface $formErrorSerializer; private ManagerRegistry $registry; private RateLimiterFactory $customFormLimiter; - private FilesystemOperator $documentsStorage; private PreviewResolverInterface $previewResolver; + private MessageBusInterface $messageBus; public function __construct( - EmailManager $emailManager, Settings $settingsBag, LoggerInterface $logger, TranslatorInterface $translator, @@ -68,10 +59,9 @@ public function __construct( FormErrorSerializerInterface $formErrorSerializer, ManagerRegistry $registry, RateLimiterFactory $customFormLimiter, - FilesystemOperator $documentsStorage, - PreviewResolverInterface $previewResolver + PreviewResolverInterface $previewResolver, + MessageBusInterface $messageBus, ) { - $this->emailManager = $emailManager; $this->settingsBag = $settingsBag; $this->logger = $logger; $this->translator = $translator; @@ -81,8 +71,8 @@ public function __construct( $this->formErrorSerializer = $formErrorSerializer; $this->registry = $registry; $this->customFormLimiter = $customFormLimiter; - $this->documentsStorage = $documentsStorage; $this->previewResolver = $previewResolver; + $this->messageBus = $messageBus; } protected function validateCustomForm(?CustomForm $customForm): void @@ -222,9 +212,6 @@ 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 { @@ -243,7 +230,7 @@ public function addAction(Request $request, int $customFormId): Response ) ); - if ($mixed instanceof RedirectResponse) { + if ($mixed instanceof Response) { $mixed->prepare($request); return $mixed->send(); } else { @@ -268,60 +255,7 @@ public function sentAction(Request $request, int $customFormId): Response } /** - * 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. + * Prepare and handle a CustomForm Form then send a confirmation email. * * * This method will return an assignation **array** if form is not validated. * * customForm @@ -336,7 +270,7 @@ public function sendAnswer( * @param string|null $emailSender * @param bool $prefix * @return array|Response - * @throws SyntaxError|RuntimeError|LoaderError|FilesystemException + * @throws FilesystemException */ public function prepareAndHandleCustomFormAssignation( Request $request, @@ -364,73 +298,35 @@ public function prepareAndHandleCustomFormAssignation( */ $answer = $helper->parseAnswerFormData($form, null, $request->getClientIp()); - /* - * 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) - ); - - $assignation['title'] = $this->translator->trans( - 'new.answer.form.%site%', - ['%site%' => $customFormsEntity->getDisplayName()] - ); + $answerId = $answer->getId(); + if (!is_int($answerId)) { + throw new \RuntimeException('Answer ID is null'); + } - if (null !== $emailSender && false !== filter_var($emailSender, FILTER_VALIDATE_EMAIL)) { - $assignation['mailContact'] = $emailSender; - } else { - $assignation['mailContact'] = $this->settingsBag->get('email_sender'); + if (null === $emailSender || false === filter_var($emailSender, FILTER_VALIDATE_EMAIL)) { + $emailSender = $this->settingsBag->get('email_sender'); } - /* - * 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()] - ); - - $session = $request->getSession(); - if ($session instanceof Session) { - $session->getFlashBag()->add('confirm', $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() - ]); + $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()] + ); + + $session = $request->getSession(); + if ($session instanceof Session) { + $session->getFlashBag()->add('confirm', $msg); } + $this->logger->info($msg); return $response; } catch (EntityAlreadyExistsException $e) { diff --git a/src/Controller/RedirectionController.php b/src/Controller/RedirectionController.php index bb478d9c..bffc5dc4 100644 --- a/src/Controller/RedirectionController.php +++ b/src/Controller/RedirectionController.php @@ -16,9 +16,6 @@ final class RedirectionController { private UrlGeneratorInterface $urlGenerator; - /** - * @param UrlGeneratorInterface $urlGenerator - */ public function __construct(UrlGeneratorInterface $urlGenerator) { $this->urlGenerator = $urlGenerator; diff --git a/src/CustomForm/CustomFormHelper.php b/src/CustomForm/CustomFormHelper.php index c333572c..ce8a28de 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 \DateTime) { + if ($rawValue instanceof \DateTimeInterface) { 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 new file mode 100644 index 00000000..8f092728 --- /dev/null +++ b/src/CustomForm/Message/CustomFormAnswerNotifyMessage.php @@ -0,0 +1,49 @@ +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 new file mode 100644 index 00000000..60cc59d7 --- /dev/null +++ b/src/CustomForm/Message/Handler/CustomFormAnswerNotifyMessageHandler.php @@ -0,0 +1,124 @@ +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 b6deacab..ee69f857 100644 --- a/src/DependencyInjection/Compiler/MediaFinderCompilerPass.php +++ b/src/DependencyInjection/Compiler/MediaFinderCompilerPass.php @@ -15,6 +15,7 @@ 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 d771a7ca..037afe46 100644 --- a/src/DependencyInjection/Compiler/NodesSourcesEntitiesPathCompilerPass.php +++ b/src/DependencyInjection/Compiler/NodesSourcesEntitiesPathCompilerPass.php @@ -15,6 +15,9 @@ 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 new file mode 100644 index 00000000..b8c0d188 --- /dev/null +++ b/src/DependencyInjection/Compiler/TreeWalkerDefinitionFactoryCompilerPass.php @@ -0,0 +1,43 @@ +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 5529c8d4..4240e7ab 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -37,6 +37,10 @@ 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() diff --git a/src/DependencyInjection/RoadizCoreExtension.php b/src/DependencyInjection/RoadizCoreExtension.php index fc150208..3bf9a7ac 100644 --- a/src/DependencyInjection/RoadizCoreExtension.php +++ b/src/DependencyInjection/RoadizCoreExtension.php @@ -21,6 +21,7 @@ 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; @@ -74,6 +75,7 @@ public function load(array $configs, ContainerBuilder $container): void $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.preview_required_role_name', $config['previewRequiredRoleName']); /* * Assets config @@ -85,9 +87,11 @@ 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', - $container->getParameter('kernel.project_dir') . DIRECTORY_SEPARATOR . trim($config['documentsLibDir'], "/ \t\n\r\0\x0B") + $projectDir . DIRECTORY_SEPARATOR . trim($config['documentsLibDir'], "/ \t\n\r\0\x0B") ); /* * Media config @@ -258,6 +262,7 @@ 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()) @@ -269,6 +274,7 @@ 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)]) @@ -286,21 +292,23 @@ 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($container->getParameter('roadiz_core.markdown_config_default'), [ + array_merge($defaultConfig, [ 'html_input' => 'allow' ]) ); $container->setParameter( 'roadiz_core.markdown_config_text_extra_converter', - array_merge($container->getParameter('roadiz_core.markdown_config_default'), [ + array_merge($defaultConfig, [ 'html_input' => 'allow' ]) ); $container->setParameter( 'roadiz_core.markdown_config_line_converter', - array_merge($container->getParameter('roadiz_core.markdown_config_default'), [ + array_merge($defaultConfig, [ 'html_input' => 'escape' ]) ); diff --git a/src/Doctrine/Event/FilterQueryBuilderEvent.php b/src/Doctrine/Event/FilterQueryBuilderEvent.php index 983b74a8..202811ca 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 string $entityClass + * @param class-string $entityClass */ public function __construct(QueryBuilder $queryBuilder, string $entityClass) { @@ -51,10 +51,10 @@ public function setQueryBuilder(QueryBuilder $queryBuilder) /** - * @param string $entityClass + * @param class-string $entityClass * @return bool */ - public function supports($entityClass): bool + public function supports(string $entityClass): bool { return $this->entityClass === $entityClass; } diff --git a/src/Doctrine/Event/QueryEvent.php b/src/Doctrine/Event/QueryEvent.php index b164dcbd..dc85f161 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 string $entityClass + * @param class-string $entityClass */ public function __construct(Query $query, string $entityClass) { @@ -35,7 +35,7 @@ public function getQuery(): Query } /** - * @return string + * @return class-string */ public function getEntityClass(): string { diff --git a/src/Doctrine/Event/QueryNodesSourcesEvent.php b/src/Doctrine/Event/QueryNodesSourcesEvent.php index f4dc8a38..03d2c385 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 string $actualEntityName + * @param class-string $actualEntityName */ public function __construct(Query $query, string $actualEntityName) { @@ -25,7 +25,7 @@ public function __construct(Query $query, string $actualEntityName) } /** - * @return string + * @return class-string */ public function getActualEntityName(): string { diff --git a/src/Doctrine/EventSubscriber/AttributeValueLifeCycleSubscriber.php b/src/Doctrine/EventSubscriber/AttributeValueLifeCycleSubscriber.php index 9e0b1e5e..4e7ada54 100644 --- a/src/Doctrine/EventSubscriber/AttributeValueLifeCycleSubscriber.php +++ b/src/Doctrine/EventSubscriber/AttributeValueLifeCycleSubscriber.php @@ -32,6 +32,13 @@ 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 index 96e5a485..acc18d84 100644 --- a/src/Doctrine/EventSubscriber/SettingLifeCycleSubscriber.php +++ b/src/Doctrine/EventSubscriber/SettingLifeCycleSubscriber.php @@ -99,6 +99,7 @@ public function postLoad(LifecycleEventArgs $event): void [ 'exception_message' => $exception->getMessage(), 'trace' => $exception->getTraceAsString(), + 'entity' => $setting ] ); } catch (InvalidMessage $exception) { @@ -107,6 +108,7 @@ public function postLoad(LifecycleEventArgs $event): void [ 'exception_message' => $exception->getMessage(), 'trace' => $exception->getTraceAsString(), + 'entity' => $setting ] ); } diff --git a/src/Doctrine/EventSubscriber/UserLifeCycleSubscriber.php b/src/Doctrine/EventSubscriber/UserLifeCycleSubscriber.php index eb342cdb..b7c59b99 100644 --- a/src/Doctrine/EventSubscriber/UserLifeCycleSubscriber.php +++ b/src/Doctrine/EventSubscriber/UserLifeCycleSubscriber.php @@ -168,8 +168,7 @@ public function postPersist(LifecycleEventArgs $event): void /** * @param LifecycleEventArgs $event - * - * @throws \Exception + * @throws \Throwable */ public function prePersist(LifecycleEventArgs $event): void { @@ -189,8 +188,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' diff --git a/src/Doctrine/ORM/SimpleQueryBuilder.php b/src/Doctrine/ORM/SimpleQueryBuilder.php index eed46053..15e3204b 100644 --- a/src/Doctrine/ORM/SimpleQueryBuilder.php +++ b/src/Doctrine/ORM/SimpleQueryBuilder.php @@ -133,7 +133,7 @@ public function buildExpressionWithoutBinding($value, string $prefix, string $ke if (null === $value) { return $this->queryBuilder->expr()->isNull($prefix . $key); } - + // @phpstan-ignore-next-line throw new \InvalidArgumentException('Value is not supported for expression.'); } @@ -187,7 +187,7 @@ public function bindValue(string $key, $value): QueryBuilder if (null === $value) { return $this->queryBuilder; } - + // @phpstan-ignore-next-line throw new \InvalidArgumentException('Value is not supported for binding.'); } diff --git a/src/Doctrine/SchemaUpdater.php b/src/Doctrine/SchemaUpdater.php index 7a7ea7a2..a176b299 100644 --- a/src/Doctrine/SchemaUpdater.php +++ b/src/Doctrine/SchemaUpdater.php @@ -98,23 +98,15 @@ public function updateSchema(): void */ public function updateNodeTypesSchema(): void { - /* - * Execute pending application migrations - */ - $this->updateSchema(); - - /* - * Update schema with new node-types - * without creating any migration - */ + $this->clearMetadata(); $process = $this->runCommand( - 'doctrine:schema:update', - '--dump-sql --force', + 'doctrine:migrations:diff', + '--namespace=DoctrineMigrations --quiet --allow-empty-diff', ); $process->run(); - if ($process->wait() === 0) { - $this->logger->info('DB schema has been updated.'); + $this->logger->info('New migration has been generated.'); + $this->updateSchema(); } 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 6abfdccf..8dfb1350 100644 --- a/src/Document/EventSubscriber/DocumentMessageDispatchSubscriber.php +++ b/src/Document/EventSubscriber/DocumentMessageDispatchSubscriber.php @@ -49,18 +49,19 @@ public function onFilterDocumentEvent(FilterDocumentEvent $event): void $document = $event->getDocument(); if ( $document instanceof Document && - null !== $document->getId() && + \is_numeric($document->getId()) && $document->isLocal() && null !== $document->getRelativePath() ) { - $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()))); + $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))); } } } diff --git a/src/Document/MessageHandler/DocumentAudioVideoMessageHandler.php b/src/Document/MessageHandler/DocumentAudioVideoMessageHandler.php index a9393dc0..92830140 100644 --- a/src/Document/MessageHandler/DocumentAudioVideoMessageHandler.php +++ b/src/Document/MessageHandler/DocumentAudioVideoMessageHandler.php @@ -67,12 +67,18 @@ 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); @@ -114,6 +120,9 @@ 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 1a8340e3..292de562 100644 --- a/src/Document/MessageHandler/DocumentPdfMessageHandler.php +++ b/src/Document/MessageHandler/DocumentPdfMessageHandler.php @@ -51,12 +51,18 @@ 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); @@ -75,6 +81,9 @@ 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/Attribute.php b/src/Entity/Attribute.php index 6fa2080e..04657460 100644 --- a/src/Entity/Attribute.php +++ b/src/Entity/Attribute.php @@ -4,16 +4,21 @@ namespace RZ\Roadiz\CoreBundle\Entity; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Metadata\ApiFilter; 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\AttributeInterface; +use RZ\Roadiz\CoreBundle\Model\AttributeTrait; +use RZ\Roadiz\CoreBundle\Model\RealmInterface; use RZ\Roadiz\CoreBundle\Repository\AttributeRepository; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation as SymfonySerializer; -use RZ\Roadiz\CoreBundle\Model\AttributeInterface; -use RZ\Roadiz\CoreBundle\Model\AttributeTrait; -use RZ\Roadiz\Core\AbstractEntities\AbstractEntity; +use Symfony\Component\Validator\Constraints\NotNull; +use Symfony\Component\Validator\Constraints\Range; /** * @package RZ\Roadiz\CoreBundle\Entity @@ -24,6 +29,8 @@ ORM\Index(columns: ["code"]), ORM\Index(columns: ["type"]), ORM\Index(columns: ["searchable"]), + ORM\Index(columns: ["weight"]), + ORM\Index(columns: ["color"]), ORM\Index(columns: ["group_id"]), ORM\HasLifecycleCallbacks, UniqueEntity(fields: ["code"]), @@ -49,6 +56,32 @@ class Attribute extends AbstractEntity implements AttributeInterface ] protected Collection $attributeDocuments; + #[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 $defaultRealm = null; + + /** + * @var int Absolute weight for sorting attributes in filtered lists. + */ + #[ + ORM\Column(type: "integer", nullable: false, options: ["default" => 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(); @@ -76,6 +109,28 @@ 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/AttributeValue.php b/src/Entity/AttributeValue.php index 59df1d87..641fd2fa 100644 --- a/src/Entity/AttributeValue.php +++ b/src/Entity/AttributeValue.php @@ -4,9 +4,9 @@ namespace RZ\Roadiz\CoreBundle\Entity; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter as BaseFilter; -use ApiPlatform\Core\Serializer\Filter\PropertyFilter; +use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; 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,6 +15,7 @@ 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; @@ -22,8 +23,13 @@ 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(PropertyFilter::class), + ApiFilter(BaseFilter\OrderFilter::class, properties: [ + "position", + ]), ] class AttributeValue extends AbstractPositioned implements AttributeValueInterface { @@ -41,7 +47,9 @@ class AttributeValue extends AbstractPositioned implements AttributeValueInterfa ApiFilter(BaseFilter\SearchFilter::class, properties: [ "node" => "exact", "node.id" => "exact", - "node.nodeName" => "exact" + "node.nodeName" => "exact", + "node.nodeType" => "exact", + "node.nodeType.name" => "exact" ]), ApiFilter(BaseFilter\BooleanFilter::class, properties: [ "node.visible" @@ -49,6 +57,18 @@ class AttributeValue extends AbstractPositioned implements AttributeValueInterfa ] protected ?Node $node = null; + #[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; + public function __construct() { $this->attributeValueTranslations = new ArrayCollection(); @@ -78,7 +98,7 @@ public function getAttributable(): ?AttributableInterface */ public function setAttributable(?AttributableInterface $attributable) { - if (null === $attributable || $attributable instanceof Node) { + if ($attributable instanceof Node) { $this->node = $attributable; return $this; } @@ -105,6 +125,17 @@ public function setNode(?Node $node): AttributeValue return $this; } + public function getRealm(): ?RealmInterface + { + return $this->realm; + } + + public function setRealm(?RealmInterface $realm): AttributeValue + { + $this->realm = $realm; + return $this; + } + /** * After clone method. * @@ -115,14 +146,12 @@ public function __clone() if ($this->id) { $this->id = null; $attributeValueTranslations = $this->getAttributeValueTranslations(); - if ($attributeValueTranslations !== null) { - $this->attributeValueTranslations = new ArrayCollection(); - /** @var AttributeValueTranslationInterface $attributeValueTranslation */ - foreach ($attributeValueTranslations as $attributeValueTranslation) { - $cloneAttributeValueTranslation = clone $attributeValueTranslation; - $cloneAttributeValueTranslation->setAttributeValue($this); - $this->attributeValueTranslations->add($cloneAttributeValueTranslation); - } + $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 3194e69b..61befe69 100644 --- a/src/Entity/CustomForm.php +++ b/src/Entity/CustomForm.php @@ -26,19 +26,22 @@ ORM\Entity(repositoryClass: CustomFormRepository::class), ORM\Table(name: "custom_forms"), ORM\HasLifecycleCallbacks, - UniqueEntity(fields: ["name"]) + UniqueEntity(fields: ["name"]), + ORM\Index(columns: ["created_at"], name: "custom_form_created_at"), + ORM\Index(columns: ["updated_at"], name: "custom_form_updated_at"), ] class CustomForm extends AbstractDateTimed { #[ - ORM\Column(name: "color", type: "string", unique: false, nullable: true), + ORM\Column(name: "color", type: "string", length: 7, unique: false, nullable: true), Serializer\Groups(["custom_form", "nodes_sources"]), + Assert\Length(max: 7), SymfonySerializer\Ignore() ] protected ?string $color = '#000000'; #[ - ORM\Column(type: "string", unique: true), + ORM\Column(type: "string", length: 250, unique: true), Serializer\Groups(["custom_form", "nodes_sources"]), SymfonySerializer\Groups(["custom_form", "nodes_sources"]), Assert\NotNull(), @@ -49,7 +52,7 @@ class CustomForm extends AbstractDateTimed private string $name = 'Untitled'; #[ - ORM\Column(name: "display_name", type: "string"), + ORM\Column(name: "display_name", type: "string", length: 250), Serializer\Groups(["custom_form", "nodes_sources"]), SymfonySerializer\Groups(["custom_form", "nodes_sources"]), Assert\NotNull(), @@ -79,6 +82,7 @@ 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; diff --git a/src/Entity/CustomFormAnswer.php b/src/Entity/CustomFormAnswer.php index 81b05aed..40121188 100644 --- a/src/Entity/CustomFormAnswer.php +++ b/src/Entity/CustomFormAnswer.php @@ -11,6 +11,7 @@ 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), @@ -22,9 +23,10 @@ class CustomFormAnswer extends AbstractEntity { #[ - ORM\Column(name: "ip", type: "string", nullable: false), + ORM\Column(name: "ip", type: "string", length: 46, nullable: false), Serializer\Groups(["custom_form_answer"]), - SymfonySerializer\Groups(["custom_form_answer"]) + SymfonySerializer\Groups(["custom_form_answer"]), + Assert\Length(max: 46) ] private string $ip = ''; diff --git a/src/Entity/Document.php b/src/Entity/Document.php index 7b7d4e46..073815c5 100644 --- a/src/Entity/Document.php +++ b/src/Entity/Document.php @@ -4,9 +4,9 @@ namespace RZ\Roadiz\CoreBundle\Entity; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter as BaseFilter; -use ApiPlatform\Core\Serializer\Filter\PropertyFilter; +use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Serializer\Filter\PropertyFilter; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Criteria; @@ -18,7 +18,6 @@ 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,6 +26,7 @@ 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. @@ -69,7 +69,7 @@ ]), ApiFilter(CopyrightValidFilter::class) ] -class Document extends AbstractDateTimed implements AdvancedDocumentInterface, HasThumbnailInterface, TimeableInterface, DisplayableInterface, FileHashInterface +class Document extends AbstractDateTimed implements AdvancedDocumentInterface, HasThumbnailInterface, TimeableInterface, FileHashInterface { use DocumentTrait; @@ -89,6 +89,37 @@ class Document extends AbstractDateTimed implements AdvancedDocumentInterface, H #[Serializer\Groups(['document_copyright'])] 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'], @@ -108,30 +139,34 @@ class Document extends AbstractDateTimed implements AdvancedDocumentInterface, H ] protected bool $raw = false; - #[ORM\Column(name: 'embedId', type: 'string', unique: false, nullable: true)] + #[ORM\Column(name: 'embedId', type: 'string', length: 250, 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: 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', unique: false, nullable: true)] + #[ORM\Column(name: 'embedPlatform', type: 'string', length: 100, 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)] protected ?string $embedPlatform = null; /** * @var Collection @@ -174,7 +209,6 @@ class Document extends AbstractDateTimed implements AdvancedDocumentInterface, H #[ORM\OneToMany( mappedBy: 'document', targetEntity: DocumentTranslation::class, - fetch: 'EAGER', orphanRemoval: true )] #[SymfonySerializer\Ignore] @@ -185,20 +219,22 @@ class Document extends AbstractDateTimed implements AdvancedDocumentInterface, H * @var string|null */ #[ApiFilter(BaseFilter\SearchFilter::class, strategy: "partial")] - #[ORM\Column(name: 'filename', type: 'string', nullable: true)] + #[ORM\Column(name: 'filename', type: 'string', length: 250, 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', nullable: true)] + #[ORM\Column(name: 'mime_type', type: 'string', length: 255, 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)] private ?string $mimeType = null; /** * @var Collection @@ -210,8 +246,9 @@ class Document extends AbstractDateTimed implements AdvancedDocumentInterface, H /** * @var string */ - #[ORM\Column(type: 'string')] + #[ORM\Column(type: 'string', length: 100)] #[SymfonySerializer\Ignore] + #[Assert\Length(max: 100)] #[Serializer\Groups(['document', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Type('string')] private string $folder = ''; @@ -254,6 +291,7 @@ 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)] private ?string $imageAverageColor = null; /** * @var int|null The filesize in bytes. @@ -804,4 +842,15 @@ 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 0dd1f9db..858d529a 100644 --- a/src/Entity/DocumentTranslation.php +++ b/src/Entity/DocumentTranslation.php @@ -13,6 +13,7 @@ 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), @@ -22,9 +23,10 @@ ] class DocumentTranslation extends AbstractEntity implements Loggable { - #[ORM\Column(type: 'string', nullable: true)] + #[ORM\Column(type: 'string', length: 250, 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; diff --git a/src/Entity/Folder.php b/src/Entity/Folder.php index 549c6e65..abb50bd2 100644 --- a/src/Entity/Folder.php +++ b/src/Entity/Folder.php @@ -4,8 +4,8 @@ namespace RZ\Roadiz\CoreBundle\Entity; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter as BaseFilter; -use ApiPlatform\Core\Serializer\Filter\PropertyFilter; +use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; +use ApiPlatform\Serializer\Filter\PropertyFilter; use ApiPlatform\Metadata\ApiFilter; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; @@ -101,11 +101,12 @@ 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', unique: true, nullable: false)] + #[ORM\Column(name: 'folder_name', type: 'string', length: 250, 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 eb7577a6..b827d27e 100644 --- a/src/Entity/FolderTranslation.php +++ b/src/Entity/FolderTranslation.php @@ -26,7 +26,7 @@ ] class FolderTranslation extends AbstractEntity { - #[ORM\Column(type: 'string')] + #[ORM\Column(type: 'string', length: 250)] #[SymfonySerializer\Groups(['folder', 'document'])] #[Serializer\Groups(['folder', 'document'])] #[Assert\Length(max: 250)] diff --git a/src/Entity/Group.php b/src/Entity/Group.php index 146cefdf..5bb7efd0 100644 --- a/src/Entity/Group.php +++ b/src/Entity/Group.php @@ -24,7 +24,7 @@ ] class Group extends AbstractEntity { - #[ORM\Column(type: 'string', unique: true)] + #[ORM\Column(type: 'string', length: 250, unique: true)] #[SymfonySerializer\Groups(['user', 'role', 'group'])] #[Serializer\Groups(['user', 'role', 'group'])] #[Assert\NotBlank] diff --git a/src/Entity/LoginAttempt.php b/src/Entity/LoginAttempt.php deleted file mode 100644 index db44dc88..00000000 --- a/src/Entity/LoginAttempt.php +++ /dev/null @@ -1,106 +0,0 @@ -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 02875828..d47d4fd5 100644 --- a/src/Entity/Node.php +++ b/src/Entity/Node.php @@ -4,8 +4,8 @@ namespace RZ\Roadiz\CoreBundle\Entity; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter as BaseFilter; -use ApiPlatform\Core\Serializer\Filter\PropertyFilter; +use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; +use ApiPlatform\Serializer\Filter\PropertyFilter; use ApiPlatform\Metadata\ApiFilter; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; @@ -56,7 +56,12 @@ ORM\Index(columns: ["home"]), ORM\HasLifecycleCallbacks, Gedmo\Loggable(logEntryClass: UserLogEntry::class), - UniqueEntity(fields: ["nodeName"]), + // Need to override repository method to see all nodes + UniqueEntity( + fields: 'nodeName', + message: 'nodeName.alreadyExists', + repositoryMethod: 'findOneWithoutSecurity' + ), ApiFilter(PropertyFilter::class) ] class Node extends AbstractDateTimedPositioned implements LeafInterface, AttributableInterface, Loggable @@ -80,7 +85,7 @@ class Node extends AbstractDateTimedPositioned implements LeafInterface, Attribu 'publishedAt' => 'ns.publishedAt', ]; - #[ORM\Column(name: 'node_name', type: 'string', unique: true)] + #[ORM\Column(name: 'node_name', type: 'string', length: 255, 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")] @@ -146,15 +151,17 @@ class Node extends AbstractDateTimedPositioned implements LeafInterface, Attribu #[Gedmo\Versioned] private bool $sterile = false; - #[ORM\Column(name: 'children_order', type: 'string')] - #[SymfonySerializer\Groups(['node'])] - #[Serializer\Groups(['node'])] + #[ORM\Column(name: 'children_order', type: 'string', length: 50)] + #[SymfonySerializer\Groups(['node', 'node_listing'])] + #[Serializer\Groups(['node', 'node_listing'])] + #[Assert\Length(max: 50)] #[Gedmo\Versioned] private string $childrenOrder = 'position'; #[ORM\Column(name: 'children_order_direction', type: 'string', length: 4)] - #[SymfonySerializer\Groups(['node'])] - #[Serializer\Groups(['node'])] + #[SymfonySerializer\Groups(['node', 'node_listing'])] + #[Serializer\Groups(['node', 'node_listing'])] + #[Assert\Length(max: 4)] #[Gedmo\Versioned] private string $childrenOrderDirection = 'ASC'; @@ -929,11 +936,19 @@ public function setVisible(bool $visible): Node #[SymfonySerializer\Ignore] public function getOneLineSourceSummary(): string { - $text = "Source " . $this->getNodeSources()->first()->getId() . PHP_EOL; + $text = "Source " . + ( + $this->getNodeSources()->first() ? + $this->getNodeSources()->first()->getId() : + '' + ) . + PHP_EOL; foreach ($this->getNodeType()->getFields() as $field) { $getterName = $field->getGetterName(); - $text .= '[' . $field->getLabel() . ']: ' . $this->getNodeSources()->first()->$getterName() . PHP_EOL; + $text .= '[' . $field->getLabel() . ']: ' . + ($this->getNodeSources()->first() ? $this->getNodeSources()->first()->$getterName() : '') . + PHP_EOL; } return $text; @@ -984,9 +999,14 @@ public function __clone() // Get a random string after node-name. // This is for safety reasons // NodeDuplicator service will override it - $namePrefix = $this->getNodeSources()->first()->getTitle() != "" ? - $this->getNodeSources()->first()->getTitle() : - $this->nodeName; + $nodeSource = $this->getNodeSources()->first(); + if ($nodeSource !== false) { + $namePrefix = $nodeSource->getTitle() != "" ? + $nodeSource->getTitle() : + $this->nodeName; + } else { + $namePrefix = $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 1f272b77..8e0375b4 100644 --- a/src/Entity/NodeType.php +++ b/src/Entity/NodeType.php @@ -9,7 +9,6 @@ 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; @@ -29,6 +28,7 @@ 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,14 +39,15 @@ class NodeType extends AbstractEntity implements NodeTypeInterface { #[ - ORM\Column(name: "color", type: "string", unique: false, nullable: true), + ORM\Column(name: "color", type: "string", length: 7, unique: false, nullable: true), Serializer\Groups(["node_type", "color"]), SymfonySerializer\Groups(["node_type", "color"]), - Serializer\Type("string") + Serializer\Type("string"), + Assert\Length(max: 7), ] protected ?string $color = '#000000'; #[ - ORM\Column(type: "string", unique: true), + ORM\Column(type: "string", length: 30, unique: true), Serializer\Groups(["node_type", "node"]), SymfonySerializer\Groups(["node_type", "node"]), Serializer\Type("string"), @@ -59,7 +60,7 @@ class NodeType extends AbstractEntity implements NodeTypeInterface ] private string $name = ''; #[ - ORM\Column(name: "display_name", type: "string"), + ORM\Column(name: "display_name", type: "string", length: 250), Serializer\Groups(["node_type", "node"]), SymfonySerializer\Groups(["node_type", "node"]), Serializer\Type("string"), @@ -89,6 +90,24 @@ 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. @@ -120,7 +139,7 @@ class NodeType extends AbstractEntity implements NodeTypeInterface * @var Collection */ #[ - ORM\OneToMany(mappedBy: "nodeType", targetEntity: NodeTypeField::class, cascade: ["persist", "merge"]), + ORM\OneToMany(mappedBy: "nodeType", targetEntity: NodeTypeField::class, cascade: ["all"]), ORM\OrderBy(["position" => "ASC"]), Serializer\Groups(["node_type"]), SymfonySerializer\Groups(["node_type"]), @@ -447,6 +466,7 @@ public function removeField(NodeTypeField $field): NodeType #[SymfonySerializer\Ignore] public function getSourceEntityFullQualifiedClassName(): string { + // @phpstan-ignore-next-line return static::getGeneratedEntitiesNamespace() . '\\' . $this->getSourceEntityClassName(); } @@ -519,4 +539,26 @@ 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 dedfcb7c..34d6fed3 100644 --- a/src/Entity/NodeTypeField.php +++ b/src/Entity/NodeTypeField.php @@ -39,7 +39,7 @@ class NodeTypeField extends AbstractField implements NodeTypeFieldInterface, SerializableInterface { #[ - ORM\Column(type: "string"), + ORM\Column(type: "string", length: 250), Serializer\Expose, Serializer\Groups(["node_type", "setting"]), SymfonySerializer\Groups(["node_type", "setting"]), diff --git a/src/Entity/NodesSources.php b/src/Entity/NodesSources.php index 50b327b5..ca91b957 100644 --- a/src/Entity/NodesSources.php +++ b/src/Entity/NodesSources.php @@ -4,8 +4,8 @@ namespace RZ\Roadiz\CoreBundle\Entity; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter as BaseFilter; -use ApiPlatform\Core\Serializer\Filter\PropertyFilter; +use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; +use ApiPlatform\Serializer\Filter\PropertyFilter; use ApiPlatform\Metadata\ApiFilter; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; @@ -20,8 +20,10 @@ 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. @@ -57,15 +59,6 @@ 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 */ @@ -75,10 +68,11 @@ class NodesSources extends AbstractEntity implements Loggable protected Collection $redirections; #[ApiFilter(BaseFilter\SearchFilter::class, strategy: "partial")] - #[ORM\Column(name: 'title', type: 'string', unique: false, nullable: true)] + #[ORM\Column(name: 'title', type: 'string', length: 250, 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)] protected ?string $title = null; #[ApiFilter(BaseFilter\DateFilter::class)] @@ -91,10 +85,11 @@ class NodesSources extends AbstractEntity implements Loggable protected ?\DateTime $publishedAt = null; #[ApiFilter(BaseFilter\SearchFilter::class, strategy: "partial")] - #[ORM\Column(name: 'meta_title', type: 'string', unique: false)] + #[ORM\Column(name: 'meta_title', type: 'string', length: 150, unique: false)] #[SymfonySerializer\Groups(['nodes_sources'])] #[Serializer\Groups(['nodes_sources'])] #[Gedmo\Versioned] + #[Assert\Length(max: 150)] protected string $metaTitle = ''; #[ORM\Column(name: 'meta_keywords', type: 'text')] @@ -132,6 +127,12 @@ 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" @@ -156,6 +157,7 @@ class NodesSources extends AbstractEntity implements Loggable #[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] private ?Node $node = null; #[ApiFilter(BaseFilter\SearchFilter::class, properties: [ @@ -205,14 +207,10 @@ 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(); } - /** - * @inheritDoc - * @Serializer\Exclude - */ + #[Serializer\Exclude] public function injectObjectManager(ObjectManager $objectManager): void { $this->objectManager = $objectManager; @@ -221,28 +219,29 @@ public function injectObjectManager(ObjectManager $objectManager): void #[ORM\PreUpdate] public function preUpdate(): void { - $this->getNode()?->setUpdatedAt(new \DateTime("now")); + $this->getNode()->setUpdatedAt(new \DateTime("now")); } /** - * @return Node|null + * @return Node */ - public function getNode(): ?Node + public function getNode(): Node { + if (null === $this->node) { + throw new \BadMethodCallException('NodeSource node should never be null.'); + } return $this->node; } /** - * @param Node|null $node + * @param Node $node * * @return $this */ - public function setNode(Node $node = null): NodesSources + public function setNode(Node $node): NodesSources { $this->node = $node; - if (null !== $node) { - $node->addNodeSources($this); - } + $node->addNodeSources($this); return $this; } @@ -285,6 +284,23 @@ 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()->isImage() && + $nsd->getDocument()->isProcessable(); + })->map(function (NodesSourcesDocuments $nsd) { + return $nsd->getDocument(); + })->first() ?: null; + } + /** * @param Collection $documentsByFields * @@ -378,27 +394,6 @@ 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 */ @@ -640,6 +635,37 @@ 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. * @@ -659,8 +685,6 @@ public function __clone() } // Clear url-aliases before cloning. $this->urlAliases->clear(); - // Clear logs before cloning. - $this->logs->clear(); } } } diff --git a/src/Entity/Realm.php b/src/Entity/Realm.php index 118fdab2..69ba3bce 100644 --- a/src/Entity/Realm.php +++ b/src/Entity/Realm.php @@ -4,20 +4,19 @@ namespace RZ\Roadiz\CoreBundle\Entity; -use ApiPlatform\Core\Serializer\Filter\PropertyFilter; +use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; 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; /** @@ -44,11 +43,13 @@ 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)] @@ -64,8 +65,9 @@ class Realm extends AbstractEntity implements RealmInterface * @var string|null * @Serializer\Exclude() */ - #[ORM\Column(name: 'plain_password', unique: false, type: 'string', length: 255, nullable: true)] + #[ORM\Column(name: 'plain_password', type: 'string', length: 255, unique: false, nullable: true)] #[SymfonySerializer\Ignore] + #[Assert\Length(max: 255)] private ?string $plainPassword = null; #[ORM\ManyToOne(targetEntity: Role::class)] @@ -77,6 +79,7 @@ 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; /** diff --git a/src/Entity/RealmNode.php b/src/Entity/RealmNode.php index d51a8b69..07f262a6 100644 --- a/src/Entity/RealmNode.php +++ b/src/Entity/RealmNode.php @@ -11,6 +11,7 @@ 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), @@ -50,6 +51,7 @@ class RealmNode extends AbstractEntity #[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; diff --git a/src/Entity/Redirection.php b/src/Entity/Redirection.php index 2428d2e9..469222ef 100644 --- a/src/Entity/Redirection.php +++ b/src/Entity/Redirection.php @@ -18,25 +18,26 @@ ORM\Entity(repositoryClass: RedirectionRepository::class), ORM\Table(name: "redirections"), ORM\HasLifecycleCallbacks, - UniqueEntity(fields: ["query"]) + 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"), ] 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 */ @@ -59,12 +60,12 @@ public function getQuery(): string } /** - * @param string $query + * @param string|null $query * @return Redirection */ - public function setQuery($query): Redirection + public function setQuery(?string $query): Redirection { - $this->query = $query; + $this->query = $query ?? ''; return $this; } @@ -80,7 +81,7 @@ public function getRedirectUri(): ?string * @param string|null $redirectUri * @return Redirection */ - public function setRedirectUri($redirectUri): Redirection + public function setRedirectUri(?string $redirectUri): Redirection { $this->redirectUri = $redirectUri; return $this; @@ -140,4 +141,18 @@ 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 593b2dc4..d1a03ca8 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', unique: true)] + #[ORM\Column(type: 'string', length: 250, 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 6fa25d8b..8b62f98e 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', unique: true)] + #[ORM\Column(type: 'string', length: 250, unique: true)] #[SymfonySerializer\Groups(['setting', 'nodes_sources'])] #[Serializer\Groups(['setting', 'nodes_sources'])] #[Assert\NotBlank] @@ -93,7 +93,6 @@ class Setting extends AbstractEntity #[ORM\ManyToOne( targetEntity: SettingGroup::class, cascade: ['persist', 'merge'], - fetch: 'EAGER', inversedBy: 'settings' )] #[ORM\JoinColumn(name: 'setting_group_id', referencedColumnName: 'id', onDelete: 'SET NULL')] @@ -216,7 +215,7 @@ public function setValue($value) ($this->getType() === AbstractField::DATETIME_T || $this->getType() === AbstractField::DATE_T) && $value instanceof \DateTimeInterface ) { - $this->value = $value->format('c'); + $this->value = $value->format('c'); // $value is instance of \DateTime } else { $this->value = (string) $value; } diff --git a/src/Entity/SettingGroup.php b/src/Entity/SettingGroup.php index 297c7d67..a8d73195 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', unique: true)] + #[ORM\Column(type: 'string', length: 250, 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 f74cd0ac..ebbf8da5 100644 --- a/src/Entity/Tag.php +++ b/src/Entity/Tag.php @@ -4,8 +4,8 @@ namespace RZ\Roadiz\CoreBundle\Entity; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter as BaseFilter; -use ApiPlatform\Core\Serializer\Filter\PropertyFilter; +use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; +use ApiPlatform\Serializer\Filter\PropertyFilter; use ApiPlatform\Metadata\ApiFilter; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; @@ -103,7 +103,6 @@ class Tag extends AbstractDateTimedPositioned implements LeafInterface mappedBy: 'tag', targetEntity: TagTranslation::class, cascade: ['all'], - fetch: 'EAGER', orphanRemoval: true )] #[SymfonySerializer\Groups(['translated_tag'])] @@ -111,7 +110,7 @@ class Tag extends AbstractDateTimedPositioned implements LeafInterface protected Collection $translatedTags; #[ApiFilter(BaseFilter\SearchFilter::class, strategy: "partial")] - #[ORM\Column(name: 'tag_name', type: 'string', unique: true)] + #[ORM\Column(name: 'tag_name', type: 'string', length: 250, unique: true)] #[SymfonySerializer\Ignore] #[Serializer\Groups(['tag'])] #[Serializer\Accessor(getter: "getTagName", setter: "setTagName")] @@ -130,7 +129,7 @@ class Tag extends AbstractDateTimedPositioned implements LeafInterface #[Serializer\Groups(['tag', 'tag_base', 'node', 'nodes_sources'])] private bool $visible = true; - #[ORM\Column(name: 'children_order', type: 'string', options: ['default' => 'position'])] + #[ORM\Column(name: 'children_order', type: 'string', length: 60, options: ['default' => 'position'])] #[SymfonySerializer\Ignore] #[Serializer\Groups(["tag", "tag_children_order"])] #[Assert\Length(max: 60)] diff --git a/src/Entity/TagTranslation.php b/src/Entity/TagTranslation.php index a93b9f45..b8aa78f1 100644 --- a/src/Entity/TagTranslation.php +++ b/src/Entity/TagTranslation.php @@ -30,7 +30,7 @@ ] class TagTranslation extends AbstractEntity { - #[ORM\Column(type: 'string')] + #[ORM\Column(type: 'string', length: 250)] #[SymfonySerializer\Groups(['tag', 'node', 'nodes_sources'])] #[Serializer\Groups(['tag', 'node', 'nodes_sources'])] #[Assert\NotBlank] diff --git a/src/Entity/Theme.php b/src/Entity/Theme.php index 86807262..57d5afd6 100644 --- a/src/Entity/Theme.php +++ b/src/Entity/Theme.php @@ -87,15 +87,16 @@ public function getInformations(): array $class = $this->getClassName(); if (class_exists($class)) { - $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']) - ]; - } + $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, + ]; } return []; diff --git a/src/Entity/Translation.php b/src/Entity/Translation.php index d585fed0..f4b2e95f 100644 --- a/src/Entity/Translation.php +++ b/src/Entity/Translation.php @@ -4,9 +4,9 @@ namespace RZ\Roadiz\CoreBundle\Entity; -use ApiPlatform\Core\Serializer\Filter\PropertyFilter; +use ApiPlatform\Serializer\Filter\PropertyFilter; use ApiPlatform\Metadata\ApiFilter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter as BaseFilter; +use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; @@ -573,7 +573,7 @@ class Translation extends AbstractDateTimed implements TranslationInterface * @Serializer\Groups({"translation", "translation_base"}) * @Serializer\Type("string") */ - #[ORM\Column(type: 'string', unique: true)] + #[ORM\Column(type: 'string', length: 250, unique: true)] #[SymfonySerializer\Groups(['translation', 'translation_base'])] #[Assert\NotNull] #[Assert\NotBlank] diff --git a/src/Entity/UrlAlias.php b/src/Entity/UrlAlias.php index b937ab13..8298c4de 100644 --- a/src/Entity/UrlAlias.php +++ b/src/Entity/UrlAlias.php @@ -22,11 +22,12 @@ ] class UrlAlias extends AbstractEntity { - #[ORM\Column(type: 'string', unique: true)] + #[ORM\Column(type: 'string', length: 250, unique: true)] #[SymfonySerializer\Groups(['url_alias'])] #[Serializer\Groups(['url_alias'])] #[Assert\NotNull] #[Assert\NotBlank] + #[Assert\Length(max: 250)] #[RoadizAssert\UniqueNodeName] private string $alias = ''; diff --git a/src/Entity/User.php b/src/Entity/User.php index eeb57296..a5d39a62 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -10,16 +10,15 @@ 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), @@ -33,6 +32,8 @@ 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") @@ -51,7 +52,7 @@ class User extends AbstractHuman implements UserInterface, AdvancedUserInterface * @Serializer\Groups({"user_personal", "human"}) * @var string|null */ - #[ORM\Column(type: 'string', unique: true, nullable: false)] + #[ORM\Column(type: 'string', length: 200, unique: true, nullable: false)] #[SymfonySerializer\Groups(['user_personal', 'human'])] #[Assert\NotNull] #[Assert\NotBlank] @@ -67,9 +68,10 @@ class User extends AbstractHuman implements UserInterface, AdvancedUserInterface #[SymfonySerializer\Ignore] protected bool $sendCreationConfirmationEmail = false; - #[ORM\Column(name: 'facebook_name', type: 'string', unique: false, nullable: true)] + #[ORM\Column(name: 'facebook_name', type: 'string', length: 128, unique: false, nullable: true)] #[SymfonySerializer\Groups(['user_social'])] #[Serializer\Groups(['user_social'])] + #[Assert\Length(max: 128)] #[ValidFacebookName] protected ?string $facebookName = null; @@ -94,8 +96,9 @@ class User extends AbstractHuman implements UserInterface, AdvancedUserInterface * @Serializer\Groups({"user_security"}) * @var string|null */ - #[ORM\Column(name: 'confirmation_token', type: 'string', unique: true, nullable: true)] + #[ORM\Column(name: 'confirmation_token', type: 'string', length: 128, unique: true, nullable: true)] #[SymfonySerializer\Groups(['user_security'])] + #[Assert\Length(max: 128)] protected ?string $confirmationToken = null; /** @@ -110,27 +113,20 @@ class User extends AbstractHuman implements UserInterface, AdvancedUserInterface * @Serializer\Groups({"user_personal", "log_user"}) * @var string */ - #[ORM\Column(type: 'string', unique: true)] + #[ORM\Column(type: 'string', length: 200, 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', nullable: false)] + #[ORM\Column(type: 'string', length: 128, nullable: false)] #[SymfonySerializer\Ignore] #[Serializer\Exclude] + #[Assert\Length(max: 128)] private string $password = ''; /** @@ -224,6 +220,7 @@ 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() @@ -232,9 +229,6 @@ public function __construct() $this->groups = new ArrayCollection(); $this->sendCreationConfirmationEmail(false); $this->initAbstractDateTimed(); - - $saltGenerator = new SaltGenerator(); - $this->setSalt($saltGenerator->generateSalt()); } /** @@ -354,17 +348,7 @@ public function setPictureUrl(?string $pictureUrl): User */ public function getSalt(): ?string { - return $this->salt; - } - - /** - * @param string $salt - * @return $this - */ - public function setSalt(string $salt): User - { - $this->salt = $salt; - return $this; + return null; } /** @@ -874,8 +858,8 @@ public function __serialize(): array { return [ $this->password, - $this->salt, $this->username, + $this->getSalt(), $this->enabled, $this->id, $this->email, @@ -891,10 +875,11 @@ 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, @@ -961,10 +946,6 @@ 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 22f2caaa..840cff92 100644 --- a/src/Entity/Webhook.php +++ b/src/Entity/Webhook.php @@ -49,6 +49,7 @@ 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 6621841f..23d662b9 100644 --- a/src/EntityApi/AbstractApi.php +++ b/src/EntityApi/AbstractApi.php @@ -38,7 +38,6 @@ abstract public function getBy(array $criteria); * Return one entity matching criteria array. * * @param array $criteria - * * @return mixed */ abstract public function getOneBy(array $criteria); @@ -47,7 +46,6 @@ 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 e3dc5a8f..d3821a8b 100644 --- a/src/EntityApi/NodeSourceApi.php +++ b/src/EntityApi/NodeSourceApi.php @@ -4,7 +4,7 @@ namespace RZ\Roadiz\CoreBundle\EntityApi; -use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\NonUniqueResultException; 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 $repository = NodesSources::class; + protected string $nodeSourceClassName = NodesSources::class; /** * @param array|null $criteria - * @return string + * @return class-string */ - protected function getRepositoryName(array $criteria = null) + protected function getNodeSourceClassName(array $criteria = null): string { if (isset($criteria['node.nodeType']) && $criteria['node.nodeType'] instanceof NodeType) { - $this->repository = $criteria['node.nodeType']->getSourceEntityFullQualifiedClassName(); + $this->nodeSourceClassName = $criteria['node.nodeType']->getSourceEntityFullQualifiedClassName(); unset($criteria['node.nodeType']); } elseif ( isset($criteria['node.nodeType']) && @@ -32,21 +32,22 @@ protected function getRepositoryName(array $criteria = null) count($criteria['node.nodeType']) === 1 && $criteria['node.nodeType'][0] instanceof NodeType ) { - $this->repository = $criteria['node.nodeType'][0]->getSourceEntityFullQualifiedClassName(); + $this->nodeSourceClassName = $criteria['node.nodeType'][0]->getSourceEntityFullQualifiedClassName(); unset($criteria['node.nodeType']); } else { - $this->repository = NodesSources::class; + $this->nodeSourceClassName = NodesSources::class; } - return $this->repository; + return $this->nodeSourceClassName; } /** - * @return NodesSourcesRepository|EntityRepository + * @return NodesSourcesRepository */ - public function getRepository() + public function getRepository(): NodesSourcesRepository { - return $this->managerRegistry->getRepository($this->repository); + // @phpstan-ignore-next-line + return $this->managerRegistry->getRepository($this->nodeSourceClassName); } /** @@ -62,7 +63,7 @@ public function getBy( ?int $limit = null, ?int $offset = null ) { - $this->getRepositoryName($criteria); + $this->getNodeSourceClassName($criteria); return $this->getRepository() ->findBy( @@ -81,8 +82,7 @@ public function getBy( */ public function countBy(array $criteria) { - $this->getRepositoryName($criteria); - + $this->getNodeSourceClassName($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->getRepositoryName($criteria); - + $this->getNodeSourceClassName($criteria); return $this->getRepository() ->findOneBy( $criteria, @@ -123,19 +123,13 @@ public function searchBy( bool $onlyVisible = false, array $additionalCriteria = [] ) { - $repository = $this->getRepository(); - - if ($repository instanceof NodesSourcesRepository) { - return $this->getRepository() - ->findByTextQuery( - $textQuery, - $limit, - $nodeTypes, - $onlyVisible, - $additionalCriteria - ); - } - - return []; + return $this->getRepository() + ->findByTextQuery( + $textQuery, + $limit, + $nodeTypes, + $onlyVisible, + $additionalCriteria + ); } } diff --git a/src/EntityHandler/DocumentHandler.php b/src/EntityHandler/DocumentHandler.php index 96b4bdbb..106ccf87 100644 --- a/src/EntityHandler/DocumentHandler.php +++ b/src/EntityHandler/DocumentHandler.php @@ -36,22 +36,26 @@ public function __construct(ObjectManager $objectManager, FilesystemOperator $do * 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(): StreamedResponse + public function getDownloadResponse(bool $asAttachment = true): StreamedResponse { if ($this->document->isLocal()) { $documentPath = $this->document->getMountPath(); if ($this->documentStorage->fileExists($documentPath)) { - return new StreamedResponse(function () use ($documentPath) { - \fpassthru($this->documentStorage->readStream($documentPath)); - }, Response::HTTP_OK, [ + $headers = [ "Content-Type" => $this->documentStorage->mimeType($documentPath), "Content-Length" => $this->documentStorage->fileSize($documentPath), - "Content-disposition" => "attachment; filename=\"" . basename($this->document->getFilename()) . "\"", - ]); + ]; + 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); } } diff --git a/src/EntityHandler/NodeTypeHandler.php b/src/EntityHandler/NodeTypeHandler.php index 8c6c7a39..c1ee99c3 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; @@ -35,6 +35,7 @@ class NodeTypeHandler extends AbstractHandler private string $serializedNodeTypesDir; private string $importFilesConfigPath; private string $kernelProjectDir; + private LoggerInterface $logger; /** * @return NodeType @@ -63,6 +64,7 @@ public function __construct( HandlerFactory $handlerFactory, SerializerInterface $serializer, ApiResourceGenerator $apiResourceGenerator, + LoggerInterface $logger, string $generatedEntitiesDir, string $serializedNodeTypesDir, string $importFilesConfigPath, @@ -77,6 +79,7 @@ public function __construct( $this->importFilesConfigPath = $importFilesConfigPath; $this->kernelProjectDir = $kernelProjectDir; $this->apiResourceGenerator = $apiResourceGenerator; + $this->logger = $logger; } public function getGeneratedEntitiesFolder(): string @@ -106,6 +109,11 @@ 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; } @@ -254,6 +262,11 @@ 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/Event/Redirection/PostCreatedRedirectionEvent.php b/src/Event/Redirection/PostCreatedRedirectionEvent.php new file mode 100644 index 00000000..13014c4f --- /dev/null +++ b/src/Event/Redirection/PostCreatedRedirectionEvent.php @@ -0,0 +1,9 @@ +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/AttributeValueIndexingSubscriber.php b/src/EventSubscriber/AttributeValueIndexingSubscriber.php deleted file mode 100644 index bd4a3e6b..00000000 --- a/src/EventSubscriber/AttributeValueIndexingSubscriber.php +++ /dev/null @@ -1,90 +0,0 @@ - '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/CloudflareCacheEventSubscriber.php b/src/EventSubscriber/CloudflareCacheEventSubscriber.php index 04b513bd..caa710e9 100644 --- a/src/EventSubscriber/CloudflareCacheEventSubscriber.php +++ b/src/EventSubscriber/CloudflareCacheEventSubscriber.php @@ -169,11 +169,15 @@ protected function createRequest(array $body): Request $this->getCloudflareCacheProxy()->getVersion(), $this->getCloudflareCacheProxy()->getZone() ); + $body = \json_encode($body); + if (false === $body) { + throw new \RuntimeException('Unable to json_encode body'); + } return new Request( 'POST', $uri, $headers, - \json_encode($body) + $body ); } diff --git a/src/EventSubscriber/RedirectionCacheSubscriber.php b/src/EventSubscriber/RedirectionCacheSubscriber.php new file mode 100644 index 00000000..49dece5c --- /dev/null +++ b/src/EventSubscriber/RedirectionCacheSubscriber.php @@ -0,0 +1,40 @@ +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 eec69e0c..b21cb81e 100644 --- a/src/EventSubscriber/ReverseProxyCacheEventSubscriber.php +++ b/src/EventSubscriber/ReverseProxyCacheEventSubscriber.php @@ -115,9 +115,16 @@ 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', - 'http://' . $frontend->getHost(), + $uri, [ 'Host' => $frontend->getDomainName() ] diff --git a/src/Exception/EmptySaltException.php b/src/Exception/EmptySaltException.php deleted file mode 100644 index 8ebc655b..00000000 --- a/src/Exception/EmptySaltException.php +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index a15276b0..00000000 --- a/src/Exception/TooManyLoginAttemptsException.php +++ /dev/null @@ -1,18 +0,0 @@ - 'ASC'] ); foreach ($attributes as $attribute) { - if (null !== $attribute->getGroup()) { - if (!isset($choices[$attribute->getGroup()->getName()])) { - $choices[$attribute->getGroup()->getName()] = []; + $label = $attribute->getLabelOrCode($options['translation']); + if ( + null !== $attribute->getGroup() && + null !== $groupName = $attribute->getGroup()->getName() + ) { + if (!isset($choices[$groupName]) || !is_array($choices[$groupName])) { + $choices[$groupName] = []; } - $choices[$attribute->getGroup()->getName()][$attribute->getLabelOrCode($options['translation'])] = $attribute->getId(); + $choices[$groupName][$label] = $attribute->getId(); } else { - $choices[$attribute->getLabelOrCode($options['translation'])] = $attribute->getId(); + $choices[$label] = $attribute->getId(); } } return $choices; diff --git a/src/Form/AttributeType.php b/src/Form/AttributeType.php index f4400d74..cd461657 100644 --- a/src/Form/AttributeType.php +++ b/src/Form/AttributeType.php @@ -10,6 +10,7 @@ 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; @@ -59,6 +60,18 @@ 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 new file mode 100644 index 00000000..540fb051 --- /dev/null +++ b/src/Form/AttributeValueRealmType.php @@ -0,0 +1,30 @@ +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 d6e5ad42..0e3b2e8b 100644 --- a/src/Form/AttributeValueTranslationType.php +++ b/src/Form/AttributeValueTranslationType.php @@ -13,10 +13,12 @@ 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; @@ -103,6 +105,9 @@ public function buildForm(FormBuilderInterface $builder, array $options): void break; } } + $builder->add('attributeValue', AttributeValueRealmType::class, [ + 'label' => false, + ]); } /** @@ -134,6 +139,15 @@ 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 9ba3b5c0..696252df 100644 --- a/src/Form/Constraint/NodeTypeFieldValidator.php +++ b/src/Form/Constraint/NodeTypeFieldValidator.php @@ -4,6 +4,7 @@ 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; @@ -19,23 +20,46 @@ class NodeTypeFieldValidator extends ConstraintValidator { + public function __construct( + private readonly ManagerRegistry $registry, + ) { + } + public function validate(mixed $value, Constraint $constraint): void { - if ($value instanceof NodeTypeFieldEntity) { - 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 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->isCollection()) { - $this->validateCollectionTypes($value, $constraint); + 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(); } - } else { - $this->context->buildViolation('Value is not a valid NodeTypeField.')->addViolation(); + } + + 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); } } diff --git a/src/Form/Constraint/UniqueEntity.php b/src/Form/Constraint/UniqueEntity.php deleted file mode 100644 index f5bfd400..00000000 --- a/src/Form/Constraint/UniqueEntity.php +++ /dev/null @@ -1,42 +0,0 @@ - - * @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 deleted file mode 100644 index 701af696..00000000 --- a/src/Form/Constraint/UniqueEntityValidator.php +++ /dev/null @@ -1,171 +0,0 @@ - - * @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 ad40496f..5cd0711c 100644 --- a/src/Form/CustomFormsType.php +++ b/src/Form/CustomFormsType.php @@ -105,11 +105,12 @@ protected function getFieldsByGroups(array $options): array /** @var CustomFormField $field */ foreach ($fields as $field) { - if ($field->getGroupName() != '') { - if (!isset($fieldsArray[$field->getGroupName()])) { - $fieldsArray[$field->getGroupName()] = []; + $groupName = $field->getGroupName(); + if (\is_string($groupName) && $groupName !== '') { + if (!isset($fieldsArray[$groupName]) || !\is_array($fieldsArray[$groupName])) { + $fieldsArray[$groupName] = []; } - $fieldsArray[$field->getGroupName()][] = $field; + $fieldsArray[$groupName][] = $field; } else { $fieldsArray[] = $field; } diff --git a/src/Form/DataTransformer/ExplorerProviderItemTransformer.php b/src/Form/DataTransformer/ExplorerProviderItemTransformer.php index 6d899917..f94a7863 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_scalar($value)) { + } elseif (\is_string($value) || \is_int($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 72825f6b..5259300a 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 string $entityClassname + * @param class-string $entityClassname */ public function __construct( NodeTypeField $nodeTypeField, @@ -74,6 +74,7 @@ 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/ThemesType.php b/src/Form/ThemesType.php index de4becbb..e47a6590 100644 --- a/src/Form/ThemesType.php +++ b/src/Form/ThemesType.php @@ -28,7 +28,9 @@ public function configureOptions(OptionsResolver $resolver): void $value = []; foreach ($options['themes_config'] as $themeConfig) { $class = $themeConfig['classname']; - $value[call_user_func([$class, 'getThemeName'])] = $class; + /** @var callable $callable */ + $callable = [$class, 'getThemeName']; + $value[call_user_func($callable)] = $class; } return $value; }); diff --git a/src/Importer/NodeTypesImporter.php b/src/Importer/NodeTypesImporter.php index da1830a7..0a8c4077 100644 --- a/src/Importer/NodeTypesImporter.php +++ b/src/Importer/NodeTypesImporter.php @@ -6,29 +6,16 @@ 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 { - protected SerializerInterface $serializer; - protected HandlerFactoryInterface $handlerFactory; - - - public function __construct(SerializerInterface $serializer, HandlerFactoryInterface $handlerFactory) - { - $this->serializer = $serializer; - $this->handlerFactory = $handlerFactory; + public function __construct( + protected SerializerInterface $serializer + ) { } - /** - * @inheritDoc - */ public function supports(string $entityClass): bool { return $entityClass === NodeType::class; @@ -39,7 +26,7 @@ public function supports(string $entityClass): bool */ public function import(string $serializedData): bool { - $nodeType = $this->serializer->deserialize( + $this->serializer->deserialize( $serializedData, NodeType::class, 'json', @@ -48,10 +35,6 @@ 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 6e54c0e3..128c964c 100644 --- a/src/ListManager/AbstractEntityListManager.php +++ b/src/ListManager/AbstractEntityListManager.php @@ -13,6 +13,7 @@ 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; @@ -136,6 +137,7 @@ public function getAssignation(): array 'pageCount' => $this->getPageCount(), 'itemPerPage' => $this->getItemPerPage(), 'itemCount' => $this->getItemCount(), + 'search' => $this->searchPattern, 'nextPageQuery' => null, 'previousPageQuery' => null, ]; @@ -214,6 +216,72 @@ 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 7822db82..f46adaa8 100644 --- a/src/ListManager/EntityListManager.php +++ b/src/ListManager/EntityListManager.php @@ -4,6 +4,7 @@ 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; @@ -13,7 +14,6 @@ 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,7 +28,6 @@ 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; @@ -119,59 +118,30 @@ public function handle(bool $disabled = false) unset($this->filteringArray["chroot"]); // remove placeholder } - 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->handleRequestQuery($disabled); $this->createPaginator(); if ( $this->allowRequestSearching && false === $disabled && - null !== $this->request && - $this->request->query->get('search') != "" + null !== $this->request ) { - $this->paginator->setSearchPattern($this->request->query->get('search')); + $search = $this->request->query->get('search'); + if (\is_string($search) && $search !== "") { + $this->paginator->setSearchPattern($search); + } } } + protected function handleOrderingParam(string $field, string $ordering): void + { + $this->validateOrderingFieldName($field); + $this->orderingArray = [ + $field => $ordering + ]; + } + + protected function createPaginator(): void { if ( @@ -213,16 +183,6 @@ 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 0b3816d3..3f8813fa 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 integer $page + * @param int $page * - * @return array + * @return array|\Doctrine\ORM\Tools\Pagination\Paginator */ - public function findByAtPage(array $order = [], $page = 1) + public function findByAtPage(array $order = [], int $page = 1) { if (null !== $this->searchPattern) { return $this->searchByAtPage($order, $page); diff --git a/src/ListManager/NodesSourcesPaginator.php b/src/ListManager/NodesSourcesPaginator.php index 288d0611..c6c746ff 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 + * @return array|\Doctrine\ORM\Tools\Pagination\Paginator */ public function findByAtPage(array $order = [], int $page = 1) { diff --git a/src/ListManager/QueryBuilderListManager.php b/src/ListManager/QueryBuilderListManager.php index d220f474..98f94230 100644 --- a/src/ListManager/QueryBuilderListManager.php +++ b/src/ListManager/QueryBuilderListManager.php @@ -14,6 +14,10 @@ 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 @@ -33,57 +37,40 @@ 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 { - // Implement your custom logic + parent::handleSearchParam($search); + + if (\is_callable($this->searchingCallable)) { + \call_user_func($this->searchingCallable, $this->queryBuilder, $search); + } } public function handle(bool $disabled = false) { - 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(); - } + $this->handleRequestQuery($disabled); + } + + protected function handleOrderingParam(string $field, string $ordering): void + { + $this->validateOrderingFieldName($field); + $this->queryBuilder->addOrderBy( + sprintf('%s.%s', $this->identifier, $field), + $ordering + ); } /** diff --git a/src/Logger/DoctrineHandler.php b/src/Logger/DoctrineHandler.php index 330dea02..dbb55899 100644 --- a/src/Logger/DoctrineHandler.php +++ b/src/Logger/DoctrineHandler.php @@ -7,9 +7,14 @@ use Doctrine\Persistence\ManagerRegistry; use Monolog\Handler\AbstractProcessingHandler; use Monolog\Logger; -use RZ\Roadiz\CoreBundle\Entity\Log; +use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; +use RZ\Roadiz\CoreBundle\Entity\Node; 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; @@ -19,40 +24,98 @@ */ final class DoctrineHandler extends AbstractProcessingHandler { - protected ManagerRegistry $managerRegistry; - protected TokenStorageInterface $tokenStorage; - protected RequestStack $requestStack; + private ManagerRegistry $managerRegistry; + private TokenStorageInterface $tokenStorage; + private RequestStack $requestStack; + private DocumentUrlGeneratorInterface $documentUrlGenerator; 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; + } - parent::__construct($level, $bubble); + protected function getThumbnailSourcePath(?DocumentInterface $thumbnail): ?string + { + if (null === $thumbnail || $thumbnail->isPrivate()) { + return null; + } + return $this->documentUrlGenerator + ->setDocument($thumbnail) + ->setOptions([ + "fit" => "150x150", + "quality" => 70, + ]) + ->getUrl(); } - /** - * @return TokenStorageInterface - */ - public function getTokenStorage(): TokenStorageInterface + protected function populateForNode(Node $value, Log $log, array &$data): void { - return $this->tokenStorage; + $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, + ] + ); + } } - /** - * @param TokenStorageInterface $tokenStorage - * - * @return $this - */ - public function setTokenStorage(TokenStorageInterface $tokenStorage): DoctrineHandler + + protected function populateForNodesSources(NodesSources $value, Log $log, array &$data): void { - $this->tokenStorage = $tokenStorage; - return $this; + $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, + ] + ); + } } /** @@ -73,39 +136,84 @@ public function write(array $record): void $log->setChannel((string) $record['channel']); $data = $record['extra']; - 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']] - ); + $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); + } + } } - $log->setAdditionalData($data); /* * Use available securityAuthorizationChecker to provide a valid user */ - if ( - null !== $this->getTokenStorage() && - null !== $token = $this->getTokenStorage()->getToken() - ) { + if (null !== $token = $this->tokenStorage->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->getUsername()); } @@ -121,15 +229,7 @@ public function write(array $record): void $log->setClientIp($this->requestStack->getMainRequest()->getClientIp()); } - /* - * Add a related node-source entity - */ - if ( - isset($record['context']['source']) && - $record['context']['source'] instanceof NodesSources - ) { - $log->setNodeSource($record['context']['source']); - } + $log->setAdditionalData($data); $manager->persist($log); $manager->flush(); diff --git a/src/Entity/Log.php b/src/Logger/Entity/Log.php similarity index 60% rename from src/Entity/Log.php rename to src/Logger/Entity/Log.php index c42579f1..8dbdf02f 100644 --- a/src/Entity/Log.php +++ b/src/Logger/Entity/Log.php @@ -2,20 +2,26 @@ declare(strict_types=1); -namespace RZ\Roadiz\CoreBundle\Entity; +namespace RZ\Roadiz\CoreBundle\Logger\Entity; use Doctrine\ORM\Mapping as ORM; +use JMS\Serializer\Annotation as Serializer; use Monolog\Logger; use RZ\Roadiz\Core\AbstractEntities\AbstractEntity; -use JMS\Serializer\Annotation as Serializer; +use RZ\Roadiz\CoreBundle\Entity\NodesSources; +use RZ\Roadiz\CoreBundle\Entity\User; 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: ["node_source_id", "datetime"], name: "log_ns_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: ["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"), @@ -27,25 +33,16 @@ ] class Log extends AbstractEntity { - 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')] + #[ORM\Column(name: 'user_id', type: 'string', length: 36, unique: false, nullable: true)] #[SymfonySerializer\Groups(['log_user'])] #[Serializer\Groups(['log_user'])] - protected ?User $user = null; + // @phpstan-ignore-next-line + protected int|string|null $userId = null; - #[ORM\Column(name: 'username', type: 'string', nullable: true)] + #[ORM\Column(name: 'username', type: 'string', length: 255, nullable: true)] #[SymfonySerializer\Groups(['log_user'])] #[Serializer\Groups(['log_user'])] + #[Assert\Length(max: 255)] protected ?string $username = null; #[ORM\Column(name: 'message', type: 'text')] @@ -56,29 +53,45 @@ class Log extends AbstractEntity #[ORM\Column(name: 'level', type: 'integer', nullable: false)] #[SymfonySerializer\Groups(['log'])] #[Serializer\Groups(['log'])] - protected int $level = Log::DEBUG; + protected int $level = Logger::DEBUG; #[ORM\Column(name: 'datetime', type: 'datetime', nullable: false)] #[SymfonySerializer\Groups(['log'])] #[Serializer\Groups(['log'])] protected \DateTime $datetime; - #[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)] + #[ORM\Column(name: 'client_ip', type: 'string', length: 46, unique: false, nullable: true)] #[SymfonySerializer\Groups(['log'])] #[Serializer\Groups(['log'])] + #[Assert\Length(max: 46)] protected ?string $clientIp = null; - #[ORM\Column(name: 'channel', type: 'string', unique: false, nullable: true)] + #[ORM\Column(name: 'channel', type: 'string', length: 64, 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'])] @@ -97,9 +110,22 @@ public function __construct(int $level, string $message) $this->datetime = new \DateTime("now"); } - public function getUser(): ?User + /** + * @return int|string|null + */ + public function getUserId(): int|string|null { - return $this->user; + return $this->userId; + } + + /** + * @param int|string|null $userId + * @return Log + */ + public function setUserId(int|string|null $userId): Log + { + $this->userId = $userId; + return $this; } /** @@ -109,7 +135,7 @@ public function getUser(): ?User */ public function setUser(User $user): Log { - $this->user = $user; + $this->userId = $user->getId(); $this->username = $user->getUsername(); return $this; } @@ -159,27 +185,22 @@ public function getDatetime(): \DateTime } /** - * Get log related node-source. + * BC setter. * - * @return NodesSources|null - */ - public function getNodeSource(): ?NodesSources - { - return $this->nodeSource; - } - - /** * @param NodesSources|null $nodeSource * @return $this */ public function setNodeSource(?NodesSources $nodeSource): Log { - $this->nodeSource = $nodeSource; + if (null !== $nodeSource) { + $this->entityClass = NodesSources::class; + $this->entityId = $nodeSource->getId(); + } return $this; } /** - * @return string + * @return string|null */ public function getClientIp(): ?string { @@ -187,7 +208,7 @@ public function getClientIp(): ?string } /** - * @param string $clientIp + * @param string|null $clientIp * @return Log */ public function setClientIp(?string $clientIp): Log @@ -236,6 +257,42 @@ 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/Mailer/EmailManager.php b/src/Mailer/EmailManager.php index 346ebf60..eb84898f 100644 --- a/src/Mailer/EmailManager.php +++ b/src/Mailer/EmailManager.php @@ -93,9 +93,13 @@ public function renderHtmlEmailBodyWithCss(): string { if (null !== $this->getEmailStylesheet()) { $htmldoc = new InlineStyle($this->renderHtmlEmailBody()); - $htmldoc->applyStylesheet(file_get_contents( + $css = file_get_contents( $this->getEmailStylesheet() - )); + ); + if (false === $css) { + throw new \RuntimeException('Unable to read email stylesheet file.'); + } + $htmldoc->applyStylesheet($css); return $htmldoc->getHTML(); } diff --git a/src/Message/ApplyRealmNodeInheritanceMessage.php b/src/Message/ApplyRealmNodeInheritanceMessage.php index 7159e176..7943f8a8 100644 --- a/src/Message/ApplyRealmNodeInheritanceMessage.php +++ b/src/Message/ApplyRealmNodeInheritanceMessage.php @@ -6,27 +6,27 @@ final class ApplyRealmNodeInheritanceMessage implements AsyncMessage { - private int $nodeId; - private ?int $realmId; + private int|string|null $nodeId; + private int|string|null $realmId; - public function __construct(int $nodeId, ?int $realmId) + public function __construct(int|string|null $nodeId, int|string|null $realmId) { $this->nodeId = $nodeId; $this->realmId = $realmId; } /** - * @return int + * @return int|string|null */ - public function getNodeId(): int + public function getNodeId(): int|string|null { return $this->nodeId; } /** - * @return int|null + * @return int|string|null */ - public function getRealmId(): ?int + public function getRealmId(): int|string|null { return $this->realmId; } diff --git a/src/Message/CleanRealmNodeInheritanceMessage.php b/src/Message/CleanRealmNodeInheritanceMessage.php index 05579c30..cf1d1201 100644 --- a/src/Message/CleanRealmNodeInheritanceMessage.php +++ b/src/Message/CleanRealmNodeInheritanceMessage.php @@ -6,27 +6,27 @@ final class CleanRealmNodeInheritanceMessage implements AsyncMessage { - private int $nodeId; - private ?int $realmId; + private int|string|null $nodeId; + private int|string|null $realmId; - public function __construct(int $nodeId, ?int $realmId) + public function __construct(int|string|null $nodeId, int|string|null $realmId) { $this->nodeId = $nodeId; $this->realmId = $realmId; } /** - * @return int + * @return int|string|null */ - public function getNodeId(): int + public function getNodeId(): int|string|null { return $this->nodeId; } /** - * @return int|null + * @return int|string|null */ - public function getRealmId(): ?int + public function getRealmId(): int|string|null { return $this->realmId; } diff --git a/src/Message/DeleteNodeTypeMessage.php b/src/Message/DeleteNodeTypeMessage.php index a5f3c682..0f3caca1 100644 --- a/src/Message/DeleteNodeTypeMessage.php +++ b/src/Message/DeleteNodeTypeMessage.php @@ -6,20 +6,20 @@ final class DeleteNodeTypeMessage implements AsyncMessage { - private int $nodeTypeId; + private int|string|null $nodeTypeId; /** - * @param int $nodeTypeId + * @param int|string|null $nodeTypeId */ - public function __construct(int $nodeTypeId) + public function __construct(int|string|null $nodeTypeId) { $this->nodeTypeId = $nodeTypeId; } /** - * @return int + * @return int|string|null */ - public function getNodeTypeId(): int + public function getNodeTypeId(): int|string|null { return $this->nodeTypeId; } diff --git a/src/Message/PurgeReverseProxyCacheMessage.php b/src/Message/PurgeReverseProxyCacheMessage.php index 58e79384..be0c54c0 100644 --- a/src/Message/PurgeReverseProxyCacheMessage.php +++ b/src/Message/PurgeReverseProxyCacheMessage.php @@ -6,20 +6,20 @@ final class PurgeReverseProxyCacheMessage implements AsyncMessage { - private int $nodeSourceId; + private int|string|null $nodeSourceId; /** - * @param int $nodeSourceId + * @param int|string|null $nodeSourceId */ - public function __construct(int $nodeSourceId) + public function __construct(int|string|null $nodeSourceId) { $this->nodeSourceId = $nodeSourceId; } /** - * @return int + * @return int|string|null */ - public function getNodeSourceId(): int + public function getNodeSourceId(): int|string|null { return $this->nodeSourceId; } diff --git a/src/Message/SearchRealmNodeInheritanceMessage.php b/src/Message/SearchRealmNodeInheritanceMessage.php index e3732026..c9ec45ff 100644 --- a/src/Message/SearchRealmNodeInheritanceMessage.php +++ b/src/Message/SearchRealmNodeInheritanceMessage.php @@ -6,17 +6,17 @@ final class SearchRealmNodeInheritanceMessage implements AsyncMessage { - private int $nodeId; + private int|string|null $nodeId; - public function __construct(int $nodeId) + public function __construct(int|string|null $nodeId) { $this->nodeId = $nodeId; } /** - * @return int + * @return int|string|null */ - public function getNodeId(): int + public function getNodeId(): int|string|null { return $this->nodeId; } diff --git a/src/Message/UpdateNodeTypeSchemaMessage.php b/src/Message/UpdateNodeTypeSchemaMessage.php index 830828b5..f4a309a5 100644 --- a/src/Message/UpdateNodeTypeSchemaMessage.php +++ b/src/Message/UpdateNodeTypeSchemaMessage.php @@ -9,20 +9,20 @@ */ final class UpdateNodeTypeSchemaMessage { - private int $nodeTypeId; + private int|string|null $nodeTypeId; /** - * @param int $nodeTypeId + * @param int|string|null $nodeTypeId */ - public function __construct(int $nodeTypeId) + public function __construct(int|string|null $nodeTypeId) { $this->nodeTypeId = $nodeTypeId; } /** - * @return int + * @return int|string|null */ - public function getNodeTypeId(): int + public function getNodeTypeId(): int|string|null { return $this->nodeTypeId; } diff --git a/src/Model/AttributeGroupTrait.php b/src/Model/AttributeGroupTrait.php index 6348ad92..9bd498c7 100644 --- a/src/Model/AttributeGroupTrait.php +++ b/src/Model/AttributeGroupTrait.php @@ -17,10 +17,11 @@ trait AttributeGroupTrait { #[ - ORM\Column(name: "canonical_name", type: "string", unique: true, nullable: false), + ORM\Column(name: "canonical_name", type: "string", length: 255, 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 = ''; diff --git a/src/Model/AttributeGroupTranslationTrait.php b/src/Model/AttributeGroupTranslationTrait.php index 241fdb31..f89961f2 100644 --- a/src/Model/AttributeGroupTranslationTrait.php +++ b/src/Model/AttributeGroupTranslationTrait.php @@ -7,6 +7,7 @@ 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 { @@ -20,9 +21,10 @@ trait AttributeGroupTranslationTrait protected ?TranslationInterface $translation = null; #[ - ORM\Column(type: "string", unique: false, nullable: false), + ORM\Column(type: "string", length: 255, unique: false, nullable: false), Serializer\Groups(["attribute_group", "attribute", "node", "nodes_sources"]), - Serializer\Type("string") + Serializer\Type("string"), + Assert\Length(max: 255) ] protected string $name = ''; diff --git a/src/Model/AttributeInterface.php b/src/Model/AttributeInterface.php index f22e2ec2..1da81a62 100644 --- a/src/Model/AttributeInterface.php +++ b/src/Model/AttributeInterface.php @@ -129,6 +129,8 @@ public function getType(): int; */ public function getColor(): ?string; + public function getWeight(): int; + /** * @param string|null $color */ @@ -215,4 +217,7 @@ 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 64cc7174..2bdde936 100644 --- a/src/Model/AttributeTrait.php +++ b/src/Model/AttributeTrait.php @@ -15,12 +15,13 @@ trait AttributeTrait { #[ - ORM\Column(type: "string", unique: true, nullable: false), + ORM\Column(type: "string", length: 255, unique: true, nullable: false), Serializer\Groups(["attribute", "node", "nodes_sources"]), SymfonySerializer\Groups(["attribute", "node", "nodes_sources"]), Serializer\Type("string"), Assert\NotNull(), - Assert\NotBlank() + Assert\NotBlank(), + Assert\Length(max: 255) ] protected string $code = ''; @@ -44,7 +45,8 @@ 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") + Serializer\Type("string"), + Assert\Length(max: 7) ] protected ?string $color = null; @@ -227,9 +229,9 @@ public function getOptions(TranslationInterface $translation): ?array function (AttributeTranslationInterface $attributeTranslation) use ($translation) { return $attributeTranslation->getTranslation() === $translation; } - ); - if ($attributeTranslation->count() > 0) { - return $attributeTranslation->first()->getOptions(); + )->first(); + if (false !== $attributeTranslation) { + return $attributeTranslation->getOptions(); } return null; @@ -344,4 +346,26 @@ 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 0c2cd860..b415ee86 100644 --- a/src/Model/AttributeTranslationTrait.php +++ b/src/Model/AttributeTranslationTrait.php @@ -4,9 +4,10 @@ namespace RZ\Roadiz\CoreBundle\Model; -use JMS\Serializer\Annotation as Serializer; 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 AttributeTranslationTrait { @@ -20,9 +21,10 @@ trait AttributeTranslationTrait protected ?TranslationInterface $translation = null; #[ - ORM\Column(type: "string", unique: false, nullable: false), + ORM\Column(type: "string", length: 250, unique: false, nullable: false), Serializer\Groups(["attribute", "node", "nodes_sources"]), - Serializer\Type("string") + Serializer\Type("string"), + Assert\Length(max: 250) ] protected string $label = ''; diff --git a/src/Model/AttributeValueInterface.php b/src/Model/AttributeValueInterface.php index 8bb43e55..aab86a8c 100644 --- a/src/Model/AttributeValueInterface.php +++ b/src/Model/AttributeValueInterface.php @@ -11,8 +11,11 @@ interface AttributeValueInterface extends PositionedInterface, PersistableInterface { + public function getRealm(): ?RealmInterface; + public function setRealm(?RealmInterface $realm): self; + /** - * @return AttributeInterface + * @return AttributeInterface|null */ public function getAttribute(): ?AttributeInterface; diff --git a/src/Model/AttributeValueTrait.php b/src/Model/AttributeValueTrait.php index fd6b0cbe..91edbd7c 100644 --- a/src/Model/AttributeValueTrait.php +++ b/src/Model/AttributeValueTrait.php @@ -9,7 +9,7 @@ use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter as BaseFilter; +use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; trait AttributeValueTrait { @@ -21,6 +21,7 @@ trait AttributeValueTrait ApiFilter(BaseFilter\SearchFilter::class, properties: [ "attribute.id" => "exact", "attribute.code" => "exact", + "attribute.color" => "exact", "attribute.type" => "exact", "attribute.group" => "exact", "attribute.group.canonicalName" => "exact", @@ -28,6 +29,13 @@ 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 = null; @@ -45,12 +53,21 @@ trait AttributeValueTrait ), Serializer\Groups(["attribute", "node", "nodes_sources"]), Serializer\Type("ArrayCollection"), - Serializer\Accessor(getter: "getAttributeValueTranslations", setter: "setAttributeValueTranslations") + 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", + ]), ] protected Collection $attributeValueTranslations; /** - * @return AttributeInterface + * @return AttributeInterface|null */ public function getAttribute(): ?AttributeInterface { @@ -60,7 +77,7 @@ public function getAttribute(): ?AttributeInterface /** * @param AttributeInterface $attribute * - * @return mixed + * @return static */ public function setAttribute(AttributeInterface $attribute) { @@ -87,7 +104,7 @@ public function getAttributeValueTranslations(): Collection /** * @param Collection $attributeValueTranslations * - * @return mixed + * @return static */ public function setAttributeValueTranslations(Collection $attributeValueTranslations) { @@ -96,13 +113,13 @@ public function setAttributeValueTranslations(Collection $attributeValueTranslat foreach ($this->attributeValueTranslations as $attributeValueTranslation) { $attributeValueTranslation->setAttributeValue($this); } - return true; + return $this; } /** * @param TranslationInterface $translation * - * @return AttributeValueTranslationInterface + * @return AttributeValueTranslationInterface|null */ public function getAttributeValueTranslation(TranslationInterface $translation): ?AttributeValueTranslationInterface { @@ -115,4 +132,16 @@ 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 94020752..abb93cb0 100644 --- a/src/Model/AttributeValueTranslationTrait.php +++ b/src/Model/AttributeValueTranslationTrait.php @@ -7,6 +7,7 @@ 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 { @@ -22,7 +23,8 @@ trait AttributeValueTranslationTrait #[ ORM\Column(type: "string", length: 255, unique: false, nullable: true), Serializer\Groups(["attribute", "node", "nodes_sources"]), - Serializer\Type("string") + Serializer\Type("string"), + Assert\Length(max: 255) ] protected ?string $value = null; @@ -34,7 +36,7 @@ trait AttributeValueTranslationTrait protected ?AttributeValueInterface $attributeValue = null; /** - * @return mixed|null + * @return bool|\DateTime|float|int|string|null * @throws \Exception */ public function getValue() @@ -42,19 +44,13 @@ public function getValue() if (null === $this->value) { return null; } - 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; - } + 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, + }; } /** @@ -76,7 +72,7 @@ public function setValue($value) return $this; case AttributeInterface::DATETIME_T: case AttributeInterface::DATE_T: - if ($value instanceof \DateTime) { + if ($value instanceof \DateTimeInterface) { $this->value = $value->format('Y-m-d H:i:s'); } else { $this->value = (string) $value; @@ -127,7 +123,7 @@ public function setAttributeValue(AttributeValueInterface $attributeValue) } /** - * @return AttributeInterface + * @return AttributeInterface|null */ public function getAttribute(): ?AttributeInterface { diff --git a/src/Model/RealmInterface.php b/src/Model/RealmInterface.php index e637d108..6518eca8 100644 --- a/src/Model/RealmInterface.php +++ b/src/Model/RealmInterface.php @@ -5,8 +5,9 @@ namespace RZ\Roadiz\CoreBundle\Model; use Doctrine\Common\Collections\Collection; +use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; -interface RealmInterface +interface RealmInterface extends PersistableInterface { public const TYPE_PLAIN_PASSWORD = 'plain_password'; public const TYPE_ROLE = 'bearer_role'; diff --git a/src/Node/NodeDuplicator.php b/src/Node/NodeDuplicator.php index 21e6c091..0c1d6131 100644 --- a/src/Node/NodeDuplicator.php +++ b/src/Node/NodeDuplicator.php @@ -79,8 +79,12 @@ public function duplicate(): Node */ private function doDuplicate(Node &$node): Node { + $nodeSource = $node->getNodeSources()->first(); + if (false === $nodeSource) { + throw new \RuntimeException('Node source is missing.'); + } $node->setNodeName( - $this->nodeNamePolicy->getSafeNodeName($node->getNodeSources()->first()) + $this->nodeNamePolicy->getSafeNodeName($nodeSource) ); /** @var Node $child */ diff --git a/src/Node/NodeFactory.php b/src/Node/NodeFactory.php index 47b33cbb..49aeff5c 100644 --- a/src/Node/NodeFactory.php +++ b/src/Node/NodeFactory.php @@ -103,7 +103,7 @@ public function createWithUrlAlias( /** @var UrlAliasRepository $repository */ $repository = $this->managerRegistry->getRepository(UrlAlias::class); if (false === $repository->exists($urlAlias)) { - $alias = new UrlAlias($node->getNodeSources()->first()); + $alias = new UrlAlias($node->getNodeSources()->first() ?: null); $alias->setAlias($urlAlias); $this->managerRegistry->getManagerForClass(UrlAlias::class)->persist($alias); } diff --git a/src/Node/NodeMover.php b/src/Node/NodeMover.php index 2e866db9..8deaff0d 100644 --- a/src/Node/NodeMover.php +++ b/src/Node/NodeMover.php @@ -14,6 +14,8 @@ 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; @@ -201,6 +203,7 @@ protected function redirect(NodesSources $nodeSource, string $previousPath, bool $this->getManager()->remove($loopingRedirection); } + /** @var Redirection|null $existingRedirection */ $existingRedirection = $redirectionRepo->findOneBy([ 'query' => $previousPath, ]); @@ -210,7 +213,7 @@ protected function redirect(NodesSources $nodeSource, string $previousPath, bool $existingRedirection->setQuery($previousPath); $this->logger->info('New redirection created', [ 'oldPath' => $previousPath, - 'nodeSource' => $nodeSource->getId(), + 'nodeSource' => $nodeSource, ]); } $existingRedirection->setRedirectNodeSource($nodeSource); @@ -219,6 +222,7 @@ 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 e1c4f98b..b9b54d77 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() ? $nodeSource->getNode()->getId() : $nodeSource->getId() + null !== $nodeSource->getNode()->getId() ); } diff --git a/src/Node/NodeTranstyper.php b/src/Node/NodeTranstyper.php index f4a20417..f5a150e1 100644 --- a/src/Node/NodeTranstyper.php +++ b/src/Node/NodeTranstyper.php @@ -13,7 +13,6 @@ 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; @@ -113,14 +112,6 @@ 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) { @@ -141,7 +132,6 @@ public function transtype(Node $node, NodeTypeInterface $destinationNodeType, bo $existingSource->getTranslation(), $sourceClass, $fieldAssociations, - $existingLogs, $existingRedirections ); $this->logger->debug('Transtyped: ' . $existingSource->getTranslation()->getLocale()); @@ -176,7 +166,6 @@ protected function removeOldSources(Node $node, array &$sources): void * @param TranslationInterface $translation * @param string $sourceClass * @param array $fieldAssociations - * @param array $existingLogs * @param array $existingRedirections * @return NodesSources */ @@ -186,7 +175,6 @@ protected function doTranstypeSingleSource( TranslationInterface $translation, string $sourceClass, array &$fieldAssociations, - array &$existingLogs, array &$existingRedirections ): NodesSources { /** @var NodesSources $source */ @@ -221,20 +209,6 @@ 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. */ diff --git a/src/Node/UniqueNodeGenerator.php b/src/Node/UniqueNodeGenerator.php index 3f3c6762..cdbedc64 100644 --- a/src/Node/UniqueNodeGenerator.php +++ b/src/Node/UniqueNodeGenerator.php @@ -4,6 +4,9 @@ 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; @@ -11,22 +14,19 @@ 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\Component\Security\Core\Security; class UniqueNodeGenerator { - 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; + public function __construct( + protected ManagerRegistry $managerRegistry, + protected NodeNamePolicyInterface $nodeNamePolicy, + protected Security $security, + ) { } /** @@ -55,9 +55,7 @@ public function generate( if (null !== $tag) { $node->addTag($tag); } - if (null !== $parent) { - $parent->addChild($node); - } + $parent?->addChild($node); if ($pushToTop) { /* @@ -66,11 +64,12 @@ 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 +88,8 @@ public function generate( * @param Request $request * * @return NodesSources - * @throws \Doctrine\ORM\ORMException - * @throws \Doctrine\ORM\OptimisticLockException + * @throws ORMException + * @throws OptimisticLockException */ public function generateFromRequest(Request $request): NodesSources { @@ -112,7 +111,13 @@ 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; } @@ -139,8 +144,8 @@ public function generateFromRequest(Request $request): NodesSources /* * If parent has only on translation, use parent translation instead of default one. */ - if (null !== $parent && $parent->getNodeSources()->count() === 1) { - $translation = $parent->getNodeSources()->first()->getTranslation(); + if (null !== $parent && false !== $parentNodeSource = $parent->getNodeSources()->first()) { + $translation = $parentNodeSource->getTranslation(); } else { /** @var Translation $translation */ $translation = $this->managerRegistry diff --git a/src/NodeType/ApiResourceGenerator.php b/src/NodeType/ApiResourceGenerator.php index 59192eaa..548ae63d 100644 --- a/src/NodeType/ApiResourceGenerator.php +++ b/src/NodeType/ApiResourceGenerator.php @@ -4,7 +4,9 @@ namespace RZ\Roadiz\CoreBundle\NodeType; +use Doctrine\Inflector\InflectorFactory; use LogicException; +use Psr\Log\LoggerInterface; use RZ\Roadiz\Contracts\NodeType\NodeTypeInterface; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\String\UnicodeString; @@ -13,10 +15,12 @@ final class ApiResourceGenerator { private string $apiResourcesDir; + private LoggerInterface $logger; - public function __construct(string $apiResourcesDir) + public function __construct(string $apiResourcesDir, LoggerInterface $logger) { $this->apiResourcesDir = $apiResourcesDir; + $this->logger = $logger; } /** @@ -38,6 +42,10 @@ public function generate(NodeTypeInterface $nodeType): ?string $resourcePath, 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 { @@ -57,6 +65,10 @@ public function remove(NodeTypeInterface $nodeType): void 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); } } @@ -70,6 +82,20 @@ protected function getResourcePath(NodeTypeInterface $nodeType): string ->toString(); } + 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())) @@ -77,15 +103,17 @@ protected function getApiResourceDefinition(NodeTypeInterface $nodeType): array ->toString(); return [ $fqcn => [ - 'iri' => $nodeType->getName(), - 'shortName' => $nodeType->getName(), - 'collectionOperations' => $this->getCollectionOperations($nodeType), - 'itemOperations' => $this->getItemOperations($nodeType), + 'types' => [$nodeType->getName()], + 'operations' => [ + ...$this->getCollectionOperations($nodeType), + ...$this->getItemOperations($nodeType) + ], ]]; } protected function getCollectionOperations(NodeTypeInterface $nodeType): array { + $operations = []; if ($nodeType->isReachable()) { $groups = [ "nodes_sources_base", @@ -98,23 +126,51 @@ protected function getCollectionOperations(NodeTypeInterface $nodeType): array "document_display_sources", ...$this->getGroupedFieldsSerializationGroups($nodeType) ]; - return [ - 'get' => [ - 'method' => 'GET', - 'normalization_context' => [ - 'enable_max_depth' => true, - 'groups' => array_values(array_filter(array_unique($groups))) - ], + $operations = array_merge( + $operations, + [ + 'ApiPlatform\Metadata\GetCollection' => [ + 'method' => 'GET', + 'shortName' => $nodeType->getName(), + 'normalizationContext' => [ + 'enable_max_depth' => true, + 'groups' => array_values(array_filter(array_unique($groups))) + ], + ] ] - ]; + ); } - return []; + if ($nodeType->isPublishable()) { + $archivesRouteName = '_api_' . $this->getResourceName($nodeType->getName()) . '_archives'; + $operations = array_merge( + $operations, + [ + $archivesRouteName => [ + 'method' => 'GET', + 'class' => 'ApiPlatform\Metadata\GetCollection', + '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 $operations; } protected function getItemOperations(NodeTypeInterface $nodeType): array { $groups = [ "nodes_sources", + "node_listing", "urls", "tag_base", "translation_base", @@ -124,9 +180,10 @@ protected function getItemOperations(NodeTypeInterface $nodeType): array ...$this->getGroupedFieldsSerializationGroups($nodeType) ]; $operations = [ - 'get' => [ + 'ApiPlatform\Metadata\Get' => [ 'method' => 'GET', - 'normalization_context' => [ + 'shortName' => $nodeType->getName(), + 'normalizationContext' => [ 'groups' => array_values(array_filter(array_unique($groups))) ], ] @@ -138,11 +195,12 @@ protected function getItemOperations(NodeTypeInterface $nodeType): array if ($nodeType->isReachable()) { $operations['getByPath'] = [ 'method' => 'GET', - 'path' => '/web_response_by_path', + 'class' => 'ApiPlatform\Metadata\Get', + 'uriTemplate' => '/web_response_by_path', 'read' => false, 'controller' => 'RZ\Roadiz\CoreBundle\Api\Controller\GetWebResponseByPathController', - 'pagination_enabled' => false, - 'normalization_context' => [ + 'normalizationContext' => [ + 'pagination_enabled' => false, 'enable_max_depth' => true, 'groups' => array_merge(array_values(array_filter(array_unique($groups))), [ 'web_response', @@ -153,6 +211,23 @@ protected function getItemOperations(NodeTypeInterface $nodeType): array 'children', ]) ], + 'openapiContext' => [ + 'tags' => ['WebResponse'], + '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/src/NodeType/DefaultValuesResolver.php b/src/NodeType/DefaultValuesResolver.php index cc2fc94d..e2180403 100644 --- a/src/NodeType/DefaultValuesResolver.php +++ b/src/NodeType/DefaultValuesResolver.php @@ -23,7 +23,6 @@ public function __construct( $this->inheritanceType = $inheritanceType; } - public function getDefaultValuesAmongAllFields(NodeTypeFieldInterface $field): array { /* @@ -31,7 +30,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 @@ -43,7 +42,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/Realm/RealmResolver.php b/src/Realm/RealmResolver.php index fa4f0bf2..46570ecb 100644 --- a/src/Realm/RealmResolver.php +++ b/src/Realm/RealmResolver.php @@ -5,22 +5,22 @@ 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\Component\Security\Core\Security; +use Symfony\Component\String\Slugger\AsciiSlugger; final class RealmResolver implements RealmResolverInterface { - private ManagerRegistry $managerRegistry; - private Security $security; - - public function __construct(ManagerRegistry $managerRegistry, Security $security) - { - $this->managerRegistry = $managerRegistry; - $this->security = $security; + public function __construct( + private readonly ManagerRegistry $managerRegistry, + private readonly Security $security, + private readonly CacheItemPoolInterface $cache + ) { } public function getRealms(?Node $node): array @@ -45,4 +45,35 @@ 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(); + } } diff --git a/src/Realm/RealmResolverInterface.php b/src/Realm/RealmResolverInterface.php index 970b094f..fd0ca8cb 100644 --- a/src/Realm/RealmResolverInterface.php +++ b/src/Realm/RealmResolverInterface.php @@ -23,4 +23,14 @@ 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 1f4fe5e4..d494a49c 100644 --- a/src/Repository/AttributeValueRepository.php +++ b/src/Repository/AttributeValueRepository.php @@ -24,14 +24,15 @@ public function __construct( /** * @param AttributableInterface $attributable - * - * @return array + * @param bool $orderByWeight + * @return array */ public function findByAttributable( - AttributableInterface $attributable + AttributableInterface $attributable, + bool $orderByWeight = false ): array { $qb = $this->createQueryBuilder('av'); - return $qb->addSelect('avt') + $qb = $qb->addSelect('avt') ->addSelect('a') ->addSelect('at') ->addSelect('ad') @@ -44,12 +45,18 @@ 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) - ->getQuery() + ->setCacheable(true); + + if ($orderByWeight) { + $qb->addOrderBy('a.weight', 'DESC'); + } else { + $qb->addOrderBy('av.position', 'ASC'); + } + + return $qb->getQuery() ->getResult(); } diff --git a/src/Repository/CustomFormAnswerRepository.php b/src/Repository/CustomFormAnswerRepository.php index 3810a296..d7774a73 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 $qb->getQuery()->getSingleScalarResult(); + return (int) $qb->getQuery()->getSingleScalarResult(); } } diff --git a/src/Repository/EntityRepository.php b/src/Repository/EntityRepository.php index ddf6f510..1b0e0223 100644 --- a/src/Repository/EntityRepository.php +++ b/src/Repository/EntityRepository.php @@ -24,6 +24,7 @@ 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; /** @@ -90,6 +91,7 @@ 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)); } @@ -98,10 +100,11 @@ protected function dispatchQueryBuilderEvent(QueryBuilder $qb, string $entityCla * @param string $property * @param mixed $value * - * @return QueryBuilderBuildEvent + * @return Event */ protected function dispatchQueryBuilderBuildEvent(QueryBuilder $qb, string $property, mixed $value): object { + // @phpstan-ignore-next-line return $this->dispatcher->dispatch(new QueryBuilderBuildEvent( $qb, $this->getEntityName(), @@ -114,10 +117,11 @@ protected function dispatchQueryBuilderBuildEvent(QueryBuilder $qb, string $prop /** * @param Query $query * - * @return QueryEvent + * @return Event */ protected function dispatchQueryEvent(Query $query): object { + // @phpstan-ignore-next-line return $this->dispatcher->dispatch(new QueryEvent( $query, $this->getEntityName() @@ -129,10 +133,11 @@ protected function dispatchQueryEvent(Query $query): object * @param string $property * @param mixed $value * - * @return QueryBuilderApplyEvent + * @return Event */ protected function dispatchQueryBuilderApplyEvent(QueryBuilder $qb, string $property, mixed $value): object { + // @phpstan-ignore-next-line return $this->dispatcher->dispatch(new QueryBuilderApplyEvent( $qb, $this->getEntityName(), @@ -257,7 +262,6 @@ public static function getSearchableColumnsNames(ClassMetadataInfo $metadata): a 'childrenOrder', 'childrenOrderDirection', 'password', - 'salt', 'token', 'confirmationToken' ]) @@ -341,12 +345,13 @@ public function searchBy( // Add ordering foreach ($orders as $key => $value) { if ( - str_contains($key, static::NODE_ALIAS . '.') && + (\str_starts_with($key, 'node.') || \str_starts_with($key, static::NODE_ALIAS . '.')) && $this->hasJoinedNode($qb, $alias) ) { + $key = preg_replace('#^node\.#', static::NODE_ALIAS . '.', $key); $qb->addOrderBy($key, $value); } elseif ( - str_contains($key, static::NODESSOURCES_ALIAS . '.') && + \str_starts_with($key, static::NODESSOURCES_ALIAS . '.') && $this->hasJoinedNodesSources($qb, $alias) ) { $qb->addOrderBy($key, $value); @@ -490,7 +495,7 @@ protected function applyFilterByTag(array &$criteria, QueryBuilder $qb): void * * @param QueryBuilder $qb * @param string $alias - * @return boolean + * @return bool */ protected function hasJoinedNode(QueryBuilder $qb, string $alias) { @@ -502,7 +507,7 @@ protected function hasJoinedNode(QueryBuilder $qb, string $alias) * * @param QueryBuilder $qb * @param string $alias - * @return boolean + * @return bool */ protected function hasJoinedNodesSources(QueryBuilder $qb, string $alias) { @@ -514,7 +519,7 @@ protected function hasJoinedNodesSources(QueryBuilder $qb, string $alias) * * @param QueryBuilder $qb * @param string $alias - * @return boolean + * @return bool */ protected function hasJoinedNodeType(QueryBuilder $qb, string $alias) { diff --git a/src/Repository/LogRepository.php b/src/Repository/LogRepository.php index ac633d1f..3419bca8 100644 --- a/src/Repository/LogRepository.php +++ b/src/Repository/LogRepository.php @@ -5,9 +5,12 @@ 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\Log; +use RZ\Roadiz\CoreBundle\Entity\Node; +use RZ\Roadiz\CoreBundle\Entity\NodesSources; +use RZ\Roadiz\CoreBundle\Logger\Entity\Log; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** @@ -30,30 +33,16 @@ 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')) - ->addGroupBy('slog.nodeSource'); + ->andWhere($subQb->expr()->in('slog.entityClass', ':entityClass')) + ->addGroupBy('slog.entityId'); $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() @@ -61,11 +50,7 @@ public function findLatestByNodesSources(int $maxResult = 5): Paginator ->getScalarResult(); $qb2 = $this->createQueryBuilder('log'); - $qb2->addSelect('ns, n, dbf') - ->andWhere($qb2->expr()->in('log.id', ':id')) - ->innerJoin('log.nodeSource', 'ns') - ->leftJoin('ns.documentsByFields', 'dbf') - ->innerJoin('ns.node', 'n') + $qb2->andWhere($qb2->expr()->in('log.id', ':id')) ->orderBy('log.datetime', 'DESC') ->setParameter(':id', array_map(function (array $item) { return $item['id']; @@ -73,4 +58,33 @@ 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 deleted file mode 100644 index 377cb35c..00000000 --- a/src/Repository/LoginAttemptRepository.php +++ /dev/null @@ -1,139 +0,0 @@ - - */ -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 33dfcfa2..914321ae 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\Component\Security\Core\Security; +use Symfony\Component\String\Slugger\AsciiSlugger; +use Symfony\Contracts\EventDispatcher\Event; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** @@ -45,10 +45,11 @@ public function __construct( * @param string $property * @param mixed $value * - * @return object|QueryBuilderBuildEvent + * @return Event */ 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()) ); @@ -59,10 +60,11 @@ protected function dispatchQueryBuilderBuildEvent(QueryBuilder $qb, string $prop * @param string $property * @param mixed $value * - * @return object|QueryBuilderApplyEvent + * @return Event */ 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()) ); @@ -905,15 +907,15 @@ public function findByReverseNodeAndFieldAndTranslation( /** * @param Node $node - * @return array + * @return array */ - public function findAllOffspringIdByNode(Node $node) + public function findAllOffspringIdByNode(Node $node): array { - $theOffprings = []; - $in = [$node->getId()]; + $theOffsprings = []; + $in = \array_filter([(int) $node->getId()]); do { - $theOffprings = array_merge($theOffprings, $in); + $theOffsprings = array_merge($theOffsprings, $in); $subQb = $this->createQueryBuilder('n'); $subQb->select('n.id') ->andWhere($subQb->expr()->in('n.parent', ':tab')) @@ -927,7 +929,7 @@ public function findAllOffspringIdByNode(Node $node) $in[] = (int) $item['id']; } } while (!empty($in)); - return $theOffprings; + return $theOffsprings; } /** @@ -1107,4 +1109,52 @@ 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; + } } diff --git a/src/Repository/NodesSourcesRepository.php b/src/Repository/NodesSourcesRepository.php index dfe4b3d3..a414a6f9 100644 --- a/src/Repository/NodesSourcesRepository.php +++ b/src/Repository/NodesSourcesRepository.php @@ -14,24 +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\SolrSearchResults; use Symfony\Component\Security\Core\Security; +use Symfony\Contracts\EventDispatcher\Event; 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 { @@ -60,10 +60,11 @@ public function __construct( * @param string $property * @param mixed $value * - * @return object|QueryBuilderNodesSourcesBuildEvent + * @return Event */ 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()) ); @@ -74,10 +75,11 @@ protected function dispatchQueryBuilderBuildEvent(QueryBuilder $qb, string $prop * @param string $property * @param mixed $value * - * @return object|QueryBuilderNodesSourcesApplyEvent + * @return Event */ 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()) ); @@ -86,10 +88,11 @@ protected function dispatchQueryBuilderApplyEvent(QueryBuilder $qb, string $prop /** * @param Query $query * - * @return object|QueryNodesSourcesEvent + * @return Event */ protected function dispatchQueryEvent(Query $query): object { + // @phpstan-ignore-next-line return $this->dispatcher->dispatch( new QueryNodesSourcesEvent($query, $this->getEntityName()) ); @@ -147,9 +150,10 @@ 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); @@ -183,6 +187,7 @@ 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); @@ -212,29 +217,17 @@ 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, - 'WITH', - $qb->expr()->lte(static::NODE_ALIAS . '.status', Node::PUBLISHED) - ); - } else { - $qb->andWhere($qb->expr()->lte(static::NODE_ALIAS . '.status', Node::PUBLISHED)); + $qb->innerJoin($prefix . '.node', static::NODE_ALIAS); } + $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, - 'WITH', - $qb->expr()->eq(static::NODE_ALIAS . '.status', Node::PUBLISHED) - ); - } else { - $qb->andWhere($qb->expr()->eq(static::NODE_ALIAS . '.status', Node::PUBLISHED)); + $qb->innerJoin($prefix . '.node', static::NODE_ALIAS); } + $qb->andWhere($qb->expr()->eq(static::NODE_ALIAS . '.status', Node::PUBLISHED)); } return $qb; } @@ -245,8 +238,8 @@ public function alterQueryBuilderWithAuthorizationChecker( * * @param array $criteria * @param array|null $orderBy - * @param integer|null $limit - * @param integer|null $offset + * @param int|null $limit + * @param int|null $offset * @return QueryBuilder */ protected function getContextualQuery( @@ -254,7 +247,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); @@ -267,7 +260,7 @@ protected function getContextualQuery( // Add ordering if (null !== $orderBy) { foreach ($orderBy as $key => $value) { - if (false !== \mb_strpos($key, 'node.')) { + if (\str_contains($key, 'node.')) { $simpleKey = str_replace('node.', '', $key); $qb->addOrderBy(static::NODE_ALIAS . '.' . $simpleKey, $value); } else { @@ -412,7 +405,7 @@ public function findBy( public function findOneBy( array $criteria, array $orderBy = null - ) { + ): ?NodesSources { $qb = $this->getContextualQuery( $criteria, $orderBy, @@ -563,18 +556,20 @@ public function findByTextQuery( * @param int $maxResult * @return Paginator */ - public function findByLatestUpdated($maxResult = 5) + public function findByLatestUpdated(int $maxResult = 5): Paginator { $subQuery = $this->_em->createQueryBuilder(); - $subQuery->select('sns.id') + $subQuery->select('slog.entityId') ->from(Log::class, 'slog') - ->innerJoin(NodesSources::class, 'sns') - ->andWhere($subQuery->expr()->isNotNull('slog.nodeSource')) + ->andWhere($subQuery->expr()->eq('slog.entityClass', ':entityClass')) ->orderBy('slog.datetime', 'DESC'); $query = $this->createQueryBuilder(static::NODESSOURCES_ALIAS); - $query->andWhere($query->expr()->in(static::NODESSOURCES_ALIAS . '.id', $subQuery->getQuery()->getDQL())); - $query->setMaxResults($maxResult); + $query + ->andWhere($query->expr()->in(static::NODESSOURCES_ALIAS . '.id', $subQuery->getQuery()->getDQL())) + ->setParameter(':entityClass', NodesSources::class) + ->setMaxResults($maxResult) + ; return new Paginator($query->getQuery()); } @@ -646,7 +641,7 @@ protected function prepareComparisons(array &$criteria, QueryBuilder $qb, $alias if (!$event->isPropagationStopped()) { $baseKey = $simpleQB->getParameterKey($key); - if (false !== \mb_strpos($key, 'node.nodeType.')) { + if (\str_contains($key, 'node.nodeType.')) { if (!$this->hasJoinedNode($qb, $alias)) { $qb->innerJoin($alias . '.node', static::NODE_ALIAS); } @@ -656,7 +651,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 (false !== \mb_strpos($key, 'node.')) { + } elseif (\str_contains($key, 'node.')) { if (!$this->hasJoinedNode($qb, $alias)) { $qb->innerJoin($alias . '.node', static::NODE_ALIAS); } @@ -752,4 +747,21 @@ 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; + } } diff --git a/src/Repository/PrefixAwareRepository.php b/src/Repository/PrefixAwareRepository.php index 4754a8ab..8db2f7bc 100644 --- a/src/Repository/PrefixAwareRepository.php +++ b/src/Repository/PrefixAwareRepository.php @@ -321,9 +321,11 @@ protected function classicLikeComparison( } foreach ($criteriaFields as $key => $value) { - $realKey = $this->getRealKey($qb, $key); - $fullKey = sprintf('LOWER(%s)', $realKey['prefix'] . $realKey['key']); - $qb->orWhere($qb->expr()->like($fullKey, $qb->expr()->literal($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))); + } } return $qb; } diff --git a/src/Repository/RealmNodeRepository.php b/src/Repository/RealmNodeRepository.php index 0e68b682..4af3e76f 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 $realmId): array + public function findByNodeIdsAndRealmId(array $nodeIds, int|string $realmId): array { $nodeIds = array_filter($nodeIds); if (empty($nodeIds)) { diff --git a/src/Repository/StatusAwareRepository.php b/src/Repository/StatusAwareRepository.php index 87fed3d5..49f35899 100644 --- a/src/Repository/StatusAwareRepository.php +++ b/src/Repository/StatusAwareRepository.php @@ -24,7 +24,7 @@ abstract class StatusAwareRepository extends EntityRepository /** * @param ManagerRegistry $registry - * @param string $entityClass + * @param class-string $entityClass * @param PreviewResolverInterface $previewResolver * @param EventDispatcherInterface $dispatcher * @param Security $security diff --git a/src/Repository/TagRepository.php b/src/Repository/TagRepository.php index 6f8c1f17..7d7c4031 100644 --- a/src/Repository/TagRepository.php +++ b/src/Repository/TagRepository.php @@ -141,7 +141,7 @@ protected function filterByTranslation( $qb->leftJoin('tg.translatedTags', 'tt'); $qb->leftJoin( 'tt.translation', - static::TRANSLATION_ALIAS, + self::TRANSLATION_ALIAS, 'WITH', 't.defaultTranslation = true' ); @@ -608,19 +608,19 @@ protected function prepareComparisons(array &$criteria, QueryBuilder $qb, $alias // Dots are forbidden in field definitions $baseKey = $simpleQB->getParameterKey($key); - if (false !== \mb_strpos($key, 'translation.')) { + if (\str_contains($key, 'translation.')) { /* * Search in translation fields */ $prefix = static::TRANSLATION_ALIAS . '.'; $key = str_replace('translation.', '', $key); - } elseif (false !== \mb_strpos($key, 'nodes.')) { + } elseif (\str_contains($key, 'nodes.')) { /* * Search in node fields */ $prefix = static::NODE_ALIAS . '.'; $key = str_replace('nodes.', '', $key); - } elseif (false !== \mb_strpos($key, 'translatedTag.')) { + } elseif (\str_contains($key, 'translatedTag.')) { /* * Search in translatedTags fields */ diff --git a/src/Repository/UrlAliasRepository.php b/src/Repository/UrlAliasRepository.php index f9728c08..86815976 100644 --- a/src/Repository/UrlAliasRepository.php +++ b/src/Repository/UrlAliasRepository.php @@ -4,6 +4,8 @@ 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; @@ -28,18 +30,21 @@ public function __construct( /** * Get all url aliases linked to given node. * - * @param integer $nodeId + * @param int|string|null $nodeId * - * @return array + * @return iterable */ - public function findAllFromNode($nodeId) + public function findAllFromNode(int|string|null $nodeId): iterable { + 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', (int) $nodeId); + ->setParameter('nodeId', $nodeId); return $query->getResult(); } @@ -48,14 +53,16 @@ public function findAllFromNode($nodeId) * @param string $alias * * @return boolean + * @throws NoResultException + * @throws NonUniqueResultException */ - public function exists($alias) + public function exists(string $alias): bool { $query = $this->_em->createQuery(' SELECT COUNT(ua.alias) FROM RZ\Roadiz\CoreBundle\Entity\UrlAlias ua WHERE ua.alias = :alias') ->setParameter('alias', $alias); - return (bool) $query->getSingleScalarResult(); + return $query->getSingleScalarResult() > 0; } } diff --git a/src/Repository/UserLogEntryRepository.php b/src/Repository/UserLogEntryRepository.php index 44caf056..a3a3a171 100644 --- a/src/Repository/UserLogEntryRepository.php +++ b/src/Repository/UserLogEntryRepository.php @@ -18,7 +18,6 @@ * @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 540b3975..a37d6c1c 100644 --- a/src/RoadizCoreBundle.php +++ b/src/RoadizCoreBundle.php @@ -14,6 +14,7 @@ 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; @@ -40,5 +41,6 @@ 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/DocumentUrlGenerator.php b/src/Routing/DocumentUrlGenerator.php index c6f25df1..e329f2f8 100644 --- a/src/Routing/DocumentUrlGenerator.php +++ b/src/Routing/DocumentUrlGenerator.php @@ -16,7 +16,7 @@ final class DocumentUrlGenerator extends AbstractDocumentUrlGenerator public function __construct( FilesystemOperator $documentsStorage, UrlHelper $urlHelper, - private UrlGeneratorInterface $urlGenerator, + private readonly UrlGeneratorInterface $urlGenerator, CacheItemPoolInterface $optionsCacheAdapter ) { parent::__construct($documentsStorage, $urlHelper, $optionsCacheAdapter); diff --git a/src/Routing/InstallRouteCollection.php b/src/Routing/InstallRouteCollection.php deleted file mode 100644 index 7c86ff93..00000000 --- a/src/Routing/InstallRouteCollection.php +++ /dev/null @@ -1,36 +0,0 @@ -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 9ee32964..30e8710b 100644 --- a/src/Routing/NodePathInfo.php +++ b/src/Routing/NodePathInfo.php @@ -96,12 +96,16 @@ public function setContainsScheme(bool $containsScheme): NodePathInfo */ public function serialize(): string { - return \json_encode([ + $json = \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 f51117d2..dfeec92a 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,24 +54,35 @@ public function __construct( /** * Get controller class path for a given node. * - * @return string + * @return class-string|null */ - public function getController(): string + public function getController(): ?string { if (null === $this->controller) { - $namespace = $this->getControllerNamespace(); - $this->controller = $namespace . '\\' . + if (!$this->node->getNodeType()->isReachable()) { + return null; + } + $controllerClassName = $this->getControllerNamespace() . '\\' . StringHandler::classify($this->node->getNodeType()->getName()) . 'Controller'; - /* - * Use a default controller if no controller was found in Theme. - */ - if (!class_exists($this->controller) && $this->node->getNodeType()->isReachable()) { + 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. + */ $this->controller = $this->defaultControllerClass; } } - + // @phpstan-ignore-next-line return $this->controller; } @@ -79,8 +90,8 @@ protected function getControllerNamespace(): string { $namespace = $this->defaultControllerNamespace; if (null !== $this->theme) { - $refl = new \ReflectionClass($this->theme->getClassName()); - $namespace = $refl->getNamespaceName() . '\\Controllers'; + $reflection = new \ReflectionClass($this->theme->getClassName()); + $namespace = $reflection->getNamespaceName() . '\\Controllers'; } return $namespace; } @@ -94,7 +105,6 @@ public function getMethod(): string * Return FALSE or TRUE if node is viewable. * * @return bool - * @throws \ReflectionException */ public function isViewable(): bool { @@ -109,33 +119,14 @@ 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; - } - /* - * For unpublished nodes - */ - if ($this->node->getStatus() < Node::PUBLISHED) { - if (true === $this->previewResolver->isPreview()) { - return true; - } - /* - * Not allowed to see unpublished nodes - */ - return false; + if ($this->previewResolver->isPreview()) { + return $this->node->isDraft() || $this->node->isPending() || $this->node->isPublished(); } /* * Everyone can view published nodes. */ - return true; + return $this->node->isPublished(); } } diff --git a/src/Routing/NodeUrlMatcher.php b/src/Routing/NodeUrlMatcher.php index 0ef42348..1e1f7b0a 100644 --- a/src/Routing/NodeUrlMatcher.php +++ b/src/Routing/NodeUrlMatcher.php @@ -8,6 +8,7 @@ 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; @@ -21,7 +22,7 @@ final class NodeUrlMatcher extends DynamicUrlMatcher implements NodeUrlMatcherIn { protected PathResolverInterface $pathResolver; /** - * @var class-string + * @var class-string */ private string $defaultControllerClass; @@ -47,7 +48,7 @@ public function getDefaultSupportedFormatExtension(): string * @param PreviewResolverInterface $previewResolver * @param Stopwatch $stopwatch * @param LoggerInterface $logger - * @param class-string $defaultControllerClass + * @param class-string $defaultControllerClass */ public function __construct( PathResolverInterface $pathResolver, diff --git a/src/Routing/NodesSourcesPathResolver.php b/src/Routing/NodesSourcesPathResolver.php index 8fe3f0f5..f40cc067 100644 --- a/src/Routing/NodesSourcesPathResolver.php +++ b/src/Routing/NodesSourcesPathResolver.php @@ -4,6 +4,7 @@ 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; @@ -61,7 +62,7 @@ public function resolvePath( } if ($path === '/') { - $this->stopwatch->start('parseRootPath'); + $this->stopwatch->start('parseRootPath', 'routing'); $translation = $this->parseTranslation(); $nodeSource = $this->getHome($translation); $this->stopwatch->stop('parseRootPath'); @@ -96,13 +97,13 @@ public function resolvePath( } } - $this->stopwatch->start('parseTranslation'); + $this->stopwatch->start('parseTranslation', 'routing'); $translation = $this->parseTranslation($tokens); $this->stopwatch->stop('parseTranslation'); /* * Try with URL Aliases OR nodeName */ - $this->stopwatch->start('parseFromIdentifier'); + $this->stopwatch->start('parseFromIdentifier', 'routing'); $nodeSource = $this->parseFromIdentifier($tokens, $translation, $allowNonReachableNodes); $this->stopwatch->stop('parseFromIdentifier'); } @@ -156,6 +157,7 @@ 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/OptimizedNodesSourcesGraphPathAggregator.php b/src/Routing/OptimizedNodesSourcesGraphPathAggregator.php index e37cdf9d..94af76a0 100644 --- a/src/Routing/OptimizedNodesSourcesGraphPathAggregator.php +++ b/src/Routing/OptimizedNodesSourcesGraphPathAggregator.php @@ -57,7 +57,7 @@ public function aggregatePath(NodesSources $nodesSources, array $parameters = [] /** * @param Node $parent * - * @return array + * @return array */ protected function getParentsIds(Node $parent): array { diff --git a/src/Routing/RedirectableUrlMatcher.php b/src/Routing/RedirectableUrlMatcher.php index a686e08c..b6f3c486 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($path, $route, $scheme = null): array + public function redirect(string $path, string $route, ?string $scheme = null): array { return [ '_controller' => RedirectionController::class . '::redirectToRouteAction', diff --git a/src/Routing/RedirectionMatcher.php b/src/Routing/RedirectionMatcher.php index 3579b0e1..7dcbfe2a 100644 --- a/src/Routing/RedirectionMatcher.php +++ b/src/Routing/RedirectionMatcher.php @@ -4,16 +4,13 @@ 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 @@ -21,34 +18,24 @@ */ final class RedirectionMatcher extends UrlMatcher { - private ManagerRegistry $managerRegistry; - private Stopwatch $stopwatch; private LoggerInterface $logger; + private RedirectionPathResolver $pathResolver; - /** - * @param RequestContext $context - * @param ManagerRegistry $managerRegistry - * @param Stopwatch $stopwatch - * @param LoggerInterface $logger - */ public function __construct( RequestContext $context, - ManagerRegistry $managerRegistry, - Stopwatch $stopwatch, + RedirectionPathResolver $pathResolver, LoggerInterface $logger ) { parent::__construct(new RouteCollection(), $context); - $this->stopwatch = $stopwatch; $this->logger = $logger; - $this->managerRegistry = $managerRegistry; + $this->pathResolver = $pathResolver; } /** * {@inheritdoc} */ - public function match($pathinfo): array + public function match(string $pathinfo): array { - $this->stopwatch->start('findRedirection'); $decodedUrl = rawurldecode($pathinfo); /* @@ -56,14 +43,12 @@ public function match($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)); } @@ -74,6 +59,7 @@ public function match($pathinfo): array */ protected function matchRedirection(string $decodedUrl): ?Redirection { - return $this->managerRegistry->getRepository(Redirection::class)->findOneByQuery($decodedUrl); + $resource = $this->pathResolver->resolvePath($decodedUrl)->getResource(); + return $resource instanceof Redirection ? $resource : null; } } diff --git a/src/Routing/RedirectionPathResolver.php b/src/Routing/RedirectionPathResolver.php index b684f7f4..7842a60d 100644 --- a/src/Routing/RedirectionPathResolver.php +++ b/src/Routing/RedirectionPathResolver.php @@ -5,16 +5,27 @@ 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 { private ManagerRegistry $managerRegistry; + private Stopwatch $stopwatch; + private CacheItemPoolInterface $cacheAdapter; - public function __construct(ManagerRegistry $managerRegistry) - { + public const CACHE_KEY = 'redirection_path_resolver_cache'; + + public function __construct( + ManagerRegistry $managerRegistry, + CacheItemPoolInterface $cacheAdapter, + Stopwatch $stopwatch, + ) { $this->managerRegistry = $managerRegistry; + $this->stopwatch = $stopwatch; + $this->cacheAdapter = $cacheAdapter; } public function resolvePath( @@ -23,15 +34,49 @@ 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) - ->findOneByQuery($path); - + ->find($redirectionId); + $this->stopwatch->stop('findRedirection'); if (null === $redirection) { throw new ResourceNotFoundException(); } - return (new ResourceInfo()) - ->setResource($redirection); + $this->stopwatch->start('incrementRedirection', 'routing'); + $redirection->incrementUseCount(); + $this->managerRegistry->getManagerForClass(Redirection::class)->flush(); + $this->stopwatch->stop('incrementRedirection'); + + return (new ResourceInfo())->setResource($redirection); } } diff --git a/src/SearchEngine/AbstractSearchHandler.php b/src/SearchEngine/AbstractSearchHandler.php index cd2c6f5f..9e12ac68 100644 --- a/src/SearchEngine/AbstractSearchHandler.php +++ b/src/SearchEngine/AbstractSearchHandler.php @@ -11,22 +11,26 @@ 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 { - private ClientRegistry $clientRegistry; + protected ClientRegistry $clientRegistry; protected ObjectManager $em; protected LoggerInterface $logger; + protected EventDispatcherInterface $eventDispatcher; protected int $highlightingFragmentSize = 150; public function __construct( ClientRegistry $clientRegistry, ObjectManager $em, - LoggerInterface $searchEngineLogger + LoggerInterface $searchEngineLogger, + EventDispatcherInterface $eventDispatcher ) { $this->clientRegistry = $clientRegistry; $this->em = $em; $this->logger = $searchEngineLogger; + $this->eventDispatcher = $eventDispatcher; } public function getSolr(): Client @@ -95,7 +99,7 @@ protected function getHighlightingOptions(array &$args = []): array { $tmp = []; $tmp["hl"] = true; - $tmp["hl.fl"] = $this->getCollectionField($args); + $tmp["hl.fl"] = $this->getTitleField($args) . ' ' . $this->getCollectionField($args); $tmp["hl.fragsize"] = $this->getHighlightingFragmentSize(); $tmp["hl.simple.pre"] = ''; $tmp["hl.simple.post"] = ''; @@ -175,7 +179,7 @@ abstract protected function nativeSearch( * ### For node-sources: * * * status (int) - * * visible (boolean) + * * visible (bool) * * 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 +194,7 @@ abstract protected function nativeSearch( * @param string $q * @param array $args * @param int $rows Results per page - * @param boolean $searchTags Search in tags/folders too, even if a node don’t match + * @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 * @@ -242,6 +246,9 @@ protected function getFormattedQuery(string $q, int $proximity = 1): 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) use ($proximity) { /* * Do not fuzz short words: Solr crashes diff --git a/src/SearchEngine/ClientRegistry.php b/src/SearchEngine/ClientRegistry.php index e15699dc..0e9cae6c 100644 --- a/src/SearchEngine/ClientRegistry.php +++ b/src/SearchEngine/ClientRegistry.php @@ -21,10 +21,17 @@ public function __construct(ContainerInterface $container) public function getClient(): ?Client { - return $this->container->get( + $client = $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 0804fbcb..58cb4084 100644 --- a/src/SearchEngine/DocumentSearchHandler.php +++ b/src/SearchEngine/DocumentSearchHandler.php @@ -6,6 +6,7 @@ use RZ\Roadiz\CoreBundle\Entity\Folder; use RZ\Roadiz\CoreBundle\Entity\Translation; +use RZ\Roadiz\CoreBundle\SearchEngine\Event\DocumentSearchQueryEvent; /** * @package RZ\Roadiz\CoreBundle\SearchEngine @@ -16,7 +17,7 @@ class DocumentSearchHandler extends AbstractSearchHandler * @param string $q * @param array $args * @param integer $rows - * @param boolean $searchTags + * @param bool $searchTags * @param integer $proximity Proximity matching: Lucene supports finding words are a within a specific distance away. * @param integer $page * @@ -30,35 +31,40 @@ protected function nativeSearch( int $proximity = 1, int $page = 1 ): ?array { - 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 { + if (empty($q)) { return null; } + $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(), + ]); + + /** @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 new file mode 100644 index 00000000..0b65abda --- /dev/null +++ b/src/SearchEngine/Event/AbstractSearchQueryEvent.php @@ -0,0 +1,36 @@ +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 new file mode 100644 index 00000000..b7d98ec2 --- /dev/null +++ b/src/SearchEngine/Event/DocumentSearchQueryEvent.php @@ -0,0 +1,9 @@ +classname = $classname; diff --git a/src/SearchEngine/Message/Handler/SolrDeleteMessageHandler.php b/src/SearchEngine/Message/Handler/SolrDeleteMessageHandler.php index bf208a6a..587b9680 100644 --- a/src/SearchEngine/Message/Handler/SolrDeleteMessageHandler.php +++ b/src/SearchEngine/Message/Handler/SolrDeleteMessageHandler.php @@ -25,6 +25,8 @@ 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 15b092f7..af3e006c 100644 --- a/src/SearchEngine/Message/Handler/SolrReindexMessageHandler.php +++ b/src/SearchEngine/Message/Handler/SolrReindexMessageHandler.php @@ -25,6 +25,8 @@ 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 3e41672a..bc4dc90d 100644 --- a/src/SearchEngine/NodeSourceSearchHandler.php +++ b/src/SearchEngine/NodeSourceSearchHandler.php @@ -9,6 +9,7 @@ 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 @@ -37,49 +38,54 @@ protected function nativeSearch( int $proximity = 1, int $page = 1 ): ?array { - if (!empty($q)) { - $query = $this->createSolrQuery($args, $rows, $page); - $queryTxt = $this->buildQuery($q, $args, $searchTags, $proximity); + if (empty($q)) { + return null; + } + $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; - } + 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); + $query->setQuery($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', - ]); + /* + * 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', + ]); - $this->logger->debug('[Solr] Request node-sources search…', [ - 'query' => $queryTxt, - 'fq' => $args["fq"] ?? [], - 'params' => $query->getParams(), - ]); + $this->logger->debug('[Solr] Request node-sources search…', [ + 'query' => $queryTxt, + 'fq' => $args["fq"] ?? [], + 'params' => $query->getParams(), + ]); - $solrRequest = $this->getSolr()->execute($query); - return $solrRequest->getData(); - } else { - return null; - } + /** @var NodeSourceSearchQueryEvent $event */ + $event = $this->eventDispatcher->dispatch( + new NodeSourceSearchQueryEvent($query, $args) + ); + $query = $event->getQuery(); + + $solrRequest = $this->getSolr()->execute($query); + return $solrRequest->getData(); } /** @@ -162,15 +168,15 @@ protected function argFqProcess(array &$args): array */ if (isset($args['publishedAt'])) { $tmp = "published_at_dt:"; - if (!is_array($args['publishedAt']) && $args['publishedAt'] instanceof \DateTime) { + if (!is_array($args['publishedAt']) && $args['publishedAt'] instanceof \DateTimeInterface) { $tmp .= $this->formatDateTimeToUTC($args['publishedAt']); } elseif ( isset($args['publishedAt'][0]) && $args['publishedAt'][0] === "BETWEEN" && isset($args['publishedAt'][1]) && - $args['publishedAt'][1] instanceof \DateTime && + $args['publishedAt'][1] instanceof \DateTimeInterface && isset($args['publishedAt'][2]) && - $args['publishedAt'][2] instanceof \DateTime + $args['publishedAt'][2] instanceof \DateTimeInterface ) { $tmp .= "[" . $this->formatDateTimeToUTC($args['publishedAt'][1]) . @@ -180,14 +186,14 @@ protected function argFqProcess(array &$args): array isset($args['publishedAt'][0]) && $args['publishedAt'][0] === "<=" && isset($args['publishedAt'][1]) && - $args['publishedAt'][1] instanceof \DateTime + $args['publishedAt'][1] instanceof \DateTimeInterface ) { $tmp .= "[* TO " . $this->formatDateTimeToUTC($args['publishedAt'][1]) . "]"; } elseif ( isset($args['publishedAt'][0]) && $args['publishedAt'][0] === ">=" && isset($args['publishedAt'][1]) && - $args['publishedAt'][1] instanceof \DateTime + $args['publishedAt'][1] instanceof \DateTimeInterface ) { $tmp .= "[" . $this->formatDateTimeToUTC($args['publishedAt'][1]) . " TO *]"; } diff --git a/src/SearchEngine/SolariumDocumentTranslation.php b/src/SearchEngine/SolariumDocumentTranslation.php index 9f0fad0a..8ea8b2fa 100644 --- a/src/SearchEngine/SolariumDocumentTranslation.php +++ b/src/SearchEngine/SolariumDocumentTranslation.php @@ -46,15 +46,16 @@ public function getDocumentId(): int|string public function getFieldsAssoc(bool $subResource = false): array { $event = new DocumentTranslationIndexingEvent($this->documentTranslation, [], $this); - - return $this->dispatcher->dispatch($event)->getAssociations(); + /** @var DocumentTranslationIndexingEvent $event */ + $event = $this->dispatcher->dispatch($event); + return $event->getAssociations(); } /** * Remove any document linked to current node-source. * * @param Query $update - * @return boolean + * @return bool */ public function clean(Query $update): bool { diff --git a/src/SearchEngine/SolariumLogger.php b/src/SearchEngine/SolariumLogger.php new file mode 100644 index 00000000..120b0a67 --- /dev/null +++ b/src/SearchEngine/SolariumLogger.php @@ -0,0 +1,187 @@ +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 cf21bdf5..0c9ecf23 100644 --- a/src/SearchEngine/SolariumNodeSource.php +++ b/src/SearchEngine/SolariumNodeSource.php @@ -51,8 +51,9 @@ public function getDocumentId(): int|string public function getFieldsAssoc(bool $subResource = false): array { $event = new NodesSourcesIndexingEvent($this->nodeSource, [], $this); - - return $this->dispatcher->dispatch($event)->getAssociations(); + /** @var NodesSourcesIndexingEvent $event */ + $event = $this->dispatcher->dispatch($event); + return $event->getAssociations(); } /** diff --git a/src/SearchEngine/SolrSearchResults.php b/src/SearchEngine/SolrSearchResults.php index fe74fc47..056653a6 100644 --- a/src/SearchEngine/SolrSearchResults.php +++ b/src/SearchEngine/SolrSearchResults.php @@ -104,24 +104,18 @@ function ($item) { } /** - * Merge collection_txt localized fields. + * Get highlighting for one field. + * This do not merge highlighting for all fields anymore. * * @param string $id - * @return array|array[]|mixed + * @return array */ - protected function getHighlighting(string $id): mixed + protected function getHighlighting(string $id): array { - $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 - ]); + if (isset($this->response['highlighting'][$id]) && \is_array($this->response['highlighting'][$id])) { + return $this->response['highlighting'][$id]; } - return $highlights; + return []; } /** diff --git a/src/SearchEngine/Subscriber/AbstractIndexingSubscriber.php b/src/SearchEngine/Subscriber/AbstractIndexingSubscriber.php index 12aa92b0..3bb5c452 100644 --- a/src/SearchEngine/Subscriber/AbstractIndexingSubscriber.php +++ b/src/SearchEngine/Subscriber/AbstractIndexingSubscriber.php @@ -4,14 +4,63 @@ 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 new file mode 100644 index 00000000..929a85e9 --- /dev/null +++ b/src/SearchEngine/Subscriber/AttributeValueIndexingSubscriber.php @@ -0,0 +1,125 @@ + ['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 66de4921..a4da9db0 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] = implode(PHP_EOL, $collection); + $assoc['collection_txt_' . $lang] = $this->flattenTextCollection($collection); $event->setAssociations($assoc); } } diff --git a/src/SearchEngine/Subscriber/DefaultNodesSourcesIndexingSubscriber.php b/src/SearchEngine/Subscriber/DefaultNodesSourcesIndexingSubscriber.php index 0077e7ed..70a7ae00 100644 --- a/src/SearchEngine/Subscriber/DefaultNodesSourcesIndexingSubscriber.php +++ b/src/SearchEngine/Subscriber/DefaultNodesSourcesIndexingSubscriber.php @@ -4,12 +4,11 @@ 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 @@ -32,18 +31,10 @@ 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[SolariumNodeSource::TYPE_DISCRIMINATOR] = SolariumNodeSource::DOCUMENT_TYPE; + $assoc[AbstractSolarium::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(); @@ -57,17 +48,23 @@ 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; } @@ -113,18 +110,61 @@ 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; - } - $criteria = new Criteria(); - $criteria->andWhere(Criteria::expr()->eq("type", AbstractField::BOOLEAN_T)); - $booleanFields = $node->getNodeType()->getFields()->matching($criteria); + $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); + + /* + * 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); + } - /** @var NodeTypeField $booleanField */ - foreach ($booleanFields as $booleanField) { - $name = $booleanField->getName(); - $name .= '_b'; - $getter = $booleanField->getGetterName(); - $assoc[$name] = $nodeSource->$getter(); + /* + * 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); + } } $searchableFields = $node->getNodeType()->getSearchableFields(); @@ -156,10 +196,32 @@ function (Tag $tag) { */ $assoc['collection_txt'] = $collection; // Compile all text content into a single localized text field. - $assoc['collection_txt_' . $lang] = implode(PHP_EOL, $collection); + $assoc['collection_txt_' . $lang] = $this->flattenTextCollection($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 diff --git a/src/SearchEngine/Subscriber/TreeWalkerIndexingEventSubscriber.php b/src/SearchEngine/Subscriber/TreeWalkerIndexingEventSubscriber.php new file mode 100644 index 00000000..76ccc9ef --- /dev/null +++ b/src/SearchEngine/Subscriber/TreeWalkerIndexingEventSubscriber.php @@ -0,0 +1,100 @@ +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 deleted file mode 100644 index 5e1e7ad1..00000000 --- a/src/Security/Authentication/Manager/LoginAttemptManager.php +++ /dev/null @@ -1,160 +0,0 @@ -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 0d4673d4..c3708ca0 100644 --- a/src/Security/Authentication/RoadizAuthenticator.php +++ b/src/Security/Authentication/RoadizAuthenticator.php @@ -117,7 +117,7 @@ private function getCredentials(Request $request): array throw new BadRequestHttpException(sprintf('The key "%s" must be a string.', $this->usernamePath)); } - if (\strlen($credentials['username']) > Security::MAX_USERNAME_LENGTH) { + if (\mb_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 adf23d44..ff05c9e3 100644 --- a/src/Security/Authorization/AccessDeniedHandler.php +++ b/src/Security/Authorization/AccessDeniedHandler.php @@ -44,7 +44,7 @@ public function __construct( } /** - * Handles an access denied failure redirecting to home page + * Handles access denied failure redirecting to home page * * @param Request $request * @param AccessDeniedException $accessDeniedException diff --git a/src/Security/Authorization/Voter/NodeTypeFieldVoter.php b/src/Security/Authorization/Voter/NodeTypeFieldVoter.php index 4deaa5cf..9789d022 100644 --- a/src/Security/Authorization/Voter/NodeTypeFieldVoter.php +++ b/src/Security/Authorization/Voter/NodeTypeFieldVoter.php @@ -15,7 +15,7 @@ final class NodeTypeFieldVoter extends Voter public const VIEW = 'VIEW'; public function __construct( - private Security $security + private readonly Security $security ) { } diff --git a/src/Security/Authorization/Voter/RealmVoter.php b/src/Security/Authorization/Voter/RealmVoter.php index 7e61ec29..c7d31b0b 100644 --- a/src/Security/Authorization/Voter/RealmVoter.php +++ b/src/Security/Authorization/Voter/RealmVoter.php @@ -44,15 +44,12 @@ protected function supports(string $attribute, $subject): bool */ public function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool { - 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; + 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, + }; } /** @@ -61,7 +58,7 @@ public function voteOnAttribute(string $attribute, $subject, TokenInterface $tok * @param TokenInterface $token * @return bool */ - private function voteForRole(string $attribute, $subject, TokenInterface $token): bool + private function voteForRole(string $attribute, RealmInterface $subject, TokenInterface $token): bool { if (null === $role = $subject->getRole()) { return false; @@ -75,7 +72,7 @@ private function voteForRole(string $attribute, $subject, TokenInterface $token) * @param TokenInterface $token * @return bool */ - private function voteForUser(string $attribute, $subject, TokenInterface $token): bool + private function voteForUser(string $attribute, RealmInterface $subject, TokenInterface $token): bool { if ($subject->getUsers()->count() === 0 || null === $token->getUser()) { return false; @@ -91,7 +88,7 @@ private function voteForUser(string $attribute, $subject, TokenInterface $token) * @param TokenInterface $token * @return bool */ - private function voteForPassword(string $attribute, $subject, TokenInterface $token): bool + private function voteForPassword(string $attribute, RealmInterface $subject, TokenInterface $token): bool { $request = $this->requestStack->getCurrentRequest(); if (null === $request || empty($subject->getPlainPassword())) { diff --git a/src/Security/User/UserViewer.php b/src/Security/User/UserViewer.php index e8681701..c6c07db9 100644 --- a/src/Security/User/UserViewer.php +++ b/src/Security/User/UserViewer.php @@ -6,11 +6,10 @@ 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\TransportException; +use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Contracts\Translation\TranslatorInterface; @@ -21,7 +20,6 @@ class UserViewer protected TranslatorInterface $translator; protected EmailManager $emailManager; protected LoggerInterface $logger; - protected ?User $user = null; public function __construct( Settings $settingsBag, @@ -40,21 +38,20 @@ public function __construct( /** * Send email to reset user password. * - * @param string|NodesSources $route + * @param User $user + * @param object|string $route * @param string $htmlTemplate * @param string $txtTemplate * * @return bool - * @throws \Exception + * @throws TransportExceptionInterface */ public function sendPasswordResetLink( - $route = 'loginResetPage', + User $user, + object|string $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(); @@ -62,7 +59,7 @@ public function sendPasswordResetLink( $resetLink = $this->urlGenerator->generate( $route, [ - 'token' => $this->user->getConfirmationToken(), + 'token' => $user->getConfirmationToken(), ], UrlGeneratorInterface::ABSOLUTE_URL ); @@ -71,14 +68,14 @@ public function sendPasswordResetLink( RouteObjectInterface::OBJECT_BASED_ROUTE_NAME, [ RouteObjectInterface::ROUTE_OBJECT => $route, - 'token' => $this->user->getConfirmationToken(), + 'token' => $user->getConfirmationToken(), ], UrlGeneratorInterface::ABSOLUTE_URL ); } $this->emailManager->setAssignation([ 'resetLink' => $resetLink, - 'user' => $this->user, + 'user' => $user, 'site' => $siteName, 'mailContact' => $emailContact, ]); @@ -87,18 +84,20 @@ 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 (TransportException $e) { + } catch (\Exception $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; } @@ -129,22 +128,4 @@ 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 eaec4ef9..d53c7666 100644 --- a/src/Serializer/CircularReferenceHandler.php +++ b/src/Serializer/CircularReferenceHandler.php @@ -4,7 +4,9 @@ namespace RZ\Roadiz\CoreBundle\Serializer; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; +use ApiPlatform\Api\UrlGeneratorInterface; +use ApiPlatform\Metadata\Operation; final class CircularReferenceHandler { @@ -18,14 +20,15 @@ public function __construct(IriConverterInterface $iriConverter) $this->iriConverter = $iriConverter; } - /** - * @param mixed $object - * @return string - */ - public function __invoke($object, string $format = null, array $context = []) + public function __invoke(mixed $object, string $format, array $context): ?string { try { - return $this->iriConverter->getIriFromItem($object); + return $this->iriConverter->getIriFromResource( + $object, + UrlGeneratorInterface::ABS_PATH, + null, + $context + ); } catch (\InvalidArgumentException $exception) { if (is_object($object) && method_exists($object, 'getId')) { return (string) $object->getId(); diff --git a/src/Serializer/Normalizer/AttributeValueNormalizer.php b/src/Serializer/Normalizer/AttributeValueNormalizer.php index 543d865d..ed4d34c9 100644 --- a/src/Serializer/Normalizer/AttributeValueNormalizer.php +++ b/src/Serializer/Normalizer/AttributeValueNormalizer.php @@ -31,15 +31,25 @@ public function normalize($object, $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) { + if ( + $translatedData instanceof AttributeValueTranslationInterface && + $translatedData->getValue() !== null + ) { $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 114414ea..e818d442 100644 --- a/src/Serializer/Normalizer/CustomFormNormalizer.php +++ b/src/Serializer/Normalizer/CustomFormNormalizer.php @@ -5,7 +5,6 @@ namespace RZ\Roadiz\CoreBundle\Serializer\Normalizer; use RZ\Roadiz\CoreBundle\Entity\CustomForm; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; use Symfony\Component\String\Slugger\AsciiSlugger; /** diff --git a/src/Serializer/Normalizer/DocumentNormalizer.php b/src/Serializer/Normalizer/DocumentNormalizer.php index 918e6aee..2f783e7c 100644 --- a/src/Serializer/Normalizer/DocumentNormalizer.php +++ b/src/Serializer/Normalizer/DocumentNormalizer.php @@ -8,7 +8,6 @@ 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; @@ -61,6 +60,14 @@ public function normalize($object, $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/TranslationAwareNormalizer.php b/src/Serializer/Normalizer/TranslationAwareNormalizer.php index a0441cc9..583afe2f 100644 --- a/src/Serializer/Normalizer/TranslationAwareNormalizer.php +++ b/src/Serializer/Normalizer/TranslationAwareNormalizer.php @@ -82,13 +82,15 @@ private function getTranslationFromLocale(string $locale): ?TranslationInterface private function getTranslationFromRequest(): ?TranslationInterface { $request = $this->requestStack->getMainRequest(); - if ( - null !== $request && - null !== $translation = $this->getTranslationFromLocale( - $request->query->get('_locale', $request->getLocale()) - ) - ) { - return $translation; + + if (null !== $request) { + $locale = $request->query->get('_locale', $request->getLocale()); + if ( + \is_string($locale) && + null !== $translation = $this->getTranslationFromLocale($locale) + ) { + return $translation; + } } return $this->managerRegistry diff --git a/src/Serializer/ObjectConstructor/ChainDoctrineObjectConstructor.php b/src/Serializer/ObjectConstructor/ChainDoctrineObjectConstructor.php index 5e3fc28a..d41adee3 100644 --- a/src/Serializer/ObjectConstructor/ChainDoctrineObjectConstructor.php +++ b/src/Serializer/ObjectConstructor/ChainDoctrineObjectConstructor.php @@ -57,9 +57,11 @@ public function construct( // Locate possible ClassMetadata $classMetadataFactory = $this->entityManager->getMetadataFactory(); + /** @var class-string $className */ + $className = $metadata->name; try { - $doctrineMetadata = $classMetadataFactory->getMetadataFor($metadata->name); - if ($doctrineMetadata->getName() !== $metadata->name) { + $doctrineMetadata = $classMetadataFactory->getMetadataFor($className); + if ($doctrineMetadata->getName() !== $className) { /* * Doctrine resolveTargetEntity has found an alternative class */ @@ -69,7 +71,9 @@ public function construct( // Object class is not a valid doctrine entity } - if ($classMetadataFactory->isTransient($metadata->name)) { + /** @var class-string $className */ + $className = $metadata->name; + if ($classMetadataFactory->isTransient($className)) { // No ClassMetadata found, proceed with normal deserialization return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context); } @@ -77,12 +81,12 @@ public function construct( // Managed entity, check for proxy load if (!\is_array($data)) { // Single identifier, load proxy - return $this->entityManager->getReference($metadata->name, $data); + return $this->entityManager->getReference($className, $data); } /** @var TypedObjectConstructorInterface $typedObjectConstructor */ foreach ($this->typedObjectConstructors as $typedObjectConstructor) { - if ($typedObjectConstructor->supports($metadata->name, $data)) { + if ($typedObjectConstructor->supports($className, $data)) { return $typedObjectConstructor->construct( $visitor, $metadata, @@ -93,10 +97,6 @@ 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 = []; diff --git a/src/Serializer/ObjectConstructor/NodeTypeObjectConstructor.php b/src/Serializer/ObjectConstructor/NodeTypeObjectConstructor.php index 71d96815..a3a53084 100644 --- a/src/Serializer/ObjectConstructor/NodeTypeObjectConstructor.php +++ b/src/Serializer/ObjectConstructor/NodeTypeObjectConstructor.php @@ -6,6 +6,8 @@ use JMS\Serializer\DeserializationContext; use JMS\Serializer\Exception\ObjectConstructionException; +use JMS\Serializer\Metadata\ClassMetadata; +use JMS\Serializer\Visitor\DeserializationVisitorInterface; use RZ\Roadiz\CoreBundle\Entity\NodeType; class NodeTypeObjectConstructor extends AbstractTypedObjectConstructor @@ -18,6 +20,38 @@ public function supports(string $className, array $data): bool return $className === NodeType::class && array_key_exists('name', $data); } + public function construct( + DeserializationVisitorInterface $visitor, + ClassMetadata $metadata, + $data, + array $type, + DeserializationContext $context + ): ?object { + $nodeType = parent::construct($visitor, $metadata, $data, $type, $context); + + if ($nodeType instanceof NodeType && \is_array($data) && \array_key_exists('fields', $data)) { + $nodeType = $this->removeExtraFields($nodeType, $data); + } + + return $nodeType; + } + + protected function removeExtraFields(NodeType $nodeType, array $data): NodeType + { + $fieldsName = array_map(function ($field) { + return $field['name']; + }, $data['fields']); + + foreach ($nodeType->getFields() as $field) { + if (!\in_array($field->getName(), $fieldsName)) { + $nodeType->getFields()->removeElement($field); + $field->setNodeType(null); + } + } + + return $nodeType; + } + /** * @inheritDoc */ diff --git a/src/Serializer/TranslationAwareContextBuilder.php b/src/Serializer/TranslationAwareContextBuilder.php index e12cc612..edf63081 100644 --- a/src/Serializer/TranslationAwareContextBuilder.php +++ b/src/Serializer/TranslationAwareContextBuilder.php @@ -4,7 +4,7 @@ namespace RZ\Roadiz\CoreBundle\Serializer; -use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface; +use ApiPlatform\Serializer\SerializerContextBuilderInterface; use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; @@ -37,15 +37,16 @@ public function createFromRequest(Request $request, bool $normalization, array $ /** @var TranslationRepository $repository */ $repository = $this->managerRegistry ->getRepository(TranslationInterface::class); + $locale = $request->query->get('_locale', $request->getLocale()); + + if (!\is_string($locale)) { + return $context; + } if ($this->previewResolver->isPreview()) { - $translation = $repository->findOneByLocaleOrOverrideLocale( - $request->query->get('_locale', $request->getLocale()) - ); + $translation = $repository->findOneByLocaleOrOverrideLocale($locale); } else { - $translation = $repository->findOneAvailableByLocaleOrOverrideLocale( - $request->query->get('_locale', $request->getLocale()) - ); + $translation = $repository->findOneAvailableByLocaleOrOverrideLocale($locale); } if ($translation instanceof TranslationInterface) { diff --git a/src/Traits/LoginRequestTrait.php b/src/Traits/LoginRequestTrait.php index d804a62d..19ea1b16 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,10 +52,9 @@ public function sendConfirmationEmail( $user->setConfirmationToken($tokenGenerator->generateToken()); $entityManager->flush(); $userViewer = $this->getUserViewer(); - $userViewer->setUser($user); - $userViewer->sendPasswordResetLink($resetRoute); + $userViewer->sendPasswordResetLink($user, $resetRoute); return true; - } catch (\Exception $e) { + } catch (\Throwable $e) { $user->setPasswordRequestedAt(null); $user->setConfirmationToken(null); $entityManager->flush(); diff --git a/src/TwigExtension/LogExtension.php b/src/TwigExtension/LogExtension.php new file mode 100644 index 00000000..8d39a699 --- /dev/null +++ b/src/TwigExtension/LogExtension.php @@ -0,0 +1,117 @@ +security = $security; + $this->urlGenerator = $urlGenerator; + } + + public function getFunctions(): array + { + return [ + new TwigFunction('log_entity_edit_path', [$this, 'getEditPath'], ['is_safe_callback' => [$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 59a2b90e..61726ab0 100644 --- a/src/TwigExtension/NodesSourcesExtension.php +++ b/src/TwigExtension/NodesSourcesExtension.php @@ -76,10 +76,10 @@ public function getTests(): array * @param NodesSources|null $ns * @param array|null $criteria * @param array|null $order - * @return array + * @return iterable * @throws RuntimeError */ - public function getChildren(NodesSources $ns = null, array $criteria = null, array $order = null) + public function getChildren(NodesSources $ns = null, array $criteria = null, array $order = null): iterable { if (null === $ns) { if ($this->throwExceptions) { diff --git a/src/Workflow/Event/NodeStatusGuardListener.php b/src/Workflow/Event/NodeStatusGuardListener.php index b9550d93..92de5153 100644 --- a/src/Workflow/Event/NodeStatusGuardListener.php +++ b/src/Workflow/Event/NodeStatusGuardListener.php @@ -5,6 +5,7 @@ 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\Component\Security\Core\Security; use Symfony\Component\Workflow\Event\GuardEvent; @@ -37,7 +38,7 @@ public static function getSubscribedEvents(): array public function guard(GuardEvent $event): void { - if (!$this->security->isGranted('ROLE_ACCESS_NODES')) { + if (!$this->security->isGranted(NodeVoter::EDIT_CONTENT, $event->getSubject())) { $event->addTransitionBlocker(new TransitionBlocker( 'User is not allowed to edit this node.', '1' @@ -47,7 +48,7 @@ public function guard(GuardEvent $event): void public function guardPublish(GuardEvent $event): void { - if (!$this->security->isGranted('ROLE_ACCESS_NODES_STATUS')) { + if (!$this->security->isGranted(NodeVoter::EDIT_STATUS, $event->getSubject())) { $event->addTransitionBlocker(new TransitionBlocker( 'User is not allowed to publish this node.', '1' @@ -65,7 +66,7 @@ public function guardArchive(GuardEvent $event): void '1' )); } - if (!$this->security->isGranted('ROLE_ACCESS_NODES_STATUS')) { + if (!$this->security->isGranted(NodeVoter::EDIT_STATUS, $event->getSubject())) { $event->addTransitionBlocker(new TransitionBlocker( 'User is not allowed to archive this node.', '1' @@ -83,7 +84,7 @@ public function guardDelete(GuardEvent $event): void '1' )); } - if (!$this->security->isGranted('ROLE_ACCESS_NODES_DELETE')) { + if (!$this->security->isGranted(NodeVoter::DELETE, $event->getSubject())) { $event->addTransitionBlocker(new TransitionBlocker( 'User is not allowed to delete this node.', '1' diff --git a/src/Xlsx/XlsxExporter.php b/src/Xlsx/XlsxExporter.php index 20988aa7..633d7017 100644 --- a/src/Xlsx/XlsxExporter.php +++ b/src/Xlsx/XlsxExporter.php @@ -85,7 +85,10 @@ public function exportXlsx($data, $keys = []) foreach ($headerkeys as $key => $value) { $columnAlpha = Coordinate::stringFromColumnIndex($key + 1); $activeSheet->getStyle($columnAlpha . $activeRow)->applyFromArray($headerStyles); - $activeSheet->setCellValueByColumnAndRow($key + 1, $activeRow, $this->translator->trans($value)); + if (\is_string($value)) { + $value = $this->translator->trans($value); + } + $activeSheet->setCellValueByColumnAndRow($key + 1, $activeRow, $value); } $activeRow++; } @@ -104,7 +107,7 @@ public function exportXlsx($data, $keys = []) continue; } - if ($value instanceof \DateTime) { + if ($value instanceof \DateTimeInterface) { $value = Date::PHPToExcel($value); $activeSheet->getStyle($columnAlpha . ($activeRow)) ->getNumberFormat() @@ -134,6 +137,11 @@ public function exportXlsx($data, $keys = []) $writer = new Xlsx($spreadsheet); ob_start(); $writer->save('php://output'); - return ob_get_clean(); + $output = ob_get_clean(); + + if (!\is_string($output)) { + throw new \RuntimeException('Output is not a string.'); + } + return $output; } } diff --git a/templates/DataCollector/solarium.html.twig b/templates/DataCollector/solarium.html.twig new file mode 100644 index 00000000..5a26a650 --- /dev/null +++ b/templates/DataCollector/solarium.html.twig @@ -0,0 +1,81 @@ +{# @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 new file mode 100644 index 00000000..2f16b911 --- /dev/null +++ b/templates/DataCollector/solr.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/templates/customForm/customForm.html.twig b/templates/customForm/customForm.html.twig index 445238db..86d9b857 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 2fc13c63..a6cf6428 100644 --- a/templates/email/forms/answerForm.html.twig +++ b/templates/email/forms/answerForm.html.twig @@ -3,6 +3,9 @@ {% block title %}{{ title }}{% endblock %} {% block content_table %} +{% if requestLocale is not defined %} + {% set requestLocale = app.request.locale %} +{% endif %}
@@ -22,10 +25,10 @@ {% for field in fields %} - +
{{ field.label|default(field.name|trans) }}{{ field.label|default(field.name|trans(locale=requestLocale)) }} {% if field.name == "submittedAt" %} - {{ field.value|format_datetime("medium", "short", locale=app.request.locale) }} + {{ field.value|format_datetime("medium", "short", locale=requestLocale) }} {% else %} {{ field.value }} {% endif %} diff --git a/templates/email/forms/answerForm.txt.twig b/templates/email/forms/answerForm.txt.twig index 762450bc..3d9eba19 100644 --- a/templates/email/forms/answerForm.txt.twig +++ b/templates/email/forms/answerForm.txt.twig @@ -3,11 +3,15 @@ {% 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) }}: {% if field.name == "submittedAt" %}{{ field.value|format_datetime("medium", "short", locale=app.request.locale) }}{% else %}{{ field.value }}{% endif %} + +{{ field.label|default(field.name|trans(locale=requestLocale)) }}: {% if field.name == "submittedAt" %}{{ field.value|format_datetime("medium", "short", locale=requestLocale) }}{% else %}{{ field.value }}{% endif %} {% endfor %} {% endblock %} diff --git a/translations/core/messages.en.xlf b/translations/core/messages.en.xlf index 186ea829..760ae856 100644 --- a/translations/core/messages.en.xlf +++ b/translations/core/messages.en.xlf @@ -157,6 +157,24 @@ 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 c71a735b..0210b7a4 100644 --- a/translations/core/messages.fr.xlf +++ b/translations/core/messages.fr.xlf @@ -157,6 +157,24 @@ 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 586b3624..ecf881ff 100644 --- a/translations/core/messages.xlf +++ b/translations/core/messages.xlf @@ -166,6 +166,24 @@ + + 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 new file mode 100644 index 00000000..972f39e0 --- /dev/null +++ b/translations/security.ar.xlf @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/translations/security.de.xlf b/translations/security.de.xlf new file mode 100644 index 00000000..5d46fc4b --- /dev/null +++ b/translations/security.de.xlf @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/translations/security.en.xlf b/translations/security.en.xlf index 76189574..92492801 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 new file mode 100644 index 00000000..73274f66 --- /dev/null +++ b/translations/security.es.xlf @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/translations/security.fr.xlf b/translations/security.fr.xlf index 1b6f7a74..bc8f9ad4 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 d'identification ont expiré. Veuillez demander un nouveau mot de passe. + Vos informations de connexion 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 new file mode 100644 index 00000000..10b490eb --- /dev/null +++ b/translations/security.id.xlf @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/translations/security.it.xlf b/translations/security.it.xlf new file mode 100644 index 00000000..1c087246 --- /dev/null +++ b/translations/security.it.xlf @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/translations/security.ru.xlf b/translations/security.ru.xlf new file mode 100644 index 00000000..bdafc723 --- /dev/null +++ b/translations/security.ru.xlf @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/translations/security.sr.xlf b/translations/security.sr.xlf new file mode 100644 index 00000000..a24d74fa --- /dev/null +++ b/translations/security.sr.xlf @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/translations/security.tr.xlf b/translations/security.tr.xlf new file mode 100644 index 00000000..fe017043 --- /dev/null +++ b/translations/security.tr.xlf @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/translations/security.uk.xlf b/translations/security.uk.xlf new file mode 100644 index 00000000..f37323e8 --- /dev/null +++ b/translations/security.uk.xlf @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/translations/security.zh.xlf b/translations/security.zh.xlf new file mode 100644 index 00000000..6b49359a --- /dev/null +++ b/translations/security.zh.xlf @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index 62c27ea0..c10b9005 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -10,6 +10,12 @@ tagName.%name%.alreadyExists Tag %name% already exists. + + + + field_with_same_name_already_exists_but_with_different_doctrine_type + There already is a "%name%" field inside "%nodeTypeName%" but with a different data-type: %type%. + diff --git a/translations/validators.fr.xlf b/translations/validators.fr.xlf index 0daaf454..9e538c0e 100644 --- a/translations/validators.fr.xlf +++ b/translations/validators.fr.xlf @@ -10,6 +10,11 @@ tagName.%name%.alreadyExists L'étiquette «%name%» existe déjà. + + + field_with_same_name_already_exists_but_with_different_doctrine_type + Un champ « %name% » existe déjà pour « %nodeTypeName% » mais avec un type de données différent : %type%. + diff --git a/translations/validators.xlf b/translations/validators.xlf index 82fbfc32..413ebfd4 100644 --- a/translations/validators.xlf +++ b/translations/validators.xlf @@ -10,6 +10,10 @@ tagName.%name%.alreadyExists + + field_with_same_name_already_exists_but_with_different_doctrine_type + +