Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: mb_convert_encoding PHP 8.2 deprecation #52

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
"mikey179/vfsstream": "^1.6.8",
"inpsyde/php-coding-standards": "^1",
"vimeo/psalm": "@stable",
"php-stubs/wordpress-stubs": ">=6.0@stable",
"johnpbloch/wordpress-core": ">=6.0"
"php-stubs/wordpress-stubs": ">=6.2@stable",
"johnpbloch/wordpress-core": ">=6.2"
},
"autoload": {
"psr-4": {
Expand Down
7 changes: 5 additions & 2 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" backupGlobals="false" backupStaticAttributes="false" bootstrap="tests/phpunit/bootstrap.php" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false">
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd" backupGlobals="false" backupStaticAttributes="false" bootstrap="tests/phpunit/bootstrap.php" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" convertDeprecationsToExceptions="true" processIsolation="false" stopOnFailure="false">
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">src</directory>
Expand All @@ -14,7 +14,10 @@
</coverage>
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">tests/phpunit/Unit</directory>
<directory>tests/phpunit/Unit</directory>
</testsuite>
<testsuite name="Integration">
<directory>tests/phpunit/Integration</directory>
</testsuite>
</testsuites>
<logging/>
Expand Down
96 changes: 23 additions & 73 deletions src/OutputFilter/AttributesOutputFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,105 +15,55 @@

use Inpsyde\Assets\Asset;

/**
* @psalm-suppress UndefinedMethod
*/
class AttributesOutputFilter implements AssetOutputFilter
{
private const ROOT_ELEMENT_START = '<root>';
private const ROOT_ELEMENT_END = '</root>';

/**
* @param string $html
* @param Asset $asset
*
* @return string
*
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
* @psalm-suppress PossiblyFalseArgument
* @psalm-suppress ArgumentTypeCoercion
*/
public function __invoke(string $html, Asset $asset): string
{
$attributes = $asset->attributes();
if (count($attributes) === 0) {
return $html;
}

$html = $this->wrapHtmlIntoRoot($html);

$doc = new \DOMDocument();
libxml_use_internal_errors(true);
@$doc->loadHTML(
mb_convert_encoding($html, 'HTML-ENTITIES', "UTF-8"),
LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
);
libxml_clear_errors();
if (!class_exists(\WP_HTML_Tag_Processor::class)) {
// phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
trigger_error(
'Adding attributes is not supported for WordPress < 6.2',
\E_USER_DEPRECATED
);
// phpcs:enable WordPress.PHP.DevelopmentFunctions.error_log_trigger_error

$scripts = $doc->getElementsByTagName('script');
foreach ($scripts as $script) {
// Only extend <script> elements with "src" attribute
// and don't extend inline <script></script> before and after.
if (!$script->hasAttribute('src')) {
continue;
}
$this->applyAttributes($script, $attributes);
return $html;
}

return $this->removeRootElement($doc->saveHTML());
}

/**
* Wrapping multiple scripts into a root-element
* to be able to load it via DOMDocument.
*
* @param string $html
*
* @return string
*/
protected function wrapHtmlIntoRoot(string $html): string
{
return self::ROOT_ELEMENT_START . $html . self::ROOT_ELEMENT_END;
}
$tags = new \WP_HTML_Tag_Processor($html);

/**
* Remove root element and return original HTML.
*
* @param string $html
*
* @return string
* @see AttributesOutputFilter::wrapHtmlIntoRoot()
*
*/
protected function removeRootElement(string $html): string
{
$regex = '~' . self::ROOT_ELEMENT_START . '(.+?)' . self::ROOT_ELEMENT_END . '~s';
preg_match($regex, $html, $matches);
// Only extend <script> elements with "src" attribute
// and don't extend inline <script></script> before and after.
if (
$tags->next_tag(['tag_name' => 'script'])
&& (string) $tags->get_attribute('src')
) {
$this->applyAttributes($tags, $attributes);
}

return $matches[1];
return $tags->get_updated_html();
}

/**
* @param \DOMElement $script
* @param array $attributes
*
* @return void
*/
protected function applyAttributes(\DOMElement $script, array $attributes)
protected function applyAttributes(\WP_HTML_Tag_Processor $script, array $attributes): void
{
foreach ($attributes as $key => $value) {
$key = esc_attr((string) $key);
if ($script->hasAttribute($key)) {
$key = esc_attr((string)$key);
if ((string) $script->get_attribute($key)) {
continue;
}
if (is_bool($value) && !$value) {
continue;
}
$value = is_bool($value)
? esc_attr($key)
: esc_attr((string) $value);
: esc_attr((string)$value);

$script->setAttribute($key, $value);
$script->set_attribute($key, $value);
}
}
}
227 changes: 227 additions & 0 deletions tests/phpunit/Integration/OutputFilter/AttributesOutputFilterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Assets package.
*
* (c) Inpsyde GmbH
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Inpsyde\Assets\Tests\Integration\OutputFilter;

use Inpsyde\Assets\OutputFilter\AssetOutputFilter;
use Inpsyde\Assets\OutputFilter\AttributesOutputFilter;
use Inpsyde\Assets\Script;
use PHPUnit\Framework\TestCase;

/**
* @runTestsInSeparateProcesses
*/
class AttributesOutputFilterTest extends TestCase
{
public static function setUpBeforeClass(): void
{
if (!class_exists(\WP_HTML_Tag_Processor::class)) {
require ABSPATH . 'wp-includes/html-api/class-wp-html-attribute-token.php';
require ABSPATH . 'wp-includes/html-api/class-wp-html-span.php';
require ABSPATH . 'wp-includes/html-api/class-wp-html-text-replacement.php';
require ABSPATH . 'wp-includes/html-api/class-wp-html-tag-processor.php';
require ABSPATH . 'wp-includes/html-api/class-wp-html-unsupported-exception.php';
require ABSPATH . 'wp-includes/html-api/class-wp-html-active-formatting-elements.php';
require ABSPATH . 'wp-includes/html-api/class-wp-html-open-elements.php';
require ABSPATH . 'wp-includes/html-api/class-wp-html-token.php';
require ABSPATH . 'wp-includes/html-api/class-wp-html-processor-state.php';
require ABSPATH . 'wp-includes/html-api/class-wp-html-processor.php';
}

if (!function_exists('esc_attr')) {
eval('function esc_attr(string $attribute): string
{
return $attribute;
}');
}
}

public function testBasic(): void
{
$testee = new AttributesOutputFilter();

$stub = new Script('stub-script', 'https://syde.com/foo.js');

$input = '<script src="foo.js"></script>';

static::assertInstanceOf(AssetOutputFilter::class, $testee);
static::assertSame($input, $testee($input, $stub));
}

/**
* @dataProvider provideAttributes
*/
public function testRenderWithAttributes(array $attributes, array $expected, array $notExpected): void
{
$stub = new Script('stub-script', 'https://syde.com/foo.js');
$stub->withAttributes($attributes);

$input = '<script src="script.js"></script>';

$testee = new AttributesOutputFilter();
$output = $testee($input, $stub);

foreach ($expected as $test) {
static::assertStringContainsString($test, $output);
}
foreach ($notExpected as $test) {
static::assertStringNotContainsString($test, $output);
}
}

/**
* @return \Generator
*/
public function provideAttributes(): \Generator
{
yield 'string value' => [
[
'key' => 'value',
],
['key="value"'],
[],
];

yield 'integer value' => [
[
'key' => 1,
],
['key="1"'],
[],
];

yield 'bool true value' => [
[
'key' => true,
],
['key="key"'],
[],
];

yield 'bool false value' => [
[
'key' => false,
],
[],
['key="key"'],
];

yield 'overwriting src-attribute' => [
[
'key' => 'value',
'src' => 'not-allowed.js',
],
['key="value"'],
['src="not-allowed.js"'],
];
}

public function testRenderNotOverwriteExistingAttributes(): void
{
$expectedKey = 'src';
$expectedValue = 'foo.js';
$expectedAttribute = sprintf('%s="%s"', $expectedKey, $expectedValue);

$stub = new Script('stub-script', 'https://syde.com/foo.js');
// We're trying to overwrite the "src" with "bar.js".
$stub->withAttributes([$expectedKey => 'bar.js']);

$input = sprintf('<script %s></script>', $expectedAttribute);

$testee = new AttributesOutputFilter();
static::assertStringContainsString($expectedAttribute, $testee($input, $stub));
}

public function testRenderInlineScriptsNotChanged()
{
$expectedKey = 'key';
$expectedValue = 'value';
$expectedAttributes = [$expectedKey => $expectedValue];

$expectedBefore = "<script>var before = 'bar';</script>";
$expectedAfter = "<script>var after = 'bar';</script>";

$stub = new Script('stub-script', 'https://syde.com/foo.js');
$stub->withAttributes($expectedAttributes);

$input = $expectedBefore . '<script src="foo.js"></script>' . $expectedAfter;

$testee = new AttributesOutputFilter();
$output = $testee($input, $stub);
static::assertStringContainsString($expectedBefore, $output);
static::assertStringContainsString($expectedAfter, $output);
}

/**
* @dataProvider provideRenderWithInlineScripts
*/
public function testRenderWithInlineScripts(string $expectedBefore, string $expectedAfter): void
{
$stub = new Script('stub-script', 'https://syde.com/foo.js');
$stub->withAttributes(['foo' => 'bar']);

$input = $expectedBefore . '<script src="foo.js"></script>' . $expectedAfter;

$testee = new AttributesOutputFilter();
$output = $testee($input, $stub);
static::assertStringContainsString($expectedBefore, $output);
static::assertStringContainsString($expectedAfter, $output);
}

public function provideRenderWithInlineScripts(): \Generator
{
$singleLineJs = '(function(){ console.log("script with single line"); })();';
$multiLineJs = <<<JS
(function() {
console.log("script with multiple lines")
})();
JS;
$multiByteLine = '<script>(function(){ console.log("Lösungen ї 𠀋"); })();</script>';
$nonStandardUrl = '<script src="http://[::1]:5173/path/to/build/@vite/client"></script>';

yield 'before single line' => [
$singleLineJs,
'',
];

yield 'after single line' => [
'',
$singleLineJs,
];

yield 'before and after single line' => [
$singleLineJs,
$singleLineJs,
];

yield 'before multi, after single line' => [
$multiLineJs,
$singleLineJs,
];

yield 'before and after multi line' => [
$multiLineJs,
$multiLineJs,
];

yield 'before and after multibyte line' => [
$multiByteLine,
$multiByteLine,
];

yield 'before and after URL with non-alphanumeric characters' => [
$nonStandardUrl,
$nonStandardUrl,
];
}
}
Loading
Loading