diff --git a/composer.json b/composer.json index 38a2049..ef68ec2 100644 --- a/composer.json +++ b/composer.json @@ -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": { @@ -45,8 +45,7 @@ }, "autoload-dev": { "psr-4": { - "Inpsyde\\Assets\\Tests\\Unit\\": "tests/phpunit/Unit/", - "Inpsyde\\Assets\\Tests\\Integration\\": "tests/phpunit/Integration/" + "Inpsyde\\Assets\\Tests\\Unit\\": "tests/phpunit/Unit/" } }, "scripts": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 0ade292..5635080 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,5 +1,5 @@ - + src @@ -14,7 +14,7 @@ - tests/phpunit/Unit + tests/phpunit/Unit diff --git a/src/OutputFilter/AttributesOutputFilter.php b/src/OutputFilter/AttributesOutputFilter.php index 8aec887..e2c995a 100644 --- a/src/OutputFilter/AttributesOutputFilter.php +++ b/src/OutputFilter/AttributesOutputFilter.php @@ -15,95 +15,34 @@ use Inpsyde\Assets\Asset; -/** - * @psalm-suppress UndefinedMethod - */ class AttributesOutputFilter implements AssetOutputFilter { - private const ROOT_ELEMENT_START = ''; - private const ROOT_ELEMENT_END = ''; - - /** - * @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) { + if (!class_exists(\WP_HTML_Tag_Processor::class) || 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(); + $tags = new \WP_HTML_Tag_Processor($html); - $scripts = $doc->getElementsByTagName('script'); - foreach ($scripts as $script) { - // Only extend before and after. - if (!$script->hasAttribute('src')) { - continue; - } - $this->applyAttributes($script, $attributes); + // Only extend before and after. + if ( + $tags->next_tag(['tag_name' => 'script']) + && (string) $tags->get_attribute('src') + ) { + $this->applyAttributes($tags, $attributes); } - 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; - } - - /** - * 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); - - 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)) { + if ((string) $script->get_attribute($key)) { continue; } if (is_bool($value) && !$value) { @@ -113,7 +52,7 @@ protected function applyAttributes(\DOMElement $script, array $attributes) ? esc_attr($key) : esc_attr((string) $value); - $script->setAttribute($key, $value); + $script->set_attribute($key, $value); } } } diff --git a/tests/phpunit/Unit/OutputFilter/AttributesOutputFilterTest.php b/tests/phpunit/Unit/OutputFilter/AttributesOutputFilterTest.php deleted file mode 100644 index c89ff98..0000000 --- a/tests/phpunit/Unit/OutputFilter/AttributesOutputFilterTest.php +++ /dev/null @@ -1,222 +0,0 @@ -expects('attributes')->andReturn([]); - - $input = ''; - - static::assertInstanceOf(AssetOutputFilter::class, $testee); - static::assertSame($input, $testee($input, $stub)); - } - - /** - * @param array $attributes - * @param array $expected - * @param array $notExpected - * - * @test - * @dataProvider provideAttributes - */ - public function testRenderWithAttributes(array $attributes, array $expected, array $notExpected) - { - // esc_attr always returns a string, even if we input an integer. - expect('esc_attr')->andReturnUsing( - static function ($input): string { - return (string) $input; - } - ); - - $stub = \Mockery::mock(Asset::class); - $stub->expects('attributes')->andReturn($attributes); - - $input = ''; - - $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"'], - ]; - } - - /** - * @test - */ - public function testRenderNotOverwriteExistingAttributes() - { - $expectedKey = 'src'; - $expectedValue = 'foo.js'; - $expectedAttribute = sprintf('%s="%s"', $expectedKey, $expectedValue); - - expect('esc_attr')->andReturnFirstArg(); - - $stub = \Mockery::mock(Asset::class); - // We're trying to overwrite the "src" with "bar.js". - $stub->expects('attributes')->andReturn([$expectedKey => 'bar.js']); - - $input = sprintf('', $expectedAttribute); - - $testee = new AttributesOutputFilter(); - static::assertStringContainsString($expectedAttribute, $testee($input, $stub)); - } - - /** - * @test - */ - public function testRenderInlineScriptsNotChanged() - { - $expectedKey = 'key'; - $expectedValue = 'value'; - $expectedAttributes = [$expectedKey => $expectedValue]; - - $expectedBefore = ""; - $expectedAfter = ""; - - expect('esc_attr')->andReturnFirstArg(); - - $stub = \Mockery::mock(Asset::class); - $stub->expects('attributes')->andReturn($expectedAttributes); - - $input = $expectedBefore . '' . $expectedAfter; - - $testee = new AttributesOutputFilter(); - $output = $testee($input, $stub); - static::assertStringContainsString($expectedBefore, $output); - static::assertStringContainsString($expectedAfter, $output); - } - - /** - * @param string $expectedBefore - * @param string $expectedAfter - * - * @test - * @dataProvider provideRenderWithInlineScripts - */ - public function testRenderWithInlineScripts(string $expectedBefore, string $expectedAfter) - { - expect('esc_attr')->andReturnFirstArg(); - $stub = \Mockery::mock(Asset::class); - $stub->expects('attributes')->andReturn(['foo' => 'bar']); - - $input = $expectedBefore . '' . $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 = << [ - $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, - ]; - } -}