Add more context switches for inline scripts and styles

This commit is contained in:
Kijin Sung 2025-03-18 23:17:42 +09:00
parent 800eb2f444
commit 62eb6b2aae
3 changed files with 69 additions and 13 deletions

View file

@ -179,20 +179,45 @@ class TemplateParser_v2
*/
protected function _addContextSwitches(string $content): string
{
return preg_replace_callback('#(<script\b([^>]*)|</script)#i', function($match) {
// Inline styles.
$content = preg_replace_callback('#(?<=\s)(style=")([^"]*?)"#i', function($match) {
return $match[1] . '<?php $this->config->context = \'CSS\'; ?>' . $match[2] . '<?php $this->config->context = \'HTML\'; ?>"';
}, $content);
// Inline scripts.
$content = preg_replace_callback('#(?<=\s)(href="javascript:|on[a-z]+=")([^"]*?)"#i', function($match) {
return $match[1] . '<?php $this->config->context = \'JS\'; ?>' . $match[2] . '<?php $this->config->context = \'HTML\'; ?>"';
}, $content);
// <style> tags.
$content = preg_replace_callback('#(<style\b([^>]*)|</style)#i', function($match) {
if (substr($match[1], 1, 1) === '/')
{
return '<?php $this->config->context = "HTML"; ?>' . $match[1];
return '<?php $this->config->context = \'HTML\'; ?>' . $match[1];
}
else
{
return $match[1] . '<?php $this->config->context = \'CSS\'; ?>';
}
}, $content);
// <script> tags that aren't links.
$content = preg_replace_callback('#(<script\b([^>]*)|</script)#i', function($match) {
if (substr($match[1], 1, 1) === '/')
{
return '<?php $this->config->context = \'HTML\'; ?>' . $match[1];
}
elseif (!str_contains($match[2] ?? '', 'src="'))
{
return $match[1] . '<?php $this->config->context = "JS"; ?>';
return $match[1] . '<?php $this->config->context = \'JS\'; ?>';
}
else
{
return $match[0];
}
}, $content);
return $content;
}
/**
@ -203,7 +228,7 @@ class TemplateParser_v2
*/
protected static function _removeContextSwitches(string $content): string
{
return preg_replace('#<\?php \$this->config->context = "[A-Z]+"; \?>#', '', $content);
return preg_replace('#<\?php \$this->config->context = \'[A-Z]+\'; \?>#', '', $content);
}
/**

View file

@ -60,7 +60,7 @@
]); ?>></span>
</div>
<script type="text/javascript"<?php $this->config->context = "JS"; ?>>
<script type="text/javascript"<?php $this->config->context = 'JS'; ?>>
const foo = '<?php echo $this->config->context === 'JS' ? escape_js($__Context->foo ?? '') : htmlspecialchars($__Context->foo ?? '', \ENT_QUOTES, 'UTF-8', false); ?>';
const bar = <?php echo $this->config->context === 'JS' ? json_encode($__Context->bar, self::$_json_options2) : htmlspecialchars(json_encode($__Context->bar, self::$_json_options), \ENT_QUOTES, 'UTF-8', false); ?>;
<?php $this->config->context = "HTML"; ?></script>
<?php $this->config->context = 'HTML'; ?></script>

View file

@ -248,6 +248,34 @@ class TemplateParserV2Test extends \Codeception\Test\Unit
$this->assertEquals($target, $this->_parse($source));
}
public function testContextSwitches()
{
// <script> tag
$source = '<script type="text/javascript"> foobar(); </script>';
$target = '<script type="text/javascript"<?php $this->config->context = \'JS\'; ?>> foobar(); <?php $this->config->context = \'HTML\'; ?></script>';
$this->assertEquals($target, $this->_parse($source, true, false));
// Inline script in link href
$source = '<a href="javascript:void(0)">Hello</a>';
$target = '<a href="javascript:<?php $this->config->context = \'JS\'; ?>void(0)<?php $this->config->context = \'HTML\'; ?>">Hello</a>';
$this->assertEquals($target, $this->_parse($source, true, false));
// Inline script in event handler
$source = '<div class="foo" onClick="bar.barr()">Hello</div>';
$target = '<div class="foo" onClick="<?php $this->config->context = \'JS\'; ?>bar.barr()<?php $this->config->context = \'HTML\'; ?>">Hello</div>';
$this->assertEquals($target, $this->_parse($source, true, false));
// <style> tag
$source = '<style> body { font-size: 16px; } </style>';
$target = '<style<?php $this->config->context = \'CSS\'; ?>> body { font-size: 16px; } <?php $this->config->context = \'HTML\'; ?></style>';
$this->assertEquals($target, $this->_parse($source, true, false));
// Inline style
$source = '<div style="background-color: #ffffff;" class="foobar"><span></span></div>';
$target = '<div style="<?php $this->config->context = \'CSS\'; ?>background-color: #ffffff;<?php $this->config->context = \'HTML\'; ?>" class="foobar"><span></span></div>';
$this->assertEquals($target, $this->_parse($source, true, false));
}
public function testEchoStatements()
{
// Basic usage of XE-style single braces
@ -366,11 +394,6 @@ class TemplateParserV2Test extends \Codeception\Test\Unit
$target = "<?php echo escape_js(\$__Context->foo ?? ''); ?>";
$this->assertEquals($target, $this->_parse($source));
// Context-aware escape
$source = '<script type="text/javascript"> foobar(); </script>';
$target = '<script type="text/javascript"<?php $this->config->context = "JS"; ?>> foobar(); <?php $this->config->context = "HTML"; ?></script>';
$this->assertEquals($target, $this->_parse($source));
// JSON using context-aware escape
$source = '{{ $foo|json }}';
$target = implode('', [
@ -573,7 +596,7 @@ class TemplateParserV2Test extends \Codeception\Test\Unit
// Script tag with external path
$source = '<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.0.0/js/bootstrap.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>';
$target = '<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.0.0/js/bootstrap.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"><?php $this->config->context = "HTML"; ?></script>';
$target = '<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.0.0/js/bootstrap.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>';
$this->assertEquals($target, $this->_parse($source));
// Absolute URL
@ -1295,9 +1318,10 @@ class TemplateParserV2Test extends \Codeception\Test\Unit
*
* @param string $source
* @param bool $force_v2 Disable version detection
* @param bool $remove_context_switches Remove context switches that make code difficult to read
* @return string
*/
protected function _parse(string $source, bool $force_v2 = true): string
protected function _parse(string $source, bool $force_v2 = true, bool $remove_context_switches = true): string
{
$tmpl = new \Rhymix\Framework\Template('./tests/_data/template', 'empty.html');
if ($force_v2)
@ -1309,6 +1333,13 @@ class TemplateParserV2Test extends \Codeception\Test\Unit
{
$result = substr($result, strlen($this->prefix));
}
// Remove context switches.
if ($remove_context_switches)
{
$result = preg_replace('#<\?php \$this->config->context = \'[A-Z]+\'; \?>#', '', $result);
}
return $result;
}