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
20 changes: 16 additions & 4 deletions src/Tags/ComponentProxy.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Illuminate\View\Compilers\BladeCompiler;
use Illuminate\View\Compilers\ComponentTagCompiler;
use Illuminate\View\ComponentAttributeBag;
use Illuminate\View\DynamicComponent;
use ReflectionClass;
use Throwable;

Expand Down Expand Up @@ -36,7 +37,7 @@ public function index()
$tagCompiler = $this->makeComponentTagCompiler();
$className = $tagCompiler->componentClass($componentName);

$data = $this->params->except('component_name___')->all();
$data = $this->params->except(['component_name___', 'component_bound___'])->all();
$attributes = new ComponentAttributeBag($data);
$constructorParameters = [];

Expand Down Expand Up @@ -74,7 +75,9 @@ public function index()
$component = $className::resolve($constructorParameters + ((array) $attributes->getIterator()));
$component->withName($componentName);
$__env->startComponent($component->resolveView(), $component->data());
$component->withAttributes($attributes->getAttributes());
$component->withAttributes($className === DynamicComponent::class
? $attributes->getAttributes()
: $this->escapeBoundAttributes($attributes->getAttributes()));

if ($this->content) {
$contextData = array_merge($this->isolatedContext?->all() ?? [], [
Expand Down Expand Up @@ -110,9 +113,9 @@ public function componentSlot()
$contextData = self::$componentStack[array_key_last(self::$componentStack)][1];

$slot = $this->params->get('component_slot___');
$context = $this->params->except('component_slot___')->all();
$context = $this->params->except(['component_slot___', 'component_bound___'])->all();

$__env->slot($slot, null, $context);
$__env->slot($slot, null, $this->escapeBoundAttributes($context));

echo $this->parse($contextData);

Expand All @@ -122,6 +125,15 @@ public function componentSlot()
}
}

protected function escapeBoundAttributes(array $attributes): array
{
$bound = explode(',', $this->params->get('component_bound___', ''));

return collect($attributes)
->map(fn ($value, $key) => in_array($key, $bound) ? BladeCompiler::sanitizeComponentAttribute($value) : $value)
->all();
}

protected function handleViewException(Throwable $e, $obLevel)
{
while (ob_get_level() > $obLevel) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ protected function compileBladeComponent(ComponentNode $component, string $prefi
$params .= ' component_name___="'.$name.'"';
}

if ($bound = $this->getBoundParameterNames($component)) {
$params .= ' component_bound___="'.implode(',', $bound).'"';
}

if ($component->isClosingTag && ! $component->isSelfClosing) {
return "{{ /%component_proxy:{$tagMethod} }}";
}
Expand All @@ -51,6 +55,15 @@ protected function compileBladeComponent(ComponentNode $component, string $prefi
return $open.$innerContent.$close;
}

protected function getBoundParameterNames(ComponentNode $component): array
{
return collect($component->parameters)
->filter(fn ($parameter) => in_array($parameter->type, [ParameterType::DynamicVariable, ParameterType::ShorthandDynamicVariable]))
->map(fn ($parameter) => $parameter->materializedName)
->values()
->all();
}

protected function getParamValue(string $value): string
{
return Str::replace('"', '\\"', $value);
Expand Down
97 changes: 97 additions & 0 deletions tests/Antlers/Components/AttributeEscapingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

namespace Tests\Antlers\Components;

use Illuminate\Support\Facades\Blade;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use Tests\Antlers\ParserTestCase;

class AttributeEscapingTest extends ParserTestCase
{
protected $value = 'a "b" <c> & d';

public static function componentProvider()
{
return [
'antlers component' => ['escaped_attributes'],
'blade component' => ['escaped_attributes_blade'],
];
}

#[Test]
#[DataProvider('componentProvider')]
public function bound_attributes_are_escaped($component)
{
$this->assertSame(
'<div data-value="a &quot;b&quot; &lt;c&gt; &amp; d"></div>',
trim($this->renderString('<x-'.$component.' :data-value="value" />', ['value' => $this->value]))
);
}

#[Test]
#[DataProvider('componentProvider')]
public function shorthand_bound_attributes_are_escaped($component)
{
$this->assertSame(
'<div value="a &quot;b&quot; &lt;c&gt; &amp; d"></div>',
trim($this->renderString('<x-'.$component.' :$value />', ['value' => $this->value]))
);
}

#[Test]
#[DataProvider('componentProvider')]
public function bound_attributes_match_blade($component)
{
$this->assertSame(
trim(Blade::render('<x-'.$component.' :data-value="$value" />', ['value' => $this->value])),
trim($this->renderString('<x-'.$component.' :data-value="value" />', ['value' => $this->value]))
);
}

#[Test]
#[DataProvider('componentProvider')]
public function literal_attributes_are_not_escaped_again($component)
{
$this->assertSame(
'<div data-value="a &amp; b"></div>',
trim($this->renderString('<x-'.$component.' data-value="a &amp; b" />'))
);
}

#[Test]
#[DataProvider('componentProvider')]
public function bound_props_are_not_escaped($component)
{
$this->assertSame(
'<div >a "b" <c> & d</div>',
trim($this->renderString('<x-'.$component.' :title="value" />', ['value' => $this->value]))
);
}

#[Test]
public function bound_slot_attributes_are_escaped()
{
$template = <<<'EOT'
<x-named_slots>
<x-slot:header :data-value="value">Header</x-slot:header>
<x-slot:footer>Footer</x-slot:footer>
</x-named_slots>
EOT;

$this->assertStringContainsString(
'<div data-value="a &quot;b&quot; &lt;c&gt; &amp; d">',
$this->renderString($template, ['value' => $this->value])
);
}

#[Test]
#[DataProvider('componentProvider')]
public function dynamic_component_attributes_are_escaped_once($component)
{
$this->assertSame(
'<div data-value="a &quot;b&quot; &lt;c&gt; &amp; d"></div>',
trim($this->renderString('<x-dynamic-component component="'.$component.'" :data-value="value" />', ['value' => $this->value]))
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@props(['title' => null])
<div {{ attributes }}>{{ title }}</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@props(['title' => null])
<div {{ $attributes }}>{!! $title !!}</div>
Loading