Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/LiveComponent/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"symfony/property-access": "^5.4.5|^6.0|^7.0",
"symfony/property-info": "^5.4|^6.0|^7.0",
"symfony/stimulus-bundle": "^2.9",
"symfony/ux-twig-component": "^2.25",
"symfony/ux-twig-component": "^2.25.1",
"twig/twig": "^3.10.3"
},
"require-dev": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
Expand Down Expand Up @@ -110,7 +109,7 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
new Reference('ux.live_component.metadata_factory'),
new Reference('serializer', ContainerInterface::NULL_ON_INVALID_REFERENCE),
$config['secret'], // defaults to '%kernel.secret%'
new Reference('ux.twig_component.component_attributes_factory'),
new Reference('twig'),
])
;

Expand Down Expand Up @@ -158,7 +157,7 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
->setArguments([
new Reference('ux.live_component.fingerprint_calculator'),
new Reference('ux.live_component.attribute_helper_factory'),
new Reference('ux.twig_component.component_attributes_factory'),
new Reference('twig'),
])
->addTag('container.service_subscriber', ['key' => ComponentFactory::class, 'id' => 'ux.twig_component.component_factory'])
->addTag('container.service_subscriber', ['key' => LiveComponentMetadataFactory::class, 'id' => 'ux.live_component.metadata_factory'])
Expand Down Expand Up @@ -219,7 +218,7 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
->setArguments([
new Reference('ux.twig_component.component_stack'),
new Reference('ux.live_component.twig.template_mapper'),
new Reference('ux.twig_component.component_attributes_factory'),
new Reference('twig'),
])
->addTag('kernel.event_subscriber')
->addTag('container.service_subscriber', ['key' => LiveControllerAttributesCreator::class, 'id' => 'ux.live_component.live_controller_attributes_creator'])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@
use Symfony\UX\LiveComponent\Twig\TemplateMap;
use Symfony\UX\LiveComponent\Util\LiveControllerAttributesCreator;
use Symfony\UX\TwigComponent\ComponentAttributes;
use Symfony\UX\TwigComponent\ComponentAttributesFactory;
use Symfony\UX\TwigComponent\ComponentMetadata;
use Symfony\UX\TwigComponent\ComponentStack;
use Symfony\UX\TwigComponent\Event\PreRenderEvent;
use Symfony\UX\TwigComponent\MountedComponent;
use Twig\Environment;
use Twig\Runtime\EscaperRuntime;

/**
* Adds the extra attributes needed to activate a live controller.
Expand All @@ -37,7 +38,7 @@ final class AddLiveAttributesSubscriber implements EventSubscriberInterface, Ser
public function __construct(
private ComponentStack $componentStack,
private TemplateMap $templateMap,
private readonly ComponentAttributesFactory $componentAttributesFactory,
private readonly Environment $twig,
private ContainerInterface $container,
) {
}
Expand Down Expand Up @@ -107,6 +108,6 @@ private function getLiveAttributes(MountedComponent $mounted, ComponentMetadata
$this->componentStack->hasParentComponent()
);

return $this->componentAttributesFactory->create($attributesCollection->toArray());
return new ComponentAttributes($attributesCollection->toArray(), $this->twig->getRuntime(EscaperRuntime::class));
}
}
7 changes: 4 additions & 3 deletions src/LiveComponent/src/LiveComponentHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
use Symfony\UX\LiveComponent\Metadata\LivePropMetadata;
use Symfony\UX\LiveComponent\Util\DehydratedProps;
use Symfony\UX\TwigComponent\ComponentAttributes;
use Symfony\UX\TwigComponent\ComponentAttributesFactory;
use Twig\Environment;
use Twig\Runtime\EscaperRuntime;

/**
* @author Kevin Bond <[email protected]>
Expand All @@ -53,7 +54,7 @@ public function __construct(
private LiveComponentMetadataFactory $liveComponentMetadataFactory,
private NormalizerInterface|DenormalizerInterface|null $serializer,
#[\SensitiveParameter] private string $secret,
private readonly ComponentAttributesFactory $componentAttributesFactory,
private readonly Environment $twig,
) {
if (!$secret) {
throw new \InvalidArgumentException('A non-empty secret is required.');
Expand Down Expand Up @@ -146,7 +147,7 @@ public function hydrate(object $component, array $props, array $updatedProps, Li
$dehydratedOriginalProps = $this->combineAndValidateProps($props, $updatedPropsFromParent);
$dehydratedUpdatedProps = DehydratedProps::createFromUpdatedArray($updatedProps);

$attributes = $this->componentAttributesFactory->create($dehydratedOriginalProps->getPropValue(self::ATTRIBUTES_KEY, []));
$attributes = new ComponentAttributes($dehydratedOriginalProps->getPropValue(self::ATTRIBUTES_KEY, []), $this->twig->getRuntime(EscaperRuntime::class));
$dehydratedOriginalProps->removePropValue(self::ATTRIBUTES_KEY);

$needProcessOnUpdatedHooks = [];
Expand Down
8 changes: 5 additions & 3 deletions src/LiveComponent/src/Util/ChildComponentPartialRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
use Symfony\Contracts\Service\ServiceSubscriberInterface;
use Symfony\UX\LiveComponent\LiveComponentHydrator;
use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadataFactory;
use Symfony\UX\TwigComponent\ComponentAttributesFactory;
use Symfony\UX\TwigComponent\ComponentAttributes;
use Symfony\UX\TwigComponent\ComponentFactory;
use Twig\Environment;
use Twig\Runtime\EscaperRuntime;

/**
* @author Ryan Weaver <[email protected]>
Expand All @@ -28,7 +30,7 @@ class ChildComponentPartialRenderer implements ServiceSubscriberInterface
public function __construct(
private FingerprintCalculator $fingerprintCalculator,
private TwigAttributeHelperFactory $attributeHelperFactory,
private ComponentAttributesFactory $componentAttributesFactory,
private Environment $twig,
private ContainerInterface $container,
) {
}
Expand Down Expand Up @@ -85,7 +87,7 @@ public function renderChildComponent(string $deterministicId, string $currentPro
private function createHtml(array $attributes, string $childTag): string
{
$attributes['data-live-preserve'] = true;
$attributes = $this->componentAttributesFactory->create($attributes);
$attributes = new ComponentAttributes($attributes, $this->twig->getRuntime(EscaperRuntime::class));

return \sprintf('<%s%s></%s>', $childTag, $attributes, $childTag);
}
Expand Down
25 changes: 13 additions & 12 deletions src/LiveComponent/tests/Integration/LiveComponentHydratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@
use Symfony\UX\LiveComponent\Tests\Fixtures\Enum\ZeroIntEnum;
use Symfony\UX\LiveComponent\Tests\LiveComponentTestHelper;
use Symfony\UX\TwigComponent\ComponentAttributes;
use Symfony\UX\TwigComponent\ComponentAttributesFactory;
use Symfony\UX\TwigComponent\ComponentMetadata;
use Twig\Environment;
use Twig\Runtime\EscaperRuntime;
use Zenstruck\Foundry\Test\Factories;
use Zenstruck\Foundry\Test\ResetDatabase;

Expand Down Expand Up @@ -76,9 +77,9 @@ private function executeHydrationTestCase(callable $testFactory, ?int $minPhpVer
$metadataFactory = self::getContainer()->get('ux.live_component.metadata_factory');
\assert($metadataFactory instanceof LiveComponentMetadataFactory);
$testCase = $testBuilder->getTest($metadataFactory);
$componentAttributesFactory = self::getContainer()->get('ux.twig_component.component_attributes_factory');
\assert($componentAttributesFactory instanceof ComponentAttributesFactory);

$twig = self::getContainer()->get('twig');
\assert($twig instanceof Environment);

// keep a copy of the original, empty component object for hydration later
$originalComponentWithData = clone $testCase->component;
Expand All @@ -94,7 +95,7 @@ private function executeHydrationTestCase(callable $testFactory, ?int $minPhpVer

$dehydratedProps = $this->hydrator()->dehydrate(
$originalComponentWithData,
$componentAttributesFactory->create([]), // not worried about testing these here
new ComponentAttributes([], $twig->getRuntime(EscaperRuntime::class)), // not worried about testing these here
$liveMetadata,
);

Expand Down Expand Up @@ -136,7 +137,7 @@ private function executeHydrationTestCase(callable $testFactory, ?int $minPhpVer

$dehydratedProps2 = $this->hydrator()->dehydrate(
$componentAfterHydration,
$componentAttributesFactory->create(),
new ComponentAttributes([], $twig->getRuntime(EscaperRuntime::class)),
$liveMetadata,
);
$this->hydrator()->hydrate(
Expand Down Expand Up @@ -1824,14 +1825,14 @@ public static function falseyValueProvider(): iterable
yield ['nullableBool', '', null];
yield 'fooey-o-booey-todo' => ['nullableBool', ' ', null];
}

private function createComponentAttributes(array $attributes = []): ComponentAttributes
{
$factory = self::getContainer()->get('ux.twig_component.component_attributes_factory');
\assert($factory instanceof ComponentAttributesFactory);
return $factory->create($attributes);
}
$twig = self::getContainer()->get('twig');
\assert($twig instanceof Environment);

return new ComponentAttributes($attributes, $twig->getRuntime(EscaperRuntime::class));
}

private function createLiveMetadata(object $component): LiveComponentMetadata
{
Expand Down
5 changes: 2 additions & 3 deletions src/LiveComponent/tests/Unit/LiveComponentHydratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
use Symfony\UX\LiveComponent\LiveComponentHydrator;
use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadataFactory;
use Symfony\UX\LiveComponent\Metadata\LivePropMetadata;
use Symfony\UX\TwigComponent\ComponentAttributesFactory;
use Twig\Environment;

final class LiveComponentHydratorTest extends TestCase
Expand All @@ -36,7 +35,7 @@ public function testConstructWithEmptySecret(): void
$this->createMock(LiveComponentMetadataFactory::class),
$this->createMock(NormalizerInterface::class),
'',
new ComponentAttributesFactory($this->createMock(Environment::class)),
$this->createMock(Environment::class),
);
}

Expand All @@ -48,7 +47,7 @@ public function testItCanHydrateWithNullValues()
$this->createMock(LiveComponentMetadataFactory::class),
new Serializer(normalizers: [new ObjectNormalizer()]),
'foo',
new ComponentAttributesFactory($this->createMock(Environment::class)),
$this->createMock(Environment::class),
);

$hydratedValue = $hydrator->hydrateValue(
Expand Down
10 changes: 8 additions & 2 deletions src/TwigComponent/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
# CHANGELOG

## 2.25.1

- [SECURITY] `ComponentAttributes` now requires a `Twig\Runtime\EscaperRuntime`
instance as second argument
- Remove `HtmlAttributeEscaperInterface`, `TwigHtmlAttributeEscaper` and `ComponentAttributesFactory`

## 2.25.0

- [SECURITY] Make `ComponentAttributes` responsible for attribute escaping ensuring
consistent and secure HTML output across all rendering contexts.
consistent and secure HTML output across all rendering contexts
- Deprecate not passing an `HtmlAttributeEscaperInterface` to the `ComponentAttributes`
constructor.
constructor

## 2.20.0

Expand Down
28 changes: 13 additions & 15 deletions src/TwigComponent/src/ComponentAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
namespace Symfony\UX\TwigComponent;

use Symfony\UX\StimulusBundle\Dto\StimulusAttributes;
use Symfony\UX\TwigComponent\Escaper\HtmlAttributeEscaperInterface;
use Symfony\WebpackEncoreBundle\Dto\AbstractStimulusDto;
use Twig\Runtime\EscaperRuntime;

/**
* @author Kevin Bond <[email protected]>
Expand All @@ -29,19 +29,13 @@ final class ComponentAttributes implements \Stringable, \IteratorAggregate, \Cou
/** @var array<string,true> */
private array $rendered = [];

private readonly ?HtmlAttributeEscaperInterface $escaper;

/**
* @param array<string, string|bool> $attributes
*/
public function __construct(
private array $attributes,
?HtmlAttributeEscaperInterface $escaper = null,
private readonly EscaperRuntime $escaper,
) {
// Third argument used as internal flag to prevent multiple deprecations
if ((null === $this->escaper = $escaper) && 3 > func_num_args()) {
trigger_deprecation('symfony/ux-twig-component', '2.24', 'Not passing an "%s" to "%s" is deprecated and will throw in 3.0.', HtmlAttributeEscaperInterface::class, self::class);
}
}

public function __toString(): string
Expand Down Expand Up @@ -87,13 +81,17 @@ public function __toString(): string
// - special syntax names (Vue.js, Svelte, Alpine.js, ...)
// v-*, x-*, @*, :*
if (!ctype_alpha(str_replace(['-', '_', ':', '@', '.'], '', $key))) {
$key = $this->escaper?->escapeName($key) ?? $key;
$key = (string) $this->escaper->escape($key, 'html_attr');
}

if (true === $value) {
$attributes .= ' '.$key;
} else {
$attributes .= ' '.\sprintf('%s="%s"', $key, $this->escaper?->escapeValue($value) ?? $value);
if (!ctype_alnum(str_replace(['-', '_'], '', $value))) {
$value = $this->escaper->escape($value, 'html');
}

$attributes .= ' '.\sprintf('%s="%s"', $key, $value);
}
}

Expand Down Expand Up @@ -167,7 +165,7 @@ public function defaults(iterable $attributes): self
unset($attributes[$attribute]);
}

return new self($attributes, $this->escaper, true);
return new self($attributes, $this->escaper);
}

/**
Expand All @@ -183,7 +181,7 @@ public function only(string ...$keys): self
}
}

return new self($attributes, $this->escaper, true);
return new self($attributes, $this->escaper);
}

/**
Expand Down Expand Up @@ -221,7 +219,7 @@ public function add($stimulusDto): self
)));
unset($controllersAttributes['data-controller']);

$clone = new self($attributes, $this->escaper, true);
$clone = new self($attributes, $this->escaper);

// add the remaining attributes for values/classes
return $clone->defaults($controllersAttributes);
Expand All @@ -233,7 +231,7 @@ public function remove($key): self

unset($attributes[$key]);

return new self($attributes, $this->escaper, true);
return new self($attributes, $this->escaper);
}

public function nested(string $namespace): self
Expand All @@ -249,7 +247,7 @@ public function nested(string $namespace): self
}
}

return new self($attributes, $this->escaper, true);
return new self($attributes, $this->escaper);
}

public function getIterator(): \Traversable
Expand Down
42 changes: 0 additions & 42 deletions src/TwigComponent/src/ComponentAttributesFactory.php

This file was deleted.

Loading
Loading