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: 20 additions & 0 deletions src/Views/EscapedValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Phenix\Views;

use Stringable;

class EscapedValue implements Stringable
{
public function __construct(
protected string $value,
) {
}

public function __toString(): string
{
return e($this->value);
}
}
4 changes: 1 addition & 3 deletions src/Views/Layout.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ public function __construct(
) {
parent::__construct($template, $data);

foreach ($sections as $name => $value) {
$this->templateFactory->startSection($name, $value);
}
$this->templateFactory->inheritSections($sections);
}
}
33 changes: 25 additions & 8 deletions src/Views/TemplateFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@
namespace Phenix\Views;

use Phenix\Views\Contracts\View as ViewContract;
use Stringable;

class TemplateFactory
{
protected string|null $section;

/**
* @var array<string, string|Stringable|null>
*/
protected array $sections;

protected string|null $layout;

protected array $data;

public function __construct(
Expand Down Expand Up @@ -46,15 +53,17 @@ public function make(string $template, array $data = []): ViewContract

public function startSection(string $name, string|null $value = null): void
{
if ($value) {
$this->sections[$name] = $value;
} else {
$this->section = $name;
if ($value !== null) {
$this->sections[$name] = new EscapedValue($value);

ob_start();

$this->sections[$name] = null;
return;
}

$this->section = $name;

ob_start();

$this->sections[$name] = null;
}

public function endSection(): void
Expand All @@ -70,7 +79,15 @@ public function endSection(): void

public function yieldSection(string $name): string
{
return $this->sections[$name] ?? '';
return (string) ($this->sections[$name] ?? '');
}

/**
* @param array<string, string|Stringable|null> $sections
*/
public function inheritSections(array $sections): void
{
$this->sections = $sections;
}

public function clear(): void
Expand Down
44 changes: 44 additions & 0 deletions tests/Unit/Views/TemplateEngineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

use Phenix\Facades\View;
use Phenix\Views\Exceptions\ViewNotFoundException;
use Phenix\Views\TemplateCache;
use Phenix\Views\TemplateEngine;
use Phenix\Views\TemplateFactory;

it('render a template successfully', function (): void {
$template = new TemplateEngine();
Expand Down Expand Up @@ -108,6 +110,48 @@
expect($output)->toContain('New title');
});

it('escapes XSS in inline section value', function (): void {
$template = new TemplateEngine();
$template->clearCache();

$output = $template->view('users.index', [
'title' => '<script>alert("xss")</script>',
])->render();

expect($output)->toBeString();
expect($output)->not->toContain('<script>');
expect($output)->toContain('&lt;script&gt;');
});

it('escapes inline section values in template factory', function (): void {
$factory = new TemplateFactory(new TemplateCache());

$factory->startSection('title', '<script>alert("xss")</script>');

expect($factory->yieldSection('title'))
->toBe('&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;');
});

it('preserves rendered block section html in template factory', function (): void {
$factory = new TemplateFactory(new TemplateCache());

$factory->startSection('content');
echo '<h1>Users</h1>';
$factory->endSection();

expect($factory->yieldSection('content'))->toBe('<h1>Users</h1>');
});

it('stores empty and zero inline section values without starting a buffer', function (): void {
$factory = new TemplateFactory(new TemplateCache());

$factory->startSection('empty', '');
$factory->startSection('zero', '0');

expect($factory->yieldSection('empty'))->toBe('');
expect($factory->yieldSection('zero'))->toBe('0');
});

it('render template using facade', function (): void {
View::clearCache();

Expand Down
Loading