diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 08e37fdeb4..ae1f767165 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -89,8 +89,8 @@ jobs: TOKEN: ${{ secrets.EZROBOT_PAT }} run: | curl -H "Authorization: token $TOKEN" -L https://github.com/ibexa/vale-styles/archive/refs/heads/main.zip -o vale.zip + rm -rf tests unzip vale.zip - rm vale.zip mv vale-styles-main/* vale-styles-main/.vale.ini . - name: Run Vale.sh diff --git a/.github/workflows/code_samples.yaml b/.github/workflows/code_samples.yaml index d410a86455..74f593675b 100644 --- a/.github/workflows/code_samples.yaml +++ b/.github/workflows/code_samples.yaml @@ -53,6 +53,9 @@ jobs: - name: Run Rector check run: composer check-rector + - name: Run PHPUnit tests + run: composer phpunit + code-samples-inclusion-check: name: Check code samples inclusion runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index d243a010bd..10f1a32e27 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ auth.json yarn.lock docs/css/*.map .deptrac.cache +.phpunit.result.cache diff --git a/code_samples/back_office/components/twig_components.yaml b/code_samples/back_office/components/twig_components.yaml index 6e0458f4d0..20d8c3caa2 100644 --- a/code_samples/back_office/components/twig_components.yaml +++ b/code_samples/back_office/components/twig_components.yaml @@ -12,7 +12,6 @@ ibexa_twig_components: priority: 0 arguments: content: 'Hello world!' - admin-ui-user-menu: duplicated_user_menu: type: menu arguments: diff --git a/code_samples/back_office/online_editor/config/packages/custom_plugin.yaml b/code_samples/back_office/online_editor/config/packages/custom_plugin.yaml deleted file mode 100644 index 426b9b75b9..0000000000 --- a/code_samples/back_office/online_editor/config/packages/custom_plugin.yaml +++ /dev/null @@ -1,13 +0,0 @@ -ibexa: - system: - admin_group: - fieldtypes: - ibexa_richtext: - toolbars: - paragraph: - buttons: - date: - priority: 0 -ibexa_fieldtype_richtext: - alloy_editor: - extra_plugins: [date] diff --git a/code_samples/front/shop/order-management/config/packages/ibexa.yaml b/code_samples/front/shop/order-management/config/packages/ibexa.yaml index e17cfbc9e5..310eaca7c3 100644 --- a/code_samples/front/shop/order-management/config/packages/ibexa.yaml +++ b/code_samples/front/shop/order-management/config/packages/ibexa.yaml @@ -65,7 +65,7 @@ framework: to: - dropped -// ... +# ... ibexa: repositories: diff --git a/code_samples/front/shop/payment/src/bundle/Resources/config/services/payment_method.yaml b/code_samples/front/shop/payment/src/bundle/Resources/config/services/payment_method.yaml index b179640c05..ef3b33c817 100644 --- a/code_samples/front/shop/payment/src/bundle/Resources/config/services/payment_method.yaml +++ b/code_samples/front/shop/payment/src/bundle/Resources/config/services/payment_method.yaml @@ -8,7 +8,7 @@ services: $domain: tags: - { name: ibexa.payment.payment_method.type, alias: new_payment_method_type } -services: + App\Payment\PaymentMethod\Voter\NewPaymentMethodTypeVoter: tags: - - { name: ibexa.payment.payment_method.voter, type: new_payment_method_type } \ No newline at end of file + - { name: ibexa.payment.payment_method.voter, type: new_payment_method_type } diff --git a/code_samples/recommendations/config/packages/ibexa_connector_raptor.yaml b/code_samples/recommendations/config/packages/ibexa_connector_raptor.yaml index 61c90252b1..1898c35aae 100644 --- a/code_samples/recommendations/config/packages/ibexa_connector_raptor.yaml +++ b/code_samples/recommendations/config/packages/ibexa_connector_raptor.yaml @@ -3,14 +3,14 @@ ibexa: : connector_raptor: enabled: true - customer_id: ~ # Required + customer_id: "12345" # Required tracking_type: client # One of: "client" or "server" # Raptor Recommendations API key - recommendations_api_key: ~ # Required + recommendations_api_key: "your_api_key_here" # Required - # Raptor Recommendations API URL, optional, set by default - recommendations_api_url: '%ibexa.connector.raptor.recommendations.api_url%' + # Raptor Recommendations API URI, optional, set by default + recommendations_api_uri: '%ibexa.connector.raptor.recommendations.api_uri%' ibexa_connector_raptor: # When enabled, tracking exceptions are thrown instead of being silently handled strict_exceptions: true diff --git a/composer.json b/composer.json index e9415fc8fd..351e081fec 100644 --- a/composer.json +++ b/composer.json @@ -4,6 +4,9 @@ "type": "library", "license": "GNU General Public License v2.0", "autoload-dev": { + "psr-4": { + "Ibexa\\Tests\\Documentation\\": "tests/" + } }, "repositories": [ { @@ -15,6 +18,9 @@ "php": "^8.3" }, "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/yaml": "^7.0", + "ibexa/connector-gemini": "5.0.x-dev", "ibexa/automated-translation": "5.0.x-dev", "ibexa/code-style": "~2.0.0", "friendsofphp/php-cs-fixer": "^3.30", @@ -51,7 +57,7 @@ "ibexa/page-builder": "5.0.x-dev", "ibexa/order-management": "5.0.x-dev", "ibexa/calendar": "5.0.x-dev", - "ibexa/payment": "5.0.x-dev", + "ibexa/payment": "~5.0.x-dev", "ibexa/shipping": "5.0.x-dev", "ibexa/fieldtype-matrix": "5.0.x-dev", "ibexa/storefront": "5.0.x-dev", @@ -85,21 +91,32 @@ "ibexa/cdp": "~5.0.x-dev", "ibexa/connector-raptor": "~5.0.x-dev", "ibexa/image-editor": "~5.0.x-dev", - "ibexa/integrated-help": "~5.0.x-dev" + "ibexa/integrated-help": "~5.0.x-dev", + "ibexa/site-context": "~5.0.x-dev", + "ibexa/fieldtype-richtext-rte": "~5.0.x-dev", + "ibexa/site-factory": "~5.0.x-dev", + "ibexa/ckeditor-premium": "~5.0.x-dev", + "ibexa/measurement": "~5.0.x-dev", + "ibexa/connector-actito": "~5.0.x-dev", + "ibexa/fastly": "~5.0.x-dev" }, "scripts": { "fix-cs": "php-cs-fixer fix --config=.php-cs-fixer.php -v --show-progress=dots", "check-cs": "@fix-cs --dry-run", "phpstan": "phpstan analyse", "deptrac": "deptrac analyse", - "check-rector": "rector process --dry-run --ansi" + "check-rector": "rector process --dry-run --ansi", + "phpunit": "phpunit", + "phpunit-update-baseline": "php tests/generate-yaml-baseline.php" }, "scripts-descriptions": { "fix-cs": "Automatically fixes code style in all files", "check-cs": "Run code style checker for all files", "phpstan": "Run static code analysis", "deptrac": "Run Deptrac architecture testing", - "check-rector": "Check for code refactoring opportunities" + "check-rector": "Check for code refactoring opportunities", + "phpunit": "Run PHPUnit tests (YAML validation)", + "phpunit-update-baseline": "Regenerate tests/yaml-validation-baseline.yaml from current failures" }, "config": { "allow-plugins": false diff --git a/docs/administration/back_office/customize_search_suggestion.md b/docs/administration/back_office/customize_search_suggestion.md index 158b3384c2..2764115e25 100644 --- a/docs/administration/back_office/customize_search_suggestion.md +++ b/docs/administration/back_office/customize_search_suggestion.md @@ -17,8 +17,9 @@ ibexa: system: : search: - min_query_length: 3 - result_limit: 5 + suggestion: + min_query_length: 3 + result_limit: 5 ``` ## Add custom suggestion source diff --git a/docs/administration/configuration/dynamic_configuration.md b/docs/administration/configuration/dynamic_configuration.md index 09f9cfbf42..c847e0899d 100644 --- a/docs/administration/configuration/dynamic_configuration.md +++ b/docs/administration/configuration/dynamic_configuration.md @@ -18,7 +18,7 @@ parameters: # Internal configuration ibexa.site_access.config.default.content.default_ttl: 60 ibexa.site_access.config.site_group.content.default_ttl: 3600 -  + # Here "myapp" is the namespace, followed by the SiteAccess name as the parameter scope # Parameter "my_param" will have a different value in site_group and admin_group myapp.site_group.my_param: value diff --git a/docs/api/graphql/graphql_customization.md b/docs/api/graphql/graphql_customization.md index 5ebf97b4f0..be6421d049 100644 --- a/docs/api/graphql/graphql_customization.md +++ b/docs/api/graphql/graphql_customization.md @@ -68,9 +68,9 @@ Mutation: createSomething: builder: Mutation builderConfig: - inputType: CreateSomethingInput - payloadType: SomethingPayload - mutateAndGetPayload: '@=mutation('CreateSomething', [value])' + inputType: CreateSomethingInput + payloadType: SomethingPayload + mutateAndGetPayload: "@=mutation('CreateSomething', [value])" CreateSomethingInput: type: relay-mutation-input diff --git a/docs/api/rest_api/rest_api_authentication.md b/docs/api/rest_api/rest_api_authentication.md index f7f77366f8..82b504a010 100644 --- a/docs/api/rest_api/rest_api_authentication.md +++ b/docs/api/rest_api/rest_api_authentication.md @@ -325,10 +325,15 @@ For more information, see [HTTP Authentication: Basic and Digest Access Authenti If the installation has a dedicated host for REST, you can enable HTTP basic authentication only on this host by setting a firewall like in the following example before the `ibexa_front` one: ```yaml +security: + firewalls: + # ... ibexa_rest: host: ^api\.example\.com$ http_basic: realm: Ibexa DXP REST API + #ibexa_front: + # ... ``` !!! caution "Back office uses REST API" diff --git a/docs/cdp/cdp_installation.md b/docs/cdp/cdp_installation.md index 73ba9dd5ae..f84719a45e 100644 --- a/docs/cdp/cdp_installation.md +++ b/docs/cdp/cdp_installation.md @@ -27,11 +27,14 @@ Symfony Flex installs and activates the package. After an installation process is finished, go to `config/packages/security.yaml` and uncomment `ibexa_cdp` rule. ```yaml -ibexa_cdp: - pattern: /cdp/webhook - guard: - authenticator: 'Ibexa\Cdp\Security\CdpRequestAuthenticator' - stateless: true +security: + firewalls: + # ... + ibexa_cdp: + request_matcher: Ibexa\Cdp\Security\RequestMatcher + custom_authenticators: + - 'Ibexa\Cdp\Security\CdpRequestAuthenticator' + stateless: true ``` Now, you can configure [[= product_name_cdp =]]. diff --git a/docs/commerce/checkout/reorder.md b/docs/commerce/checkout/reorder.md index 4d022782e4..96f83c6bed 100644 --- a/docs/commerce/checkout/reorder.md +++ b/docs/commerce/checkout/reorder.md @@ -54,11 +54,11 @@ framework: places: !php/const Ibexa\OrderManagement\Value\Status::COMPLETED_PLACE: metadata: - ... + # ... can_be_reordered: true !php/const Ibexa\OrderManagement\Value\Status::CANCELLED_PLACE: metadata: - ... + # ... can_be_reordered: true ``` diff --git a/docs/commerce/payment/enable_paypal_payments.md b/docs/commerce/payment/enable_paypal_payments.md index 3bccfdcdc5..2e4b890c25 100644 --- a/docs/commerce/payment/enable_paypal_payments.md +++ b/docs/commerce/payment/enable_paypal_payments.md @@ -42,5 +42,4 @@ ibexa: type: pp_express_checkout: name: "Translated PayPal Express Checkout name" - ``` diff --git a/docs/commerce/payment/enable_stripe_payments.md b/docs/commerce/payment/enable_stripe_payments.md index 105ed62402..5d03bd8829 100644 --- a/docs/commerce/payment/enable_stripe_payments.md +++ b/docs/commerce/payment/enable_stripe_payments.md @@ -43,5 +43,4 @@ ibexa: type: strp_checkout: name: "Translated Stripe Checkout name" - ``` diff --git a/docs/commerce/payment/payum_integration.md b/docs/commerce/payment/payum_integration.md index 7da7da20b1..408515f257 100644 --- a/docs/commerce/payment/payum_integration.md +++ b/docs/commerce/payment/payum_integration.md @@ -43,7 +43,7 @@ ibexa_connector_payum: refunded: cancelled captured: pending authorized: authorized -[...] +# ... ``` ## Payment service name translations diff --git a/docs/commerce/storefront/configure_storefront.md b/docs/commerce/storefront/configure_storefront.md index bdc96c02c8..5d69a73251 100644 --- a/docs/commerce/storefront/configure_storefront.md +++ b/docs/commerce/storefront/configure_storefront.md @@ -94,9 +94,10 @@ Settings for a Storefront user are configured under the `ibexa.system..st ibexa: system: site_group: - user_settings_groups: - - location - - custom_group + storefront: + user_settings_groups: + - location + - custom_group ``` By default, only the `location` user settings is provided: diff --git a/docs/content_management/collaborative_editing/configure_collaborative_editing.md b/docs/content_management/collaborative_editing/configure_collaborative_editing.md index 482040f3e1..268f774ff4 100644 --- a/docs/content_management/collaborative_editing/configure_collaborative_editing.md +++ b/docs/content_management/collaborative_editing/configure_collaborative_editing.md @@ -57,14 +57,15 @@ security: ```yaml security: # ... - ibexa_shareable_link: - request_matcher: Ibexa\Collaboration\Security\RequestMatcher\ShareableLinkRequestMatcher - pattern: ^/ - provider: shared - stateless: true - user_checker: Ibexa\Core\MVC\Symfony\Security\UserChecker - custom_authenticators: - - Ibexa\Collaboration\Security\Authenticator\ShareableLinkAuthenticator + firewalls: + ibexa_shareable_link: + request_matcher: Ibexa\Collaboration\Security\RequestMatcher\ShareableLinkRequestMatcher + pattern: ^/ + provider: shared + stateless: true + user_checker: Ibexa\Core\MVC\Symfony\Security\UserChecker + custom_authenticators: + - Ibexa\Collaboration\Security\Authenticator\ShareableLinkAuthenticator ``` ### Configuration diff --git a/docs/content_management/data_migration/managing_migrations.md b/docs/content_management/data_migration/managing_migrations.md index 072a7a0e6d..c96e078208 100644 --- a/docs/content_management/data_migration/managing_migrations.md +++ b/docs/content_management/data_migration/managing_migrations.md @@ -50,7 +50,7 @@ You can configure a different folder by using the following settings: ``` yaml ibexa_migrations: - migration_directory: %kernel.project_dir%/src/Migrations/MyMigrations/ + migration_directory: '%kernel.project_dir%/src/Migrations/MyMigrations/' migrations_files_subdir: migration_files ``` @@ -64,7 +64,6 @@ ibexa_migrations: ``` yaml ibexa_migrations: migration_directory: '%kernel.project_dir%/data/' - ... ``` Then, when you run the migration command, you must use the [`--siteaccess` option](exporting_data.md#siteaccess) and provide the name of the SiteAccess that you want to migrate. diff --git a/docs/content_management/field_types/field_type_storage.md b/docs/content_management/field_types/field_type_storage.md index ed3ab221c3..2aa8816b52 100644 --- a/docs/content_management/field_types/field_type_storage.md +++ b/docs/content_management/field_types/field_type_storage.md @@ -147,7 +147,7 @@ services: autoconfigure: true public: false - App\FieldType\MyField\Storage\MyFieldStorage: ~ + App\FieldType\MyField\Storage\MyFieldStorage: tags: - {name: ibexa.field_type.storage.external.handler, alias: myfield} ``` diff --git a/docs/content_management/field_types/form_and_template.md b/docs/content_management/field_types/form_and_template.md index 471cdf36c9..9babd387cc 100644 --- a/docs/content_management/field_types/form_and_template.md +++ b/docs/content_management/field_types/form_and_template.md @@ -190,7 +190,7 @@ If you don't use the design engine, apply the following configuration: ``` yaml ibexa: - systems: + system: admin_group: field_templates: - { template: 'adminui/field/custom_field_view.html.twig', priority: 10 } diff --git a/docs/content_management/images/add_image_asset_from_dam.md b/docs/content_management/images/add_image_asset_from_dam.md index 8c7c236fd3..52b91bbc7c 100644 --- a/docs/content_management/images/add_image_asset_from_dam.md +++ b/docs/content_management/images/add_image_asset_from_dam.md @@ -49,15 +49,15 @@ Next, in `config/packages/ibexa.yaml`, set the `dam.html.twig` template for the For more information about displaying content, see [Content rendering](render_content.md). ``` yaml - ibexa: - system: - site: - content_view: - embed: - image_dam: - template: '@ibexadesign/embed/dam.html.twig' - match: - Identifier\ContentType: +ibexa: + system: + site: + content_view: + embed: + image_dam: + template: '@ibexadesign/embed/dam.html.twig' + match: + Identifier\ContentType: ``` In your [configuration file](configuration.md#configuration-files) add the following configuration: diff --git a/docs/content_management/taxonomy/taxonomy.md b/docs/content_management/taxonomy/taxonomy.md index f01ddb75df..118b787a18 100644 --- a/docs/content_management/taxonomy/taxonomy.md +++ b/docs/content_management/taxonomy/taxonomy.md @@ -191,10 +191,9 @@ By default, the system returns three suggestions. You can change the default number if needed by altering the following setting: ``` yaml hl_lines="4" -ibexa: - taxonomy: +ibexa_taxonomy: text_to_taxonomy: - default_suggested_taxonomies_limit: 5 + default_suggested_taxonomies_limit: 5 ``` You can also override this setting per AI action by editing its configuration. diff --git a/docs/content_management/url_management/url_management.md b/docs/content_management/url_management/url_management.md index a249b83d0e..a9416514a5 100644 --- a/docs/content_management/url_management/url_management.md +++ b/docs/content_management/url_management/url_management.md @@ -67,13 +67,13 @@ ibexa: url_checker: handlers: http: - enabled: true - batch_size: 64 + enabled: true + batch_size: 64 https: - enabled: true - ignore_certificate: false + enabled: true + ignore_certificate: false mailto: - enabled: false + enabled: false ``` Available options are protocol-specific. @@ -128,7 +128,6 @@ Then you must register the service with an `ibexa.url_checker.handler` tag, like ```yaml app.url_checker.handler.custom: class: 'App\URLChecker\Handler\CustomHandler' - ... tags: - { name: ibexa.url_checker.handler, scheme: custom } ``` diff --git a/docs/content_management/user_generated_content.md b/docs/content_management/user_generated_content.md index 4a12ecc3bf..a35612334e 100644 --- a/docs/content_management/user_generated_content.md +++ b/docs/content_management/user_generated_content.md @@ -56,14 +56,17 @@ For example, `/content/edit/draft/1/5/eng-GB` enables you to edit draft 5 of con You can use custom templates for the content editing forms. -Define the templates under the `ibexa.system..content_edit.templates` [configuration key](configuration.md#configuration-files): +Define the templates under the `ibexa.system..content_edit_view` [configuration key](configuration.md#configuration-files): ``` yaml ibexa: system: default: - content_edit: - templates: - edit: content/edit/content_edit.html.twig - create_draft: content/edit/content_create_draft.html.twig + content_edit_view: + full: + : + template: content/edit/content_edit.html.twig + match: true + params: + viewbaseLayout: '@ibexadesign/ui/layout.html.twig' ``` diff --git a/docs/customer_management/cp_page_builder.md b/docs/customer_management/cp_page_builder.md index 69e3bdd6b6..d8673c5b55 100644 --- a/docs/customer_management/cp_page_builder.md +++ b/docs/customer_management/cp_page_builder.md @@ -51,7 +51,7 @@ ibexa: languages: [ eng-GB ] content: tree_root: - location_id: location_id_of_customer_portal + location_id: 9999 # location_id_of_customer_portal excluded_uri_prefixes: [ /media/, /images/ ] ``` @@ -135,7 +135,7 @@ ibexa: languages: [ eng-GB ] content: tree_root: - location_id: location_id_of_customer_portals_root_folder + location_id: 9999 # location_id_of_customer_portals_root_folder excluded_uri_prefixes: [ /media/, /images/ ] ``` @@ -233,7 +233,7 @@ ibexa: page_layout: "@App/my_page_layout.html.twig" content: tree_root: - location_id: location_id_of_customer_portals_root_folder + location_id: 999 #location_id_of_customer_portals_root_folder excluded_uri_prefixes: [ /media/, /images/ ] ``` diff --git a/docs/infrastructure_and_maintenance/cache/http_cache/content_aware_cache.md b/docs/infrastructure_and_maintenance/cache/http_cache/content_aware_cache.md index d6ab0444a1..ae176fac6d 100644 --- a/docs/infrastructure_and_maintenance/cache/http_cache/content_aware_cache.md +++ b/docs/infrastructure_and_maintenance/cache/http_cache/content_aware_cache.md @@ -291,7 +291,7 @@ With the same content structure as above, the `[Child]` location is moved below The new structure is then: -```yaml +```text - [Home] (content-id=52, location-id=2) ez-all c52 ct42 l2 pl1 p1 p2 | diff --git a/docs/infrastructure_and_maintenance/cache/http_cache/reverse_proxy.md b/docs/infrastructure_and_maintenance/cache/http_cache/reverse_proxy.md index f309604195..bd8f0921b0 100644 --- a/docs/infrastructure_and_maintenance/cache/http_cache/reverse_proxy.md +++ b/docs/infrastructure_and_maintenance/cache/http_cache/reverse_proxy.md @@ -142,6 +142,9 @@ ibexa: If the Varnish server is protected by Basic Auth, specify the Basic Auth credentials within the `purge_servers` setting using the format: ``` yaml +ibexa: + system: + my_siteaccess_group: http_cache: purge_servers: [http://myuser:mypasswd@my.varnish.server:8081] ``` diff --git a/docs/multisite/site_factory/site_factory.md b/docs/multisite/site_factory/site_factory.md index 74bed2fefb..3afd0628e9 100644 --- a/docs/multisite/site_factory/site_factory.md +++ b/docs/multisite/site_factory/site_factory.md @@ -204,7 +204,7 @@ Keep in mind that with disabled Site Factory you're unable to add new sites or u doctrine: dbal: connections: - ... + # ... # This connection is dedicated for SiteFactory to avoid known issues site_factory: ``` @@ -214,7 +214,7 @@ doctrine: ``` yaml framework: cache: - ... + # ... pools: # This pool should be used only by SiteFactory bundle site_factory_pool: diff --git a/docs/recommendations/raptor_integration/connector_installation_configuration.md b/docs/recommendations/raptor_integration/connector_installation_configuration.md index dfc2645400..3e133a440f 100644 --- a/docs/recommendations/raptor_integration/connector_installation_configuration.md +++ b/docs/recommendations/raptor_integration/connector_installation_configuration.md @@ -35,7 +35,7 @@ To configure the Raptor connector, use the `ibexa.system..connector_rapto - `client` - tracking is executed in the browser using JavaScript snippets generated by the [Twig functions](recommendations_twig_functions.md) and included in the templates. This approach may be blocked by ad blockers. - `server` - tracking is handled on the backend, with events sent directly to the tracking API. It's not affected by ad blockers. - `recommendations_api_key` - an API key used to authenticate requests to the Recommendations API. This key allows the connector to retrieve personalized recommendations from the recommendation engine. You can find this value as ["API key"](connector_installation_configuration.md#recommendations-api-key) in Raptor Control Panel. -- `recommendations_api_url` (optional) - overrides the default Raptor address, do not set it unless a custom endpoint is required. +- `recommendations_api_uri` (optional) - overrides the default Raptor address, do not set it unless a custom endpoint is required. By default, `tracking_type` is set to `client` as client-side tracking is the standard Raptor mode. To understand the differences between client and server tracking types, including their advantages and disadvantages, refer to the [Raptor documentation](https://content.raptorservices.com/help-center/client-side-vs.-server-side-tracking). diff --git a/docs/release_notes/ez_platform_v2.4.md b/docs/release_notes/ez_platform_v2.4.md index 90562b8ada..16d92cbdb2 100644 --- a/docs/release_notes/ez_platform_v2.4.md +++ b/docs/release_notes/ez_platform_v2.4.md @@ -197,18 +197,18 @@ The biggest benefit of this feature is saving load time on complex landing pages 2\. Add the following configuration to `/app/config/config.yml` ``` yaml - lexik_jwt_authentication: - secret_key: '%secret%' - encoder: + lexik_jwt_authentication: + secret_key: '%secret%' + encoder: signature_algorithm: HS256 # Disabled by default, because Page Builder uses custom extractor - token_extractors: - authorization_header: - enabled: false - cookie: - enabled: false - query_parameter: - enabled: false + token_extractors: + authorization_header: + enabled: false + cookie: + enabled: false + query_parameter: + enabled: false ``` By default `HS256` is used as signature algorithm for generated token but we strongly recommend switching to SSH keys. @@ -218,23 +218,23 @@ The biggest benefit of this feature is saving load time on complex landing pages 3\. Add `EzSystems\EzPlatformPageBuilder\Security\EditorialMode\TokenAuthenticator` authentication provider to `ezpublish_front` firewall before `form_login` in `app/config/security.yml`: ``` yaml - security: + security: # ... - firewalls: - ezpublish_front: - # ... - simple_preauth: - authenticator: 'EzSystems\EzPlatformPageBuilder\Security\EditorialMode\TokenAuthenticator' - form_login: - require_previous_session: false - # ... + firewalls: + ezpublish_front: + # ... + simple_preauth: + authenticator: 'EzSystems\EzPlatformPageBuilder\Security\EditorialMode\TokenAuthenticator' + form_login: + require_previous_session: false + # ... ``` 4\. Make sure that parameter `page_builder.token_authenticator.enabled` has value `true`. If the parameter isn't present, add it to `/app/config/config.yml`: ``` yaml - # ... - parameters: + # ... + parameters: # ... page_builder.token_authenticator.enabled: true ``` diff --git a/docs/search/search_engines/elasticsearch/configure_elasticsearch.md b/docs/search/search_engines/elasticsearch/configure_elasticsearch.md index 3e05c45665..38c9cf33bb 100644 --- a/docs/search/search_engines/elasticsearch/configure_elasticsearch.md +++ b/docs/search/search_engines/elasticsearch/configure_elasticsearch.md @@ -182,7 +182,7 @@ If your Elasticsearch server is protected by HTTP authentication, you must provi In the basic authentication, you must pass the following parameters: ``` yaml - +: # ... authentication: type: basic @@ -377,14 +377,14 @@ Index names use the following pattern: You can create index templates with settings that apply to a specific language only, for example, to eliminate stop words from the index, or help divide concatenations. You use patterns to identify index templates that contain settings specific for a given language: - ``` yaml - ibexa_elasticsearch: +``` yaml +ibexa_elasticsearch: # ... index_templates: default_en_us: patterns: ['default_*', '*eng_us*'] - # ... - ``` + # ... +``` - `settings` - Settings under this key control all aspects related to an index. @@ -392,21 +392,21 @@ For more information and a list of available settings, see [Elasticsearch docume For example, you can define settings that convert text into a format that is optimized for search, like a normalizer that changes a case of all phrases in the index: - ``` yaml - ibexa_elasticsearch: - # ... - index_templates: - default: - # ... - settings: - analysis: - normalizer: - lowercase_normalizer: - type: custom - char_filter: [] - filter: lowercase - # ... - ``` +``` yaml +ibexa_elasticsearch: + # ... + index_templates: + default: + # ... + settings: + analysis: + normalizer: + lowercase_normalizer: + type: custom + char_filter: [] + filter: lowercase + # ... +``` - `mappings` - Settings under this key define mapping for fields in the index. diff --git a/docs/search/search_engines/solr_search_engine/install_solr.md b/docs/search/search_engines/solr_search_engine/install_solr.md index edf2a76c0c..b7de523435 100644 --- a/docs/search/search_engines/solr_search_engine/install_solr.md +++ b/docs/search/search_engines/solr_search_engine/install_solr.md @@ -160,9 +160,9 @@ The Solr Search Engine Bundle can be configured in many ways. The config further below assumes you have parameters set up for Solr DSN and search engine *(however both are optional)*, for example: ``` yaml - env(SEARCH_ENGINE): solr - env(SOLR_DSN): 'http://localhost:8983/solr' - env(SOLR_CORE): collection1 +env(SEARCH_ENGINE): solr +env(SOLR_DSN): 'http://localhost:8983/solr' +env(SOLR_CORE): collection1 ``` ### Configure Solr version diff --git a/docs/templating/image_variations.md b/docs/templating/image_variations.md index cdcb8d9046..e4718dadfb 100644 --- a/docs/templating/image_variations.md +++ b/docs/templating/image_variations.md @@ -26,7 +26,9 @@ ibexa: : reference: null filters: - : + filter_name: + - parameter1 + - parameter2 ``` Variation name must be unique. diff --git a/docs/templating/templates/view_matcher_reference.md b/docs/templating/templates/view_matcher_reference.md index 687569c488..4e706fbeb1 100644 --- a/docs/templating/templates/view_matcher_reference.md +++ b/docs/templating/templates/view_matcher_reference.md @@ -235,7 +235,7 @@ match: ``` yaml match: - '@Ibexa\Taxonomy\View\Matcher\TaxonomyEntryBased\Id': [1, 2, 3]' + '@Ibexa\Taxonomy\View\Matcher\TaxonomyEntryBased\Id': [1, 2, 3] ``` ## Taxonomy entry identifier @@ -264,4 +264,4 @@ match: ``` yaml match: '@Ibexa\Taxonomy\View\Matcher\TaxonomyEntryBased\Taxonomy': 'product_category' -``` \ No newline at end of file +``` diff --git a/docs/users/invitations.md b/docs/users/invitations.md index 446e852902..ddcebdd860 100644 --- a/docs/users/invitations.md +++ b/docs/users/invitations.md @@ -30,13 +30,13 @@ If the SiteAccess isn't set, it falls back to the default `site` value. For example, use the following [configuration](configuration.md#configuration-files): ```yaml - ibexa: - system: - : - user_invitation: - hash_expiration_time: P7D - templates: - mail: "@@App/invitation/mail.html.twig" +ibexa: + system: + : + user_invitation: + hash_expiration_time: P7D + templates: + mail: "@@App/invitation/mail.html.twig" ``` Here, you can specify which template should be used for the invitation mail, and what should be the expiration time for the invitation link included in that mail. diff --git a/docs/users/oauth_server.md b/docs/users/oauth_server.md index 45ac506920..bc12a9889f 100644 --- a/docs/users/oauth_server.md +++ b/docs/users/oauth_server.md @@ -89,7 +89,7 @@ In `config/packages/security.yaml`, uncomment the three following lines under th ```yaml security: #… - firewall: + firewalls: #… # Uncomment oauth2_token firewall if you wish to use product as an OAuth2 Server. diff --git a/docs/users/user_authentication.md b/docs/users/user_authentication.md index d2b7280301..9ec9e79c5d 100644 --- a/docs/users/user_authentication.md +++ b/docs/users/user_authentication.md @@ -66,7 +66,7 @@ services: App\EventListener\InteractiveLoginListener: arguments: ['@ibexa.api.service.user'] tags: - - { name: kernel.event_subscriber }  + - { name: kernel.event_subscriber } ``` Don't mix `MVCEvents::INTERACTIVE_LOGIN` event (specific to [[= product_name =]]) and `SecurityEvents::INTERACTIVE_LOGIN` event (fired by Symfony security component). diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000000..e084420e57 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,14 @@ + + + + + tests/ + + + diff --git a/tests/ConfigurationProvider.php b/tests/ConfigurationProvider.php new file mode 100644 index 0000000000..4dfbe49523 --- /dev/null +++ b/tests/ConfigurationProvider.php @@ -0,0 +1,178 @@ +container = $this->buildContainer(); + } + + public function hasExtension(string $alias): bool + { + return $this->container->hasExtension($alias); + } + + public function createConfiguration(string $alias): ConfigurationInterface + { + return $this->container->getExtension($alias)->getConfiguration([], $this->container); + } + + /** + * Recursively resolves %parameter% placeholders using the container's + * parameter bag, mirroring what the real Symfony kernel does before + * passing config to the Config component. Unknown parameters (custom app + * params not present in the test container) are left as-is. + * + * @param array $config + * + * @return array + */ + public function resolveParameters(array $config): array + { + /** @var array $result */ + $result = $this->resolveValue($this->container->getParameterBag(), $config); + + return $result; + } + + private function resolveValue(ParameterBagInterface $bag, mixed $value): mixed + { + if (is_array($value)) { + return array_map(fn (mixed $v): mixed => $this->resolveValue($bag, $v), $value); + } + + if (!is_string($value)) { + return $value; + } + + try { + return $bag->resolveValue($value); + } catch (ParameterNotFoundException) { + return $value; + } + } + + private function buildContainer(): ContainerBuilder + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.debug', false); + $container->setParameter('kernel.bundles', []); + $container->setParameter('kernel.bundles_metadata', []); + $container->setParameter('kernel.project_dir', sys_get_temp_dir()); + $container->setParameter('kernel.environment', 'test'); + + $bundles = self::discoverBundles(); + + // Register all extensions before calling build() on any bundle, + // because some bundles call $container->getExtension('ibexa') during build(). + foreach ($bundles as $bundle) { + try { + $extension = $bundle->getContainerExtension(); + if ($extension !== null) { + $container->registerExtension($extension); + } + } catch (\Throwable) { + // Skip bundles whose extension cannot be instantiated. + } + } + + // build() registers parsers/factories into the extensions. + foreach ($bundles as $bundle) { + try { + $bundle->build($container); + } catch (\Throwable) { + // Skip bundles whose build() fails (e.g. missing sibling extensions). + } + } + + return $container; + } + + /** + * Returns all installed bundles with SecurityBundle and IbexaCoreBundle + * guaranteed first (other bundles may call getExtension('ibexa') or + * getExtension('security') during their build()). + * + * @return list + */ + private static function discoverBundles(): array + { + // These must be registered before any bundle that calls + // $container->getExtension('ibexa'/'security') inside build(). + $bundles = [ + new SecurityBundle(), + new IbexaCoreBundle(), + ]; + + $seen = [SecurityBundle::class, IbexaCoreBundle::class]; + + $vendorBase = __DIR__ . '/../vendor'; + $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($vendorBase)); + + foreach ($iterator as $file) { + if (!$file->isFile() || !preg_match('/\w+Bundle\.php$/', $file->getFilename())) { + continue; + } + + $content = file_get_contents($file->getPathname()); + preg_match('/^namespace (.+);/m', $content, $nsMatch); + preg_match('/^(?:(?:final|abstract)\s+)?class (\w+Bundle)\b/m', $content, $clsMatch); + + if (empty($nsMatch[1]) || empty($clsMatch[1])) { + continue; + } + + $fqcn = $nsMatch[1] . '\\' . $clsMatch[1]; + + if (!class_exists($fqcn) || in_array($fqcn, $seen, true)) { + continue; + } + + $reflection = new \ReflectionClass($fqcn); + if ($reflection->isAbstract() || !$reflection->implementsInterface(BundleInterface::class)) { + continue; + } + + $seen[] = $fqcn; + + try { + $bundles[] = new $fqcn(); + } catch (\Throwable) { + // Skip bundles that cannot be instantiated without arguments. + } + } + + return $bundles; + } +} diff --git a/tests/Markdown/MarkdownYamlExtractor.php b/tests/Markdown/MarkdownYamlExtractor.php new file mode 100644 index 0000000000..a78f95385d --- /dev/null +++ b/tests/Markdown/MarkdownYamlExtractor.php @@ -0,0 +1,78 @@ + *)```\s*yaml[^\n]*\n(?P.*?)\n(?P=indent)```/ms'; + + private const string SKIP_PATTERN = '/include_file\s*\(|--8<--/'; + + /** + * @return iterable + */ + public function extract(string $content): iterable + { + if (!preg_match_all(self::FENCE_PATTERN, $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { + return; + } + + foreach ($matches as $match) { + $body = $match['body'][0]; + $offset = $match['body'][1]; + + if (preg_match(self::SKIP_PATTERN, $body)) { + continue; + } + + $indent = $match['indent'][0]; + if ($indent !== '') { + $body = $this->stripIndentation($body, strlen($indent)); + } + + $line = substr_count(substr($content, 0, $offset), "\n") + 1; + + yield ['body' => $body, 'line' => $line]; + } + } + + private function stripIndentation(string $body, int $spaces): string + { + $prefix = str_repeat(' ', $spaces); + $lines = explode("\n", $body); + $stripped = array_map( + static fn (string $line): string => str_starts_with($line, $prefix) + ? substr($line, $spaces) + : $line, + $lines + ); + + return implode("\n", $stripped); + } +} diff --git a/tests/Markdown/MarkdownYamlExtractorTest.php b/tests/Markdown/MarkdownYamlExtractorTest.php new file mode 100644 index 0000000000..b65c6445a8 --- /dev/null +++ b/tests/Markdown/MarkdownYamlExtractorTest.php @@ -0,0 +1,208 @@ +extractor = new MarkdownYamlExtractor(); + } + + public function testExtractsNothing(): void + { + self::assertEmpty(iterator_to_array($this->extractor->extract('No code blocks here.'))); + self::assertEmpty(iterator_to_array($this->extractor->extract(''))); + } + + public function testIgnoresNonYamlFences(): void + { + $content = <<<'MD' + ```php + $x = 1; + ``` + + ```json + {"key": "value"} + ``` + MD; + + self::assertEmpty(iterator_to_array($this->extractor->extract($content))); + } + + public function testExtractsSingleBlock(): void + { + $content = <<<'MD' + Some text. + + ```yaml + foo: bar + ``` + + More text. + MD; + + $blocks = iterator_to_array($this->extractor->extract($content)); + + self::assertCount(1, $blocks); + self::assertSame('foo: bar', $blocks[0]['body']); + } + + public function testExtractsMultipleBlocks(): void + { + $content = <<<'MD' + ```yaml + first: 1 + ``` + + ```yaml + second: 2 + ``` + MD; + + $blocks = iterator_to_array($this->extractor->extract($content)); + + self::assertCount(2, $blocks); + self::assertSame('first: 1', $blocks[0]['body']); + self::assertSame('second: 2', $blocks[1]['body']); + } + + public function testReportsCorrectLineNumber(): void + { + $content = "line1\nline2\nline3\n```yaml\nfoo: bar\n```\n"; + + $blocks = iterator_to_array($this->extractor->extract($content)); + + self::assertCount(1, $blocks); + // The body starts on line 5 (after 4 preceding newlines inside the fence open) + self::assertSame(5, $blocks[0]['line']); + } + + public function testAcceptsSpaceBeforeLanguageTag(): void + { + $content = "``` yaml\nfoo: bar\n```\n"; + + $blocks = iterator_to_array($this->extractor->extract($content)); + + self::assertCount(1, $blocks); + self::assertSame('foo: bar', $blocks[0]['body']); + } + + public function testAcceptsTrailingAnnotations(): void + { + $content = "```yaml hl_lines=\"1 2\"\nfoo: bar\n```\n"; + + $blocks = iterator_to_array($this->extractor->extract($content)); + + self::assertCount(1, $blocks); + self::assertSame('foo: bar', $blocks[0]['body']); + } + + public function testStripsAdmonitionIndentation(): void + { + $content = <<<'MD' + !!! note + + ```yaml + foo: bar + baz: qux + ``` + MD; + + $blocks = iterator_to_array($this->extractor->extract($content)); + + self::assertCount(1, $blocks); + self::assertSame("foo: bar\nbaz: qux", $blocks[0]['body']); + } + + public function testSkipsBlocksWithIncludeFile(): void + { + $content = <<<'MD' + ```yaml + [[= include_file('some/file.yaml') =]] + ``` + MD; + + self::assertEmpty(iterator_to_array($this->extractor->extract($content))); + } + + public function testSkipsBlocksWithSnippetMarker(): void + { + $content = <<<'MD' + ```yaml + --8<-- + some/file.yaml + ``` + MD; + + self::assertEmpty(iterator_to_array($this->extractor->extract($content))); + } + + public function testSkipsOnlyMatchingBlocksWhenMixed(): void + { + $content = <<<'MD' + ```yaml + [[= include_file('foo.yaml') =]] + ``` + + ```yaml + real: config + ``` + MD; + + $blocks = iterator_to_array($this->extractor->extract($content)); + + self::assertCount(1, $blocks); + self::assertSame('real: config', $blocks[0]['body']); + } + + /** + * @param array $expected + */ + #[DataProvider('provideMultilineBlocks')] + public function testExtractsMultilineBody(string $content, array $expected): void + { + $blocks = iterator_to_array($this->extractor->extract($content)); + + self::assertCount(count($expected), $blocks); + foreach ($expected as $i => $exp) { + self::assertSame($exp['body'], $blocks[$i]['body'], "body at index $i"); + self::assertSame($exp['line'], $blocks[$i]['line'], "line at index $i"); + } + } + + /** + * @return iterable}> + */ + public static function provideMultilineBlocks(): iterable + { + yield 'nested mapping' => [ + "```yaml\nparent:\n child: value\n```\n", + [['body' => "parent:\n child: value", 'line' => 2]], + ]; + + yield 'sequence' => [ + "```yaml\nlist:\n - a\n - b\n```\n", + [['body' => "list:\n - a\n - b", 'line' => 2]], + ]; + + yield 'two blocks with correct lines' => [ + "```yaml\nfoo: 1\n```\n\nsome text\n\n```yaml\nbar: 2\n```\n", + [ + ['body' => 'foo: 1', 'line' => 2], + ['body' => 'bar: 2', 'line' => 8], + ], + ]; + } +} diff --git a/tests/ValidationBaseline.php b/tests/ValidationBaseline.php new file mode 100644 index 0000000000..d689915b2f --- /dev/null +++ b/tests/ValidationBaseline.php @@ -0,0 +1,74 @@ +|null */ + private ?array $entries = null; + + public function __construct( + private readonly string $baselineFile, + private readonly string $repoRoot, + ) { + } + + public function isInBaseline(string $relativePath, ?int $line, string $errorMessage): bool + { + foreach ($this->getEntries() as $entry) { + $entryPath = $entry['path'] ?? ''; + + // Path: exact match or trailing-suffix match (allows glob-like partial paths) + if ($relativePath !== $entryPath && !str_ends_with($relativePath, ltrim($entryPath, '/'))) { + continue; + } + + // Line (optional): must match exactly when provided + if (isset($entry['line']) && $line !== null && (int) $entry['line'] !== $line) { + continue; + } + + // Message (optional): treated as a regex pattern + if (isset($entry['message']) && !preg_match($entry['message'], $errorMessage)) { + continue; + } + + return true; + } + + return false; + } + + /** + * @return list + */ + private function getEntries(): array + { + if ($this->entries !== null) { + return $this->entries; + } + + if (!file_exists($this->baselineFile)) { + return $this->entries = []; + } + + $parsed = \Symfony\Component\Yaml\Yaml::parseFile($this->baselineFile); + + return $this->entries = $parsed['ignoreErrors'] ?? []; + } +} diff --git a/tests/Yaml/CodeSample.php b/tests/Yaml/CodeSample.php new file mode 100644 index 0000000000..7429e6feeb --- /dev/null +++ b/tests/Yaml/CodeSample.php @@ -0,0 +1,13 @@ + + */ + public function getCodeSampleYaml(): iterable + { + yield from $this->iterateCodeSampleYaml(); + yield from $this->iterateMarkdownYamlBlocks(); + } + + /** + * Yields every .yaml file found recursively under code_samples/. + * + * @return iterable + */ + private function iterateCodeSampleYaml(): iterable + { + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator(self::CODE_SAMPLES_DIR, RecursiveDirectoryIterator::SKIP_DOTS) + ); + + /** @var \SplFileInfo $file */ + foreach ($iterator as $file) { + if (!$file->isFile() || $file->getExtension() !== 'yaml') { + continue; + } + + $body = file_get_contents($file->getRealPath()); + + if ($body === false) { + continue; + } + + yield new CodeSample($file->getRealPath(), 0, $body); + } + } + + /** + * Yields every fenced YAML block found in .md files under docs/. + * + * @return iterable + */ + private function iterateMarkdownYamlBlocks(): iterable + { + $extractor = new MarkdownYamlExtractor(); + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator(self::DOCS_DIR, RecursiveDirectoryIterator::SKIP_DOTS) + ); + + /** @var \SplFileInfo $file */ + foreach ($iterator as $file) { + if (!$file->isFile() || $file->getExtension() !== 'md') { + continue; + } + + $path = $file->getRealPath(); + $content = file_get_contents($path); + + if ($content === false) { + continue; + } + + foreach ($extractor->extract($content) as $block) { + yield new CodeSample($path, $block['line'], $block['body']); + } + } + } +} diff --git a/tests/Yaml/YamlTest.php b/tests/Yaml/YamlTest.php new file mode 100644 index 0000000000..6cf2824e60 --- /dev/null +++ b/tests/Yaml/YamlTest.php @@ -0,0 +1,165 @@ +isInBaseline($filePath, $line ?: null, $e->getMessage())) { + self::markTestSkipped(sprintf( + 'Known baseline issue in %s at line %d: %s', + $filePath, + $line, + $e->getMessage(), + )); + } + + self::fail(sprintf( + 'YAML parse error in %s at line %d: %s', + $filePath, + $line, + $e->getMessage(), + )); + } + + $this->addToAssertionCount(1); + } + + /** + * @param int $line Starting line of the config block (0 for standalone YAML files). + */ + #[DataProvider('provideBundleConfigs')] + public function testBundleConfigurationIsValid( + string $extensionName, + mixed $config, + string $filePath, + int $line + ): void { + $configuration = self::configurationProvider()->createConfiguration($extensionName); + $processor = new Processor(); + + $config = self::configurationProvider()->resolveParameters(is_array($config) ? $config : []); + + try { + $processor->processConfiguration($configuration, [$config]); + } catch (\Exception $e) { + if (self::baseline()->isInBaseline($filePath, $line ?: null, $e->getMessage())) { + self::markTestSkipped(sprintf( + 'Known baseline issue for "%s" in %s:%d: %s', + $extensionName, + $filePath, + $line, + $e->getMessage(), + )); + } + + self::fail(sprintf( + 'Invalid configuration for "%s" in %s:%d — %s', + $extensionName, + $filePath, + $line, + $e->getMessage(), + )); + } + + $this->addToAssertionCount(1); + } + + /** + * Yields all standalone YAML files from code_samples/ plus every fenced + * YAML block extracted from docs Markdown files. + * + * @return iterable + */ + public static function provideYamlSources(): iterable + { + foreach (self::samplesProvider()->getCodeSampleYaml() as $item) { + yield self::makeLabel($item->path, $item->line) => [$item->path, $item->line, $item->body]; + } + } + + /** + * Yields one entry per (extension, config) pair found in YAML files and + * in fenced YAML blocks from docs Markdown files. + * + * @return iterable + */ + public static function provideBundleConfigs(): iterable + { + foreach (self::provideYamlSources() as [$filePath, $line, $body]) { + $path = self::relativePath($filePath); + try { + $parsed = Yaml::parse($body, Yaml::PARSE_CUSTOM_TAGS); + } catch (\Throwable) { + continue; + } + + if (!is_array($parsed)) { + continue; + } + + foreach ($parsed as $extensionName => $config) { + if (!is_string($extensionName) || !self::configurationProvider()->hasExtension($extensionName)) { + continue; + } + + yield sprintf('%s (%s)', $extensionName, self::makeLabel($path, $line)) => [$extensionName, $config, $path, $line]; + } + } + } + + private static function configurationProvider(): ConfigurationProvider + { + static $provider = null; + + return $provider ??= new ConfigurationProvider(); + } + + private static function samplesProvider(): YamlSamplesProvider + { + static $provider = null; + + return $provider ??= new YamlSamplesProvider(); + } + + private static function baseline(): ValidationBaseline + { + static $baseline = null; + + return $baseline ??= new ValidationBaseline(self::BASELINE_FILE, realpath(self::REPO_ROOT)); + } + + private static function makeLabel(string $absolutePath, int $lineNumber): string + { + return ltrim(str_replace(realpath(self::REPO_ROOT), '', $absolutePath), '/') . ':' . $lineNumber; + } + + private static function relativePath(string $absolutePath): string + { + return ltrim(str_replace(realpath(self::REPO_ROOT), '', $absolutePath), '/'); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000000..9374e6f3a2 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,40 @@ +isDir() && $item->getFilename() === 'src') { + $loader->addPsr4('App\\', $item->getRealPath()); + } +} diff --git a/tests/generate-yaml-baseline.php b/tests/generate-yaml-baseline.php new file mode 100644 index 0000000000..d8a6f7d7ff --- /dev/null +++ b/tests/generate-yaml-baseline.php @@ -0,0 +1,142 @@ +#!/usr/bin/env php +/dev/null', + escapeshellarg($phpunitBin), + escapeshellarg($tmpLog), +); + +passthru($cmd); + +if (!file_exists($tmpLog)) { + if ($previousBaseline !== null) { + file_put_contents($outFile, $previousBaseline); + } + fwrite(STDERR, "ERROR: PHPUnit did not produce a JUnit log file.\n"); + exit(1); +} + +$xml = simplexml_load_file($tmpLog); +unlink($tmpLog); + +if ($xml === false) { + fwrite(STDERR, "ERROR: Could not parse JUnit XML.\n"); + exit(1); +} + +$repoRoot = dirname(__DIR__); +$entries = []; + +// Collect all failures/errors at any nesting depth using XPath +/** @var \SimpleXMLElement[] $testcases */ +$testcases = $xml->xpath('//testcase[failure or error]') ?: []; + +foreach ($testcases as $testcase) { + $failure = $testcase->failure ?? $testcase->error ?? null; + if ($failure === null) { + continue; + } + + $message = (string) $failure; + // Extract file path and line from failure message + // Patterns: + // "Invalid configuration for "X" in path/to/file.yaml: error" + // "Invalid configuration for "X" in path/to/file.md:123 — error" + // "YAML parse error in path/to/file.md at line 123: error" + $path = null; + $line = null; + $errorMessage = null; + + if (preg_match('/Invalid configuration for "[^"]*" in ([^\n:]+?):(\d+) — (.+)/s', $message, $m)) { + $path = trim($m[1]); + $line = (int) $m[2]; + $errorMessage = trim(explode("\n", $m[3])[0]); + } elseif (preg_match('/Invalid configuration for "[^"]*" in ([^\n:]+?): (.+)/s', $message, $m)) { + $path = trim($m[1]); + $errorMessage = trim(explode("\n", $m[2])[0]); + } elseif (preg_match('/YAML parse error in ([^\n]+?) at line (\d+): (.+)/s', $message, $m)) { + $path = trim($m[1]); + $line = (int) $m[2]; + $errorMessage = trim(explode("\n", $m[3])[0]); + } + + if ($path === null) { + continue; + } + + // Convert absolute path to relative + if (str_starts_with($path, $repoRoot)) { + $path = ltrim(substr($path, strlen($repoRoot)), '/'); + } + + $entry = ['path' => $path]; + if ($line !== null) { + $entry['line'] = $line; + } + if ($errorMessage !== null) { + // Store as a regex: escape special chars, keep it readable + $entry['message'] = '~' . preg_quote($errorMessage, '~') . '~'; + } + + $key = $path . ':' . ($line ?? ''); + $entries[$key] = $entry; +} + +ksort($entries); + +// Render as YAML manually (keep it readable without needing a YAML library) +$lines = []; +$lines[] = '# Auto-generated by `composer phpunit-update-baseline`. Do not edit manually.'; +$lines[] = '# To suppress a failure: regenerate this file after confirming it is expected.'; +$lines[] = '# To fix a suppressed failure: fix the doc error and regenerate.'; +$lines[] = 'ignoreErrors:'; + +foreach ($entries as $entry) { + $lines[] = ' -'; + $lines[] = sprintf(' path: %s', $entry['path']); + if (isset($entry['line'])) { + $lines[] = sprintf(' line: %d', $entry['line']); + } + if (isset($entry['message'])) { + // Wrap message in single quotes, escaping internal single quotes + $msg = str_replace("'", "''", $entry['message']); + $lines[] = sprintf(" message: '%s'", $msg); + } +} + +$content = implode("\n", $lines) . "\n"; +$outFile = __DIR__ . '/yaml-validation-baseline.yaml'; +file_put_contents($outFile, $content); + +$count = count($entries); +echo "Baseline written to tests/yaml-validation-baseline.yaml ({$count} entries)\n"; +if ($count === 0) { + echo "No failures found — baseline is empty. All tests pass!\n"; +} diff --git a/tests/yaml-validation-baseline.yaml b/tests/yaml-validation-baseline.yaml new file mode 100644 index 0000000000..01e5acfe11 --- /dev/null +++ b/tests/yaml-validation-baseline.yaml @@ -0,0 +1,316 @@ +# Auto-generated by `composer phpunit-update-baseline`. Do not edit manually. +# To suppress a failure: regenerate this file after confirming it is expected. +# To fix a suppressed failure: fix the doc error and regenerate. +ignoreErrors: + - + path: code_samples/forms/custom_form_attribute/config/custom_services.yaml + line: 0 + message: '~Unable to parse at line 1 \(near " App\\FormBuilder\\FieldType\\Field\\Mapper\\CheckboxWithRichtextDescriptionFieldMapper\:"\)\.~' + - + path: code_samples/workflow/custom_workflow/config/packages/workflows.yaml + line: 0 + message: '~The child config "stages" under "ibexa\.system\.default\.workflows\.quick_review" must be configured\.~' + - + path: docs/administration/back_office/back_office_elements/extending_thumbnails.md + line: 109 + message: '~Unable to parse at line 1 \(near " App\\Thumbnails\\FieldValueUrl\:"\)\.~' + - + path: docs/administration/back_office/configure_product_tour.md + line: 26 + message: '~The value "\" is not allowed for path "ibexa\.system\.\\>\.product_tour\.\\.type"\. Permissible values\: "general", "targetable"\.~' + - + path: docs/cdp/cdp_data_customization.md + line: 29 + message: '~Unable to parse at line 1 \(near " App\\Export\\User\\DateOfBirthUserItemProcessor\:"\)\.~' + - + path: docs/commerce/checkout/customize_checkout.md + line: 130 + message: '~Invalid configuration for path "framework\.workflows\.workflows\.ibexa_checkout"\: "supports" or "support_strategy" should be configured\.~' + - + path: docs/commerce/order_management/configure_order_management.md + line: 51 + message: '~The child config "transitions" under "framework\.workflows\.workflows\.ibexa_order" must be configured\.~' + - + path: docs/commerce/payment/enable_paypal_payments.md + line: 40 + message: '~Unrecognized option "payment_method" under "ibexa"\. Available options are "http_cache", "image_placeholder", "imagemagick", "locale_conversion", "orm", "repositories", "router", "siteaccess", "system", "ui", "url_alias", "url_wildcards"\.~' + - + path: docs/commerce/payment/enable_stripe_payments.md + line: 41 + message: '~Unrecognized option "payment_method" under "ibexa"\. Available options are "http_cache", "image_placeholder", "imagemagick", "locale_conversion", "orm", "repositories", "router", "siteaccess", "system", "ui", "url_alias", "url_wildcards"\.~' + - + path: docs/commerce/payment/payum_integration.md + line: 55 + message: '~Unrecognized option "payment_method" under "ibexa"\. Available options are "http_cache", "image_placeholder", "imagemagick", "locale_conversion", "orm", "repositories", "router", "siteaccess", "system", "ui", "url_alias", "url_wildcards"\.~' + - + path: docs/commerce/transactional_emails/extend_transactional_emails.md + line: 17 + message: '~The child config "transitions" under "framework\.workflows\.workflows\.ibexa_payment" must be configured\.~' + - + path: docs/content_management/collaborative_editing/configure_collaborative_editing.md + line: 48 + message: '~The child config "firewalls" under "security" must be configured\.~' + - + path: docs/content_management/collaborative_editing/configure_collaborative_editing.md + line: 78 + message: '~Invalid type for path "ibexa\.repositories\.\\.collaboration\.participants\.auto_invite"\. Expected "bool", but got "string"\.~' + - + path: docs/content_management/data_migration/data_migration_actions.md + line: 102 + message: '~Unable to parse at line 1 \(near " actions\:"\)\.~' + - + path: docs/content_management/data_migration/data_migration_actions.md + line: 114 + message: '~Unable to parse at line 1 \(near " actions\:"\)\.~' + - + path: docs/content_management/data_migration/data_migration_actions.md + line: 136 + message: '~Unable to parse at line 1 \(near " actions\:"\)\.~' + - + path: docs/content_management/data_migration/data_migration_actions.md + line: 156 + message: '~Unable to parse at line 1 \(near " actions\:"\)\.~' + - + path: docs/content_management/data_migration/data_migration_actions.md + line: 178 + message: '~Unable to parse at line 1 \(near " actions\:"\)\.~' + - + path: docs/content_management/data_migration/data_migration_actions.md + line: 78 + message: '~Unable to parse at line 1 \(near " actions\:"\)\.~' + - + path: docs/content_management/data_migration/data_migration_actions.md + line: 86 + message: '~Unable to parse at line 1 \(near " actions\:"\)\.~' + - + path: docs/content_management/data_migration/data_migration_actions.md + line: 96 + message: '~Unable to parse at line 1 \(near " actions\:"\)\.~' + - + path: docs/content_management/data_migration/importing_data.md + line: 200 + message: '~Unable to parse at line 1 \(near " \- fieldDefIdentifier\: show_children"\)\.~' + - + path: docs/content_management/data_migration/importing_data.md + line: 220 + message: '~Unable to parse at line 1 \(near " \- fieldDefIdentifier\: some_field"\)\.~' + - + path: docs/content_management/data_migration/importing_data.md + line: 228 + message: '~Unable to parse at line 1 \(near " \- fieldDefIdentifier\: project_directory"\)\.~' + - + path: docs/content_management/data_migration/importing_data.md + line: 313 + message: '~Unable to parse at line 1 \(near " \- fieldDefIdentifier\: image"\)\.~' + - + path: docs/content_management/taxonomy/taxonomy.md + line: 194 + message: '~The child config "default_embedding_max_tokens" under "ibexa_taxonomy\.text_to_taxonomy" must be configured\: Maximum number of tokens sent when generating embeddings~' + - + path: docs/content_management/workflow/workflow.md + line: 135 + message: '~The child config "matcher_value_templates" under "ibexa\.system\.default\.workflows_config" must be configured\: Matcher templates configuration\.~' + - + path: docs/customer_management/cp_page_builder.md + line: 119 + message: '~The child config "default_siteaccess" under "ibexa\.siteaccess" must be configured\: Name of the default siteaccess~' + - + path: docs/customer_management/cp_page_builder.md + line: 207 + message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~' + - + path: docs/customer_management/cp_page_builder.md + line: 35 + message: '~The child config "default_siteaccess" under "ibexa\.siteaccess" must be configured\: Name of the default siteaccess~' + - + path: docs/discounts/extend_discounts.md + line: 139 + message: '~Unable to parse at line 1 \(near " App\\Discounts\\Condition\\IsAccountAnniversaryConditionFactory\:"\)\.~' + - + path: docs/discounts/extend_discounts.md + line: 187 + message: '~Unable to parse at line 1 \(near " App\\Discounts\\Rule\\PurchasingPowerParityRuleFactory\:"\)\.~' + - + path: docs/discounts/extend_discounts.md + line: 211 + message: '~Unable to parse at line 1 \(near " App\\Discounts\\Rule\\PurchaseParityValueFormatter\:"\)\.~' + - + path: docs/discounts/extend_discounts.md + line: 229 + message: '~Unable to parse at line 1 \(near " App\\Discounts\\RecentDiscountPrioritizationStrategy\:"\)\.~' + - + path: docs/discounts/extend_discounts.md + line: 64 + message: '~Unable to parse at line 1 \(near " App\\Discounts\\ExpressionProvider\\CurrentUserRegistrationDateResolver\:"\)\.~' + - + path: docs/discounts/extend_discounts.md + line: 82 + message: '~Unable to parse at line 1 \(near " App\\Discounts\\ExpressionProvider\\IsAnniversaryResolver\:"\)\.~' + - + path: docs/discounts/extend_discounts_wizard.md + line: 142 + message: '~Unable to parse at line 1 \(near " App\\Form\\FormMapper\\PurchasingPowerParityValueMapper\: \~"\)\.~' + - + path: docs/getting_started/first_steps.md + line: 115 + message: '~The child config "default_siteaccess" under "ibexa\.siteaccess" must be configured\: Name of the default siteaccess~' + - + path: docs/infrastructure_and_maintenance/cache/http_cache/reverse_proxy.md + line: 128 + message: '~The child config "fastly" under "ibexa\.system\.my_siteaccess_group\.http_cache" must be configured\.~' + - + path: docs/infrastructure_and_maintenance/cache/http_cache/reverse_proxy.md + line: 145 + message: '~The child config "fastly" under "ibexa\.system\.my_siteaccess_group\.http_cache" must be configured\.~' + - + path: docs/infrastructure_and_maintenance/security/development_security.md + line: 20 + message: '~Unrecognized option "require_previous_session" under "security\.firewalls\.ibexa_front\.form_login"\. Available options are "always_use_default_target_path", "check_path", "csrf_parameter", "csrf_token_id", "default_target_path", "enable_csrf", "failure_forward", "failure_handler", "failure_path", "failure_path_parameter", "form_only", "login_path", "password_parameter", "post_only", "provider", "remember_me", "success_handler", "target_path_parameter", "use_forward", "use_referer", "username_parameter"\.~' + - + path: docs/infrastructure_and_maintenance/security/security_checklist.md + line: 142 + message: '~The child config "firewalls" under "security" must be configured\.~' + - + path: docs/multisite/languages/languages.md + line: 135 + message: '~The child config "match" under "ibexa\.siteaccess" must be configured\: Siteaccess match configuration\. First key is the matcher class, value is passed to the matcher\. Key can be a service identifier \(prepended by "@"\), or a FQ class name \(prepended by "\\"\)~' + - + path: docs/multisite/languages/languages.md + line: 78 + message: '~The child config "match" under "ibexa\.siteaccess" must be configured\: Siteaccess match configuration\. First key is the matcher class, value is passed to the matcher\. Key can be a service identifier \(prepended by "@"\), or a FQ class name \(prepended by "\\"\)~' + - + path: docs/multisite/multisite_configuration.md + line: 28 + message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~' + - + path: docs/multisite/multisite_configuration.md + line: 57 + message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~' + - + path: docs/multisite/multisite_configuration.md + line: 69 + message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~' + - + path: docs/multisite/set_up_translation_siteaccess.md + line: 43 + message: '~The child config "default_siteaccess" under "ibexa\.siteaccess" must be configured\: Name of the default siteaccess~' + - + path: docs/multisite/site_factory/site_factory.md + line: 40 + message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~' + - + path: docs/multisite/site_factory/site_factory.md + line: 58 + message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~' + - + path: docs/multisite/site_factory/site_factory_configuration.md + line: 81 + message: '~The child config "siteaccess_group" under "ibexa_site_factory\.templates\.\" must be configured\.~' + - + path: docs/multisite/siteaccess/siteaccess_matching.md + line: 108 + message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~' + - + path: docs/multisite/siteaccess/siteaccess_matching.md + line: 123 + message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~' + - + path: docs/multisite/siteaccess/siteaccess_matching.md + line: 140 + message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~' + - + path: docs/multisite/siteaccess/siteaccess_matching.md + line: 161 + message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~' + - + path: docs/multisite/siteaccess/siteaccess_matching.md + line: 178 + message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~' + - + path: docs/multisite/siteaccess/siteaccess_matching.md + line: 194 + message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~' + - + path: docs/multisite/siteaccess/siteaccess_matching.md + line: 237 + message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~' + - + path: docs/multisite/siteaccess/siteaccess_matching.md + line: 32 + message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~' + - + path: docs/multisite/siteaccess/siteaccess_matching.md + line: 72 + message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~' + - + path: docs/multisite/siteaccess/siteaccess_matching.md + line: 91 + message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~' + - + path: docs/permissions/limitation_reference.md + line: 28 + message: '~Unable to parse at line 3 \(near " ibexa\.api\.role\.limitation_type\.function_list\:"\)\.~' + - + path: docs/personalization/attribute_search_in_elasticsearch.md + line: 16 + message: '~Invalid output type in \{"\"\:\{"title"\:"\"\}\}\. Output type id should be type of int\.~' + - + path: docs/product_catalog/enable_purchasing_products.md + line: 106 + message: '~Unable to parse at line 1 \(near " none\:"\)\.~' + - + path: docs/product_catalog/product_catalog_configuration.md + line: 77 + message: '~Unable to parse at line 1 \(near " none\:"\)\.~' + - + path: docs/recommendations/raptor_integration/tracking_functions.md + line: 33 + message: '~Duplicate key "connector_raptor" detected at line 6 \(near " tracking_type\: ''client'' \# Returns \ tags"\)\.~' + - + path: docs/release_notes/ez_platform_v2.4.md + line: 221 + message: '~Unrecognized option "require_previous_session" under "security\.firewalls\.ezpublish_front\.form_login"\. Available options are "always_use_default_target_path", "check_path", "csrf_parameter", "csrf_token_id", "default_target_path", "enable_csrf", "failure_forward", "failure_handler", "failure_path", "failure_path_parameter", "form_only", "login_path", "password_parameter", "post_only", "provider", "remember_me", "success_handler", "target_path_parameter", "use_forward", "use_referer", "username_parameter"\.~' + - + path: docs/templating/design_engine/add_new_design.md + line: 16 + message: '~The child config "match" under "ibexa\.siteaccess" must be configured\: Siteaccess match configuration\. First key is the matcher class, value is passed to the matcher\. Key can be a service identifier \(prepended by "@"\), or a FQ class name \(prepended by "\\"\)~' + - + path: docs/templating/templates/template_configuration.md + line: 99 + message: '~Duplicate key "match" detected at line 3 \(near "match\: \[\]"\)\.~' + - + path: docs/update_and_migration/from_3.3/to_4.0.md + line: 237 + message: '~Unrecognized option "ezrichtext" under "ibexa\.system\.admin_group\.fieldtypes"\. Available options are "ibexa_image_asset", "ibexa_richtext"\.~' + - + path: docs/update_and_migration/from_4.3/update_from_4.3_old_commerce.md + line: 168 + message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~' + - + path: docs/update_and_migration/from_4.6/update_from_4.6.md + line: 513 + message: '~Unrecognized option "trace" under "ibexa_elasticsearch\.connections\.default"\. Available options are "authentication", "connection_pool", "connection_selector", "debug", "elastic_cloud_id", "hosts", "index_templates", "node_pool_resurrect", "node_pool_selector", "retries", "ssl"\.~' + - + path: docs/update_and_migration/from_4.6/update_from_4.6.md + line: 80 + message: '~Unrecognized option "Ibexa\\Contracts\\Shipping\\Notification\\ShipmentStatusChange" under "ibexa\.system\.my_siteacces_name\.notifications\.subscriptions"\. Available option is "timeout"\.~' + - + path: docs/update_and_migration/from_5.0/update_from_5.0.md + line: 238 + message: '~Unrecognized option "trace" under "ibexa_elasticsearch\.connections\.default"\. Available options are "authentication", "connection_pool", "connection_selector", "debug", "elastic_cloud_id", "hosts", "index_templates", "node_pool_resurrect", "node_pool_selector", "retries", "ssl"\.~' + - + path: docs/update_and_migration/migrate_to_ibexa_dxp/migrating_from_ez_publish_platform.md + line: 526 + message: '~Mapping values are not allowed in multi\-line blocks at line 2 \(near " ezpublish\.persistence\.slug_converter\:"\)\.~' + - + path: docs/update_and_migration/migrate_to_ibexa_dxp/migrating_from_ez_publish_platform.md + line: 537 + message: '~Unable to parse at line 1 \(near " ezpublish\.persistence\.slug_converter\:"\)\.~' + - + path: docs/users/oauth_server.md + line: 78 + message: '~The child config "firewalls" under "security" must be configured\.~' + - + path: docs/users/user_authentication.md + line: 41 + message: '~Unrecognized option "encoders" under "security"\. Available options are "access_control", "access_decision_manager", "access_denied_url", "erase_credentials", "expose_security_errors", "firewalls", "hide_user_not_found", "password_hashers", "providers", "role_hierarchy", "session_fixation_strategy"\.~'