8000 Add `Illuminate\Support\EncodedHtmlString` (#54737) · laravel/framework@d7434ea · GitHub
[go: up one dir, main page]

Skip to content

Commit d7434ea

Browse files
crynoboneStyleCIBotshaedrichtaylorotwell
authored
Add Illuminate\Support\EncodedHtmlString (#54737)
* wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * Update EncodedHtmlString.php * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * Apply fixes from StyleCI * Update EncodedHtmlString.php * Update Markdown.php * Update src/Illuminate/Mail/Markdown.php Co-authored-by: Sebastian Hädrich <11225821+shaedrich@users.noreply.github.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * formatting * formatting " --------- Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> Co-authored-by: StyleCI Bot <bot@styleci.io> Co-authored-by: Sebastian Hädrich <11225821+shaedrich@users.noreply.github.com> Co-authored-by: Taylor Otwell <taylor@laravel.com>
1 parent 88bdf47 commit d7434ea

File tree

8 files changed

+329
-11
lines changed

8 files changed

+329
-11
lines changed

src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use Illuminate\Queue\Console\WorkCommand;
2727
use Illuminate\Queue\Queue;
2828
use Illuminate\Support\Carbon;
29+
use Illuminate\Support\EncodedHtmlString;
2930
use Illuminate\Support\Facades\Facade;
3031
use Illuminate\Support\Facades\ParallelTesting;
3132
use Illuminate\Support\Once;
@@ -171,6 +172,7 @@ protected function tearDownTheTestEnvironment(): void
171172
Component::forgetFactory();
172173
ConvertEmptyStringsToNull::flushState();
173174
Factory::flushState();
175+
EncodedHtmlString::flushState();
174176
EncryptCookies::flushState();
175177
HandleExceptions::flushState();
176178
Migrator::withoutMigrations([]);

src/Illuminate/Mail/Mailable.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Illuminate\Contracts\Support\Renderable;
1414
use Illuminate\Contracts\Translation\HasLocalePreference;
1515
use Illuminate\Support\Collection;
16+
use Illuminate\Support\EncodedHtmlString;
1617
use Illuminate\Support\HtmlString;
1718
use Illuminate\Support\Str;
1819
use Illuminate\Support\Traits\Conditionable;
@@ -1371,7 +1372,7 @@ public function assertHasSubject($subject)
13711372
*/
13721373
public function assertSeeInHtml($string, $escape = true)
13731374
{
1374-
$string = $escape ? e($string) : $string;
1375+
$string = $escape ? EncodedHtmlString::convert($string, withQuote: isset($this->markdown)) : $string;
13751376

13761377
[$html, $text] = $this->renderForAssertions();
13771378

@@ -1393,7 +1394,7 @@ public function assertSeeInHtml($string, $escape = true)
13931394
*/
13941395
public function assertDontSeeInHtml($string, $escape = true)
13951396
{
1396-
$string = $escape ? e($string) : $string;
1397+
$string = $escape ? EncodedHtmlString::convert($string, withQuote: isset($this->markdown)) : $string;
13971398

13981399
[$html, $text] = $this->renderForAssertions();
13991400

@@ -1415,7 +1416,9 @@ public function assertDontSeeInHtml($string, $escape = true)
14151416
*/
14161417
public function assertSeeInOrderInHtml($strings, $escape = true)
14171418
{
1418-
$strings = $escape ? array_map('e', $strings) : $strings;
1419+
$strings = $escape ? array_map(function ($string) {
1420+
return EncodedHtmlString::convert($string, withQuote: isset($this->markdown));
1421+
}, $strings) : $strings;
14191422

14201423
[$html, $text] = $this->renderForAssertions();
14211424

src/Illuminate/Mail/Markdown.php

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Illuminate\Mail;
44

55
use Illuminate\Contracts\View\Factory as ViewFactory;
6+
use Illuminate\Support\EncodedHtmlString;
67
use Illuminate\Support\HtmlString;
78
use Illuminate\Support\Str;
89
use League\CommonMark\Environment\Environment;
@@ -60,9 +61,19 @@ public function render($view, array $data = [], $inliner = null)
6061
{
6162
$this->view->flushFinderCache();
6263

63-
$contents = $this->view->replaceNamespace(
64-
'mail', $this->htmlComponentPaths()
65-
)->make($view, $data)->render();
64+
$bladeCompiler = $this->view
65+
->getEngineResolver()
66+
->resolve('blade')
67+
->getCompiler();
68+
69+
$contents = $bladeCompiler->usingEchoFormat(
70+
'new \Illuminate\Support\EncodedHtmlString(%s)',
71+
function () use ($view, $data) {
72+
return $this->view->replaceNamespace(
73+
'mail', $this->htmlComponentPaths()
74+
)->make($view, $data)->render();
75+
}
76+
);
6677

6778
if ($this->view->exists($customTheme = Str::start($this->theme, 'mail.'))) {
6879
$theme = $customTheme;
@@ -105,16 +116,48 @@ public function renderText($view, array $data = [])
105116
*/
106117
public static function parse($text)
107118
{
108-
$environment = new Environment([
119+
EncodedHtmlString::encodeUsing(function ($value) {
120+
$replacements = [
121+
'[' => '\[',
122+
'<' => '\<',
123+
];
124+
125+
$html = str_replace(array_keys($replacements), array_values($replacements), $value);
126+
127+
return static::converter([
128+
'html_input' => 'escape',
129+
])->convert($html)->getContent();
130+
});
131+
132+
$html = '';
133+
134+
try {
135+
$html = static::converter()->convert($text)->getContent();
136+
} finally {
137+
EncodedHtmlString::flushState();
138+
}
139+
140+
return new HtmlString($html);
141+
}
142+
143+
/**
144+
* Get a Markdown converter instance.
145+
*
146+
* @internal
147+
*
148+
* @param array<string, mixed> $config
149+
* @return \League\CommonMark\MarkdownConverter
150+
*/
151+
public static function converter(array $config = [])
152+
{
153+
$environment = new Environment(array_merge([
109154
'allow_unsafe_links' => false,
110-
]);
155+
], $config));
111156

112157
$environment->addExtension(new CommonMarkCoreExtension);
113158
$environment->addExtension(new TableExtension);
114159

115-
$converter = new MarkdownConverter($environment);
116-
117-
return new HtmlString($converter->convert($text)->getContent());
160+
return new MarkdownConverter($environment);
118161
}
119162

120163
/**
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
namespace Illuminate\Support;
4+
5+
class EncodedHtmlString extends HtmlString
6+
{
7+
/**
8+
* The callback that should be used to encode the HTML strings.
9+
*
10+
* @var callable|null
11+
*/
12+
protected static $encodeUsingFactory;
13+
14+
/**
15+
* Create a new encoded HTML string instance.
16+
*
17+
* @param string $html
18+
* @param bool $doubleEncode
19+
* @return void
20+
*/
21+
public function __construct($html = '', protected bool $doubleEncode = true)
22+
{
23+
parent::__construct($html);
24+
}
25+
26+
/**
27+
* Convert the special characters in the given value.
28+
*
29+
* @internal
30+
*
31+
* @param string|null $value
32+
* @param int $withQuote
33+
* @param bool $doubleEncode
34+
* @return string
35+
*/
36+
public static function convert($value, bool $withQuote = true, bool $doubleEncode = true)
37+
{
38+
$flag = $withQuote ? ENT_QUOTES : ENT_NOQUOTES;
39+
40+
return htmlspecialchars($value ?? '', $flag | ENT_SUBSTITUTE, 'UTF-8', $doubleEncode);
41+
}
42+
43+
/**
44+
* Get the HTML string.
45+
*
46+
* @return string
47+
*/
48+
#[\Override]
49+
public function toHtml()
50+
{
51+
return (static::$encodeUsingFactory ?? function ($value, $doubleEncode) {
52+
return static::convert($value, doubleEncode: $doubleEncode);
53+
})($this->html, $this->doubleEncode);
54+
}
55+
56+
/**
57+
* Set the callable that will be used to encode the HTML strings.
58+
*
59+
* @param callable|null $factory
60+
* @return void
61+
*/
62+
public static function encodeUsing(?callable $factory = null)
63+
{
64+
static::$encodeUsingFactory = $factory;
65+
}
66+
67+
/**
68+
* Flush the class's global state.
69+
*
70+
* @return void
71+
*/
72+
public static function flushState()
73+
{
74+
static::$encodeUsingFactory = null;
75+
}
76+
}

src/Illuminate/View/Compilers/BladeCompiler.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,6 +1003,28 @@ public function precompiler(callable $precompiler)
10031003
$this->precompilers[] = $precompiler;
10041004
}
10051005

1006+
/**
1007+
* Execute the given callback using a custom echo format.
1008+
*
1009+
* @param string $format
1010+
* @param callable $callback
1011+
* @return string
1012+
*/
1013+
public function usingEchoFormat($format, callable $callback)
1014+
{
1015+
$originalEchoFormat = $this->echoFormat;
1016+
1017+
$this->setEchoFormat($format);
1018+
1019+
try {
1020+
$output = call_user_func($callback);
1021+
} finally {
1022+
$this->setEchoFormat($originalEchoFormat);
1023+
}
1024+
1025+
return $output;
1026+
}
1027+
10061028
/**
10071029
* Set the echo format to be used by the compiler.
10081030
*
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Integration\Mail;
4+
5+
use Illuminate\Mail\Mailable;
6+
use Illuminate\Mail\Mailables\Content;
7+
use Illuminate\Mail\Mailables\Envelope;
8+
use Orchestra\Testbench\TestCase;
9+
use PHPUnit\Framework\Attributes\DataProvider;
10+
11+
class MailableTest extends TestCase
12+
{
13+
/** {@inheritdoc} */
14+
#[\Override]
15+
protected function defineEnvironment($app)
16+
{
17+
$app['view']->addLocation(__DIR__.'/Fixtures');
18+
}
19+
20+
#[DataProvider('markdownEncodedDataProvider')]
21+
public function testItCanAssertMarkdownEncodedString($given, $expected)
22+
{
23+
$mailable = new class($given) extends Mailable
24+
{
25+
public function __construct(public string $message)
26+
{
27+
//
28+
}
29+
30+
public function envelope()
31+
{
32+
return new Envelope(
33+
subject: 'My basic title',
34+
);
35+
}
36+
37+
public function content()
38+
{
39+
return new Content(
40+
markdown: 'message',
41+
);
42+
}
43+
};
44+
45+
$mailable->assertSeeInHtml($expected, false);
46+
}
47+
48+
public static function markdownEncodedDataProvider()
49+
{
50+
yield ['[Laravel](https://laravel.com)', 'My message is: [Laravel](https://laravel.com)'];
51+
52+
yield [
53+
'![Welcome to Laravel](https://laravel.com/assets/img/welcome/background.svg)',
54+
'My message is: ![Welcome to Laravel](https://laravel.com/assets/img/welcome/background.svg)',
55+
];
56+
57+
yield [
58+
'Visit https://laravel.com/docs to browse the documentation',
59+
'My message is: Visit https://laravel.com/docs to browse the documentation',
60+
];
61+
62+
yield [
63+
'Visit <https://laravel.com/docs> to browse the documentation',
64+
'My message is: Visit &lt;https://laravel.com/docs&gt; to browse the documentation',
65+
];
66+
67+
yield [
68+
'Visit <span>https://laravel.com/docs</span> to browse the documentation',
69+
'My message is: Visit &lt;span&gt;https://laravel.com/docs&lt;/span&gt; to browse the documentation',
70+
];
71+
}
72+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Integration\Mail;
4+
5+
use Illuminate\Mail\Markdown;
6+
use Illuminate\Support\EncodedHtmlString;
7+
use Illuminate\Support\HtmlString;
8+
use Orchestra\Testbench\TestCase;
9+
use PHPUnit\Framework\Attributes\DataProvider;
10+
11+
class MarkdownParserTest extends TestCase
12+
{
13+
#[DataProvider('markdownDataProvider')]
14+
public function testItCanParseMarkdownString($given, $expected)
15+
{
16+
tap(Markdown::parse($given), function ($html) use ($expected) {
17+
$this->assertInstanceOf(HtmlString::class, $html);
18+
19+
$this->assertStringEqualsStringIgnoringLineEndings($expected.PHP_EOL, (string) $html);
20+
$this->assertSame((string) $html, (string) $html->toHtml());
21+
});
22+
}
23+
24+
#[DataProvider('markdownEncodedDataProvider')]
25+
public function testItCanParseMarkdownEncodedString($given, $expected)
26+
{
27+
tap(Markdown::parse($given), function ($html) use ($expected) {
28+
$this->assertInstanceOf(HtmlString::class, $html);
29+
30+
$this->assertStringEqualsStringIgnoringLineEndings($expected.PHP_EOL, (string) $html);
31+
});
32+
}
33+
34+
public static function markdownDataProvider()
35+
{
36+
yield ['[Laravel](https://laravel.com)', '<p><a href="https://laravel.com">Laravel</a></p>'];
37+
yield ['\[Laravel](https://laravel.com)', '<p>[Laravel](https://laravel.com)</p>'];
38+
yield ['![Welcome to Laravel](https://laravel.com/assets/img/welcome/background.svg)', '<p><img src="https://laravel.com/assets/img/welcome/background.svg" alt="Welcome to Laravel" /></p>'];
39+
yield ['!\[Welcome to Laravel](https://laravel.com/assets/img/welcome/background.svg)', '<p>![Welcome to Laravel](https://laravel.com/assets/img/welcome/background.svg)</p>'];
40+
yield ['Visit https://laravel.com/docs to browse the documentation', '<p>Visit https://laravel.com/docs to browse the documentation</p>'];
41+
yield ['Visit <https://laravel.com/docs> to browse the documentation', '<p>Visit <a href="https://laravel.com/docs">https://laravel.com/docs</a> to browse the documentation</p>'];
42+
yield ['Visit <span>https://laravel.com/docs</span> to browse the documentation', '<p>Visit <span>https://laravel.com/docs</span> to browse the documentation</p>'];
43+
}
44+
45+
public static function markdownEncodedDataProvider()
46+
{
47+
yield [new EncodedHtmlString('[Laravel](https://laravel.com)'), '<p>[Laravel](https://laravel.com)</p>'];
48+
49+
yield [
50+
new EncodedHtmlString('![Welcome to Laravel](https://laravel.com/assets/img/welcome/background.svg)'),
51+
'<p>![Welcome to Laravel](https://laravel.com/assets/img/welcome/background.svg)</p>',
52+
];
53+
54+
yield [
55+
new EncodedHtmlString('Visit https://laravel.com/docs to browse the documentation'),
56+
'<p>Visit https://laravel.com/docs to browse the documentation</p>',
57+
];
58+
59+
yield [
60+
new EncodedHtmlString('Visit <https://laravel.com/docs> to browse the documentation'),
61+
'<p>Visit &lt;https://laravel.com/docs&gt; to browse the documentation</p>',
62+
];
63+
64+
yield [
65+
new EncodedHtmlString('Visit <span>https://laravel.com/docs</span> to browse the documentation'),
66+
'<p>Visit &lt;span&gt;https://laravel.com/docs&lt;/span&gt; to browse the documentation</p>',
67+
];
68+
69+
yield [
70+
'![Welcome to Laravel](https://laravel.com/assets/img/welcome/background.svg)<br />'.new EncodedHtmlString('Visit <span>https://laravel.com/docs</span> to browse the documentation'),
71+
'<p><img src="https://laravel.com/assets/img/welcome/background.svg" alt="Welcome to Laravel" /><br />Visit &lt;span&gt;https://laravel.com/docs&lt;/span&gt; to browse the documentation</p>',
72+
];
73+
}
74+
}

0 commit comments

Comments
 (0)
0