Repository: symfony/ux
Pull request #2600 (2025-02-24 10:50:25)

[Autocomplete] Rethrow `BadRequestHttpException` in case of malformed `extra_options`
| Q             | A
| ------------- | ---
| Bug fix?      | yes
| New feature?  | no
| Issues        | N/A
| License       | MIT

Currently it's enough just to manually edit extra_options in URL to a random string and then get a 500 error while polluting logs. I think it's better to swallow it and properly exit with 400 status.


Patch:
diff --git a/src/Autocomplete/src/Controller/EntityAutocompleteController.php b/src/Autocomplete/src/Controller/EntityAutocompleteController.php
index f8d85512..e3143fc5 100644
--- a/src/Autocomplete/src/Controller/EntityAutocompleteController.php
+++ b/src/Autocomplete/src/Controller/EntityAutocompleteController.php
@@ -76,7 +76,11 @@ final class EntityAutocompleteController
             return [];
         }
 
-        $extraOptions = $this->getDecodedExtraOptions($request->query->getString(self::EXTRA_OPTIONS));
+        try {
+            $extraOptions = $this->getDecodedExtraOptions($request->query->getString(self::EXTRA_OPTIONS));
+        } catch (\JsonException $e) {
+            throw new BadRequestHttpException('The extra options cannot be parsed.', $e);
+        }
 
         if (!\array_key_exists(AutocompleteChoiceTypeExtension::CHECKSUM_KEY, $extraOptions)) {
             throw new BadRequestHttpException('The extra options are missing the checksum.');
diff --git a/src/StimulusBundle/assets/dist/loader.js b/src/StimulusBundle/assets/dist/loader.js
index b2c49cfa..3f70fed3 100644
--- a/src/StimulusBundle/assets/dist/loader.js
+++ b/src/StimulusBundle/assets/dist/loader.js
@@ -53,17 +53,22 @@ class StimulusLazyControllerHandler {
             return;
         }
         new MutationObserver((mutationsList) => {
-            for (const { attributeName, target, type } of mutationsList) {
-                switch (type) {
-                    case 'attributes': {
-                        if (attributeName === controllerAttribute &&
-                            target.getAttribute(controllerAttribute)) {
-                            extractControllerNamesFrom(target).forEach((controllerName) => this.loadLazyController(controllerName));
+            for (const mutation of mutationsList) {
+                switch (mutation.type) {
+                    case 'childList': {
+                        for (const node of mutation.addedNodes) {
+                            if (node instanceof Element) {
+                                extractControllerNamesFrom(node).forEach((controllerName) => {
+                                    this.loadLazyController(controllerName);
+                                });
+                            }
                         }
                         break;
                     }
-                    case 'childList': {
-                        this.lazyLoadExistingControllers(target);
+                    case 'attributes': {
+                        if (mutation.attributeName === controllerAttribute) {
+                            extractControllerNamesFrom(mutation.target).forEach((controllerName) => this.loadLazyController(controllerName));
+                        }
                     }
                 }
             }
diff --git a/src/StimulusBundle/assets/src/loader.ts b/src/StimulusBundle/assets/src/loader.ts
index 178214a6..393ded69 100644
--- a/src/StimulusBundle/assets/src/loader.ts
+++ b/src/StimulusBundle/assets/src/loader.ts
@@ -99,23 +99,26 @@ class StimulusLazyControllerHandler {
             return;
         }
         new MutationObserver((mutationsList) => {
-            for (const { attributeName, target, type } of mutationsList) {
-                switch (type) {
-                    case 'attributes': {
-                        if (
-                            attributeName === controllerAttribute &&
-                            (target as Element).getAttribute(controllerAttribute)
-                        ) {
-                            extractControllerNamesFrom(target as Element).forEach((controllerName) =>
-                                this.loadLazyController(controllerName)
-                            );
+            for (const mutation of mutationsList) {
+                switch (mutation.type) {
+                    case 'childList': {
+                        // @ts-ignore
+                        for (const node of mutation.addedNodes) {
+                            if (node instanceof Element) {
+                                extractControllerNamesFrom(node).forEach((controllerName) => {
+                                    this.loadLazyController(controllerName);
+                                });
+                            }
                         }
-
                         break;
                     }
 
-                    case 'childList': {
-                        this.lazyLoadExistingControllers(target as Element);
+                    case 'attributes': {
+                        if (mutation.attributeName === controllerAttribute) {
+                            extractControllerNamesFrom(mutation.target as Element).forEach((controllerName) =>
+                                this.loadLazyController(controllerName)
+                            );
+                        }
                     }
                 }
             }
diff --git a/src/StimulusBundle/doc/index.rst b/src/StimulusBundle/doc/index.rst
index 88ede629..286f3871 100644
--- a/src/StimulusBundle/doc/index.rst
+++ b/src/StimulusBundle/doc/index.rst
@@ -8,7 +8,7 @@ StimulusBundle: Symfony integration with Stimulus
 This bundle adds integration between Symfony, `Stimulus`_ and the Symfony UX packages:
 
 * Twig ``stimulus_`` functions & filters to add Stimulus controllers,
-  actions & targets in your templates;
+   actions & targets in your templates;
 * Integration to load :ref:`UX Packages <ux-packages>` (extra Stimulus controllers)
 
 Installation
@@ -100,7 +100,33 @@ common problems. StimulusBundle activates any 3rd party Stimulus controllers
 that are mentioned in your ``assets/controllers.json`` file. This file is updated
 whenever you install a UX package.
 
-Check out the `official UX packages`_.
+The official UX packages are:
+
+* `ux-autocomplete`_: Transform ``EntityType``, ``ChoiceType`` or *any*
+  ``<select>`` element into an Ajax-powered autocomplete field
+  (`see demo <https://ux.symfony.com/autocomplete>`_)
+* `ux-chartjs`_: Easy charts with `Chart.js`_ (`see demo <https://ux.symfony.com/chartjs>`_)
+* `ux-cropperjs`_: Form Type and tools for cropping images (`see demo <https://ux.symfony.com/cropperjs>`_)
+* `ux-dropzone`_: Form Type for stylized "drop zone" for file uploads
+  (`see demo <https://ux.symfony.com/dropzone>`_)
+* `ux-lazy-image`_: Optimize Image Loading with BlurHash
+  (`see demo <https://ux.symfony.com/lazy-image>`_)
+* `ux-live-component`_: Build Dynamic Interfaces with Zero JavaScript
+  (`see demo <https://ux.symfony.com/live-component>`_)
+* `ux-notify`_: Send server-sent native notification with Mercure
+  (`see demo <https://ux.symfony.com/notify>`_)
+* `ux-react`_: Render `React`_ component from Twig (`see demo <https://ux.symfony.com/react>`_)
+* `ux-svelte`_: Render `Svelte`_ component from Twig (`see demo <https://ux.symfony.com/svelte>`_)
+* `ux-swup`_: Integration with `Swup`_ (`see demo <https://ux.symfony.com/swup>`_)
+* `ux-toggle-password`_: Toggle visibility of password inputs
+  (`see demo <https://ux.symfony.com/toggle-password>`_)
+* `ux-translator`_: Use your Symfony translations in JavaScript (`see demo <https://ux.symfony.com/translator>`_)
+* `ux-turbo`_: Integration with `Turbo Drive`_ for a single-page-app experience
+  (`see demo <https://ux.symfony.com/turbo>`_)
+* `ux-twig-component`_: Build Twig Components Backed by a PHP Class
+  (`see demo <https://ux.symfony.com/twig-component>`_)
+* `ux-typed`_: Integration with `Typed`_ (`see demo <https://ux.symfony.com/typed>`_)
+* `ux-vue`_: Render `Vue`_ component from Twig (`see demo <https://ux.symfony.com/vue>`_)
 
 Lazy Stimulus Controllers
 ~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -132,7 +158,7 @@ To make a third-party controller lazy, in ``assets/controllers.json``, set
 .. note::
 
     If you write your controllers using TypeScript and you're using
-    StimulusBundle 2.21.0 or earlier, make sure ``removeComments`` is not set
+    StimulusBundle ≤ 2.21.0, make sure ``removeComments`` is not set
     to ``true`` in your TypeScript config.
 
 Stimulus Tools around the World
@@ -214,10 +240,7 @@ And with outlets:
 
 .. code-block:: html+twig
 
-    <div {{ stimulus_controller('chart',
-            { 'name': 'Likes', 'data': [1, 2, 3, 4] },
-            { 'loading': 'spinner' },
-            { 'other': '.target' } ) }}>
+    <div {{ stimulus_controller('chart', { 'name': 'Likes', 'data': [1, 2, 3, 4] }, { 'loading': 'spinner' }, { 'other': '.target' ) }}>
         Hello
     </div>
 
@@ -527,9 +550,31 @@ it will normalize it:
 .. _`parameters`: https://stimulus.hotwired.dev/reference/actions#action-parameters
 .. _`Stimulus Targets`: https://stimulus.hotwired.dev/reference/targets
 .. _`StimulusBundle Flex recipe`: https://github.com/symfony/recipes/tree/main/symfony/stimulus-bundle
+.. _`ux-autocomplete`: https://symfony.com/bundles/ux-autocomplete/current/index.html
+.. _`ux-chartjs`: https://symfony.com/bundles/ux-chartjs/current/index.html
+.. _`ux-cropperjs`: https://symfony.com/bundles/ux-cropperjs/current/index.html
+.. _`ux-dropzone`: https://symfony.com/bundles/ux-dropzone/current/index.html
+.. _`ux-lazy-image`: https://symfony.com/bundles/ux-lazy-image/current/index.html
+.. _`ux-live-component`: https://symfony.com/bundles/ux-live-component/current/index.html
+.. _`ux-notify`: https://symfony.com/bundles/ux-notify/current/index.html
+.. _`ux-react`: https://symfony.com/bundles/ux-react/current/index.html
+.. _ux-translator: https://symfony.com/bundles/ux-translator/current/index.html
+.. _`ux-swup`: https://symfony.com/bundles/ux-swup/current/index.html
+.. _`ux-toggle-password`: https://symfony.com/bundles/ux-toggle-password/current/index.html
+.. _`ux-turbo`: https://symfony.com/bundles/ux-turbo/current/index.html
+.. _`ux-twig-component`: https://symfony.com/bundles/ux-twig-component/current/index.html
+.. _`ux-typed`: https://symfony.com/bundles/ux-typed/current/index.html
+.. _`ux-vue`: https://symfony.com/bundles/ux-vue/current/index.html
+.. _`ux-svelte`: https://symfony.com/bundles/ux-svelte/current/index.html
+.. _`Chart.js`: https://www.chartjs.org/
+.. _`Swup`: https://swup.js.org/
+.. _`React`: https://reactjs.org/
+.. _`Svelte`: https://svelte.dev/
+.. _`Turbo Drive`: https://turbo.hotwired.dev/
+.. _`Typed`: https://github.com/mattboldt/typed.js/
+.. _`Vue`: https://vuejs.org/
 .. _`stimulus-use`: https://stimulus-use.github.io/stimulus-use
 .. _`stimulus-components`: https://www.stimulus-components.com/
 .. _`TypeScript`: https://www.typescriptlang.org/
 .. _`sensiolabs/typescript-bundle`: https://github.com/sensiolabs/AssetMapperTypeScriptBundle
 .. _`Stimulus plugin`: https://plugins.jetbrains.com/plugin/24562-stimulus
-.. _`official UX packages`: https://ux.symfony.com/packages
diff --git a/src/TwigComponent/src/Twig/TwigPreLexer.php b/src/TwigComponent/src/Twig/TwigPreLexer.php
index 3cdee464..5d6d41f3 100644
--- a/src/TwigComponent/src/Twig/TwigPreLexer.php
+++ b/src/TwigComponent/src/Twig/TwigPreLexer.php
@@ -233,7 +233,6 @@ class TwigPreLexer
             $isAttributeDynamic = false;
 
             // :someProp="dynamicVar"
-            $this->consumeWhitespace();
             if ($this->check(':')) {
                 $this->consume(':');
                 $isAttributeDynamic = true;
@@ -245,7 +244,6 @@ class TwigPreLexer
 
             // <twig:component someProp> -> someProp: true
             if (!$this->check('=')) {
-                $this->consumeWhitespace();
                 // don't allow "<twig:component :someProp>"
                 if ($isAttributeDynamic) {
                     throw new SyntaxError(\sprintf('Expected "=" after ":%s" when parsing the "<twig:%s" syntax.', $key, $componentName), $this->line);
@@ -334,12 +332,6 @@ class TwigPreLexer
         $whitespace = substr($this->input, $this->position, strspn($this->input, " \t\n\r\0\x0B", $this->position));
         $this->line += substr_count($whitespace, "\n");
         $this->position += \strlen($whitespace);
-
-        if ($this->check('#')) {
-            $this->consume('#');
-            $this->consumeUntil("\n");
-            $this->consumeWhitespace();
-        }
     }
 
     /**


Test patch:
diff --git a/src/TwigComponent/tests/Unit/TwigPreLexerTest.php b/src/TwigComponent/tests/Unit/TwigPreLexerTest.php
index 2878fb3b..f4489867 100644
--- a/src/TwigComponent/tests/Unit/TwigPreLexerTest.php
+++ b/src/TwigComponent/tests/Unit/TwigPreLexerTest.php
@@ -376,65 +376,5 @@ final class TwigPreLexerTest extends TestCase
             '<twig:foobar bar="baz" {{ ...attr }}>content</twig:foobar>',
             '{% component \'foobar\' with { bar: \'baz\', ...attr } %}{% block content %}content{% endblock %}{% endcomponent %}',
         ];
-        yield 'component_with_comment_line' => [
-            "<twig:foo \n   # bar  \n />",
-            '{{ component(\'foo\') }}',
-        ];
-        yield 'component_with_comment_line_between_args' => [
-            <<<TWIG
-            <twig:foo
-                # bar
-                bar="baz"
-            />
-            TWIG,
-            '{{ component(\'foo\', { bar: \'baz\' }) }}',
-        ];
-        yield 'component_with_comment_lines_between_args' => [
-            <<<TWIG
-            <twig:foo
-                # comment
-                foo="foo"
-                # comment
-                bar="bar"
-            />
-            TWIG,
-            '{{ component(\'foo\', { foo: \'foo\', bar: \'bar\' }) }}',
-        ];
-        yield 'component_with_comment_line_containing_ending_tag' => [
-            <<<TWIG
-            <twig:foo
-                # comment /></twig:foo>
-                bar="bar"
-            />
-            TWIG,
-            '{{ component(\'foo\', { bar: \'bar\' }) }}',
-        ];
-        yield 'component_with_comment_line_in_argument_value' => [
-            <<<TWIG
-            <twig:foo
-                bar="# bar"
-            />
-            TWIG,
-            '{{ component(\'foo\', { bar: \'# bar\' }) }}',
-        ];
-        yield 'component_with_comment_line_in_argument_array_value_is_kept' => [
-            <<<TWIG
-            <twig:foo
-                bar="{{ {
-                    a: 'b',
-                    # comment
-                    c: 'd'
-                } }}"
-            />
-            TWIG,
-            // Twig will remove the comment, we don't need to remove it
-            <<<TWIG
-            {{ component('foo', { bar: ({
-                    a: 'b',
-                    # comment
-                    c: 'd'
-                }) }) }}
-            TWIG,
-        ];
     }
 }