Skip to content

Commit

Permalink
[LiveComponent] Allow configuring secret for fingerprints and checksums
Browse files Browse the repository at this point in the history
  • Loading branch information
smnandre committed Dec 24, 2024
1 parent c3ee75b commit a641a2e
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 6 deletions.
4 changes: 4 additions & 0 deletions src/LiveComponent/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# CHANGELOG

## 2.23.0

- Allow configuring the secret used to compute fingerprints and checksums.

## 2.22.0

- Remove CSRF tokens - rely on same-origin/CORS instead
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
namespace Symfony\UX\LiveComponent\DependencyInjection;

use Symfony\Component\AssetMapper\AssetMapperInterface;
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\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
Expand Down Expand Up @@ -50,14 +54,12 @@
use Symfony\UX\TwigComponent\ComponentFactory;
use Symfony\UX\TwigComponent\ComponentRenderer;

use function Symfony\Component\DependencyInjection\Loader\Configurator\tagged_iterator;

/**
* @author Kevin Bond <[email protected]>
*
* @internal
*/
final class LiveComponentExtension extends Extension implements PrependExtensionInterface
final class LiveComponentExtension extends Extension implements PrependExtensionInterface, ConfigurationInterface
{
public const TEMPLATES_MAP_FILENAME = 'live_components_twig_templates.map';

Expand Down Expand Up @@ -93,16 +95,19 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
}
);

$configuration = $this->getConfiguration($configs, $container);
$config = $this->processConfiguration($configuration, $configs);

$container->registerForAutoconfiguration(HydrationExtensionInterface::class)
->addTag(LiveComponentBundle::HYDRATION_EXTENSION_TAG);

$container->register('ux.live_component.component_hydrator', LiveComponentHydrator::class)
->setArguments([
tagged_iterator(LiveComponentBundle::HYDRATION_EXTENSION_TAG),
new TaggedIteratorArgument(LiveComponentBundle::HYDRATION_EXTENSION_TAG),
new Reference('property_accessor'),
new Reference('ux.live_component.metadata_factory'),
new Reference('serializer', ContainerInterface::NULL_ON_INVALID_REFERENCE),
'%kernel.secret%',
$config['secret'], // defaults to '%kernel.secret%'
])
;

Expand Down Expand Up @@ -236,7 +241,7 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {

$container->register('ux.live_component.deterministic_id_calculator', DeterministicTwigIdCalculator::class);
$container->register('ux.live_component.fingerprint_calculator', FingerprintCalculator::class)
->setArguments(['%kernel.secret%']);
->setArguments([$config['secret']]); // default to %kernel.secret%

$container->setAlias(ComponentValidatorInterface::class, ComponentValidator::class);

Expand All @@ -258,6 +263,35 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
->addTag('kernel.cache_warmer');
}

public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('live_component');
$rootNode = $treeBuilder->getRootNode();
\assert($rootNode instanceof ArrayNodeDefinition);

$rootNode
->addDefaultsIfNotSet()
->children()
->scalarNode('secret')
->info('The secret used to compute fingerprints and checksums')
->beforeNormalization()
->ifString()
->then(trim(...))
->end()
->cannotBeEmpty()
->defaultValue('%kernel.secret%')
->end()
->end()
;

return $treeBuilder;
}

public function getConfiguration(array $config, ContainerBuilder $container): ConfigurationInterface
{
return $this;
}

private function isAssetMapperAvailable(ContainerBuilder $container): bool
{
if (!interface_exists(AssetMapperInterface::class)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\UX\LiveComponent\Tests\Unit\DependencyInjection;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\Definition\Processor;
use Symfony\UX\LiveComponent\DependencyInjection\LiveComponentExtension;

class LiveComponentConfigurationTest extends TestCase
{
public function testDefaultSecret()
{
$processor = new Processor();
$config = $processor->processConfiguration(new LiveComponentExtension(), []);

$this->assertEquals('%kernel.secret%', $config['secret']);
}

public function testEmptySecretThrows()
{
$this->expectException(InvalidConfigurationException::class);
$this->expectExceptionMessage('The path "live_component.secret" cannot contain an empty value, but got null.');

$processor = new Processor();
$config = $processor->processConfiguration(new LiveComponentExtension(), [
'live_component' => [
'secret' => null,
],
]);
}

public function testCustomSecret()
{
$processor = new Processor();
$config = $processor->processConfiguration(new LiveComponentExtension(), [
'live_component' => [
'secret' => 'my_secret',
],
]);

$this->assertEquals('my_secret', $config['secret']);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\UX\LiveComponent\Tests\Unit\DependencyInjection;

use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\UX\LiveComponent\DependencyInjection\LiveComponentExtension;

class LiveComponentExtensionTest extends TestCase
{
public function testKernelSecretIsUsedByDefault(): void
{
$container = $this->createContainer();
$container->registerExtension(new LiveComponentExtension());
$container->loadFromExtension('live_component', []);
$this->compileContainer($container);

$this->assertSame('%kernel.secret%', $container->getDefinition('ux.live_component.component_hydrator')->getArgument(4));
$this->assertSame('%kernel.secret%', $container->getDefinition('ux.live_component.fingerprint_calculator')->getArgument(0));
}

public function testCustomSecretIsUsedInDefinition(): void
{
$container = $this->createContainer();
$container->registerExtension(new LiveComponentExtension());
$container->loadFromExtension('live_component', [
'secret' => 'custom_secret',
]);
$this->compileContainer($container);

$this->assertSame('custom_secret', $container->getDefinition('ux.live_component.component_hydrator')->getArgument(4));
$this->assertSame('custom_secret', $container->getDefinition('ux.live_component.fingerprint_calculator')->getArgument(0));
}

private function createContainer(): ContainerBuilder
{
$container = new ContainerBuilder(new ParameterBag([
'kernel.cache_dir' => __DIR__,
'kernel.project_dir' => __DIR__,
'kernel.charset' => 'UTF-8',
'kernel.debug' => false,
'kernel.bundles' => [],
'kernel.bundles_metadata' => [],
]));

return $container;
}

private function compileContainer(ContainerBuilder $container): void
{
$container->getCompilerPassConfig()->setOptimizationPasses([]);
$container->getCompilerPassConfig()->setRemovingPasses([]);
$container->getCompilerPassConfig()->setAfterRemovingPasses([]);
$container->compile();
}
}

0 comments on commit a641a2e

Please sign in to comment.