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
75 changes: 74 additions & 1 deletion plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ private function reduce_layout_shifts( OD_Tag_Visitor_Context $context ): void {
foreach ( $minimums as $minimum ) {
$style_rule = sprintf(
'#%s { min-height: %dpx; }',
$element_id,
$this->escape_css( $element_id ),
$minimum['height']
);

Expand All @@ -206,6 +206,79 @@ private function reduce_layout_shifts( OD_Tag_Visitor_Context $context ): void {
}
}

/**
* Escapes a CSS identifier.
*
* This is a PHP implementation of the CSS.escape() method in CSSOM, based on the
* JavaScript polyfill by Mathias Bynens.
*
* @since 1.0.0
* @link https://drafts.csswg.org/cssom/#the-css.escape()-method
* @link https://github.com/mathiasbynens/CSS.escape
* @link https://mathiasbynens.be/notes/css-escapes
* @license MIT
*
* @param string $ident Identifier to escape.
* @return string Escaped identifier.
*/
private function escape_css( string $ident ): string {
$length = strlen( $ident );
$result = '';
$first_code_unit = $length > 0 ? ord( $ident[0] ) : 0;

for ( $i = 0; $i < $length; $i++ ) {
$code_unit = ord( $ident[ $i ] );

// If the character is NULL (U+0000), then the REPLACEMENT CHARACTER (U+FFFD).
if ( 0x0000 === $code_unit ) {
$result .= "\u{FFFD}";
continue;
}

if (
// If the character is in the range [\1-\1f] (U+0001 to U+001F) or is U+007F...
$code_unit <= 0x001F || 0x007F === $code_unit ||
// If the character is the first character and is in the range [0-9] (U+0030 to U+0039)...
( 0 === $i && $code_unit >= 0x0030 && $code_unit <= 0x0039 ) ||
// If the character is the second character and is in the range [0-9] (U+0030 to U+0039) and the first character is a `-` (U+002D)...
( 1 === $i && $code_unit >= 0x0030 && $code_unit <= 0x0039 && 0x002D === $first_code_unit )
) {
$result .= '\\' . dechex( $code_unit ) . ' ';
continue;
}

// If the character is the first character and is a `-` (U+002D), and there is no second character...
if (
0 === $i &&
1 === $length &&
0x002D === $code_unit
) {
$result .= '\\' . $ident[ $i ];
continue;
}

// If the character is not handled by one of the above rules and is
// greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or
// is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to
// U+005A), or [a-z] (U+0061 to U+007A)...
if (
$code_unit >= 0x0080 ||
0x002D === $code_unit ||
0x005F === $code_unit ||
( $code_unit >= 0x0030 && $code_unit <= 0x0039 ) ||
( $code_unit >= 0x0041 && $code_unit <= 0x005A ) ||
( $code_unit >= 0x0061 && $code_unit <= 0x007A )
) {
$result .= $ident[ $i ];
continue;
}

// Otherwise, the escaped character.
$result .= '\\' . $ident[ $i ];
}
return $result;
}

/**
* Gets preconnect URLs based on embed type.
*
Expand Down
4 changes: 2 additions & 2 deletions plugins/embed-optimizer/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Description: Optimizes the performance of embeds through lazy-loading, preconnecting, and reserving space to reduce layout shifts.
* Requires at least: 6.6
* Requires PHP: 7.2
* Version: 1.0.0-beta3
* Version: 1.0.0-beta4
* Author: WordPress Performance Team
* Author URI: https://make.wordpress.org/performance/
* License: GPLv2 or later
Expand Down Expand Up @@ -71,7 +71,7 @@ static function ( string $global_var_name, string $version, Closure $load ): voi
}
)(
'embed_optimizer_pending_plugin',
'1.0.0-beta3',
'1.0.0-beta4',
static function ( string $version ): void {
if ( defined( 'EMBED_OPTIMIZER_VERSION' ) ) {
return;
Expand Down
8 changes: 7 additions & 1 deletion plugins/embed-optimizer/readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Contributors: wordpressdotorg
Tested up to: 6.9
Stable tag: 1.0.0-beta3
Stable tag: 1.0.0-beta4
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Tags: performance, embeds, optimization-detective
Expand Down Expand Up @@ -67,6 +67,12 @@ The [plugin source code](https://github.com/WordPress/performance/tree/trunk/plu

== Changelog ==

= 1.0.0-beta4 =

**Security**

* Add escaping for ID selector in styles added to reduce layout shifts. This fixes an XSS security vulnerability which required an authenticated user with at least a contributor role. Props to [duc193](https://github.com/nduc193) for [responsible disclosure](https://github.com/WordPress/performance/blob/trunk/SECURITY.md). ([2397](https://github.com/WordPress/performance/pull/2397))

= 1.0.0-beta3 =

**Enhancements**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>...</title>
<meta name="generator" content="optimization-detective 0.0.0">
<meta name="generator" content="embed-optimizer 0.0.0">
</head>
<body>
<div class="wp-site-blocks">
<figure
id="foo[bar][baz]"
class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio">
<div class="wp-block-embed__wrapper">
<iframe title="Introduction to WordPress Playground landing page" width="500" height="281"
src="https://www.youtube.com/embed/fVj1sze2kAY?feature=oembed" frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div>
</figure>

<figure
id="a.b.c"
class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio">
<div class="wp-block-embed__wrapper">
<iframe title="Previewing GitHub branches with WordPress Playground" width="500" height="281"
src="https://www.youtube.com/embed/2VQkCPYyabQ?feature=oembed" frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div>
</figure>

<figure
id="hover:text-blue-500"
class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio">
<div class="wp-block-embed__wrapper">
<iframe title="Using WordPress Playground to work with AI agents" width="500" height="281"
src="https://www.youtube.com/embed/r9eXlgUPCV4?feature=oembed" frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div>
</figure>
</div>
</body>
</html>

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php
return static function ( Test_Embed_Optimizer_Optimization_Detective $test_case ): void {
$test_case->populate_url_metrics(
array(
array(
'xpath' => '/HTML/BODY/DIV[@class=\'wp-site-blocks\']/*[1][self::FIGURE]/*[1][self::DIV]',
'isLCP' => true,
'intersectionRatio' => 1,
'resizedBoundingClientRect' => array_merge( $test_case->get_sample_dom_rect(), array( 'height' => 500 ) ),
),
array(
'xpath' => '/HTML/BODY/DIV[@class=\'wp-site-blocks\']/*[2][self::FIGURE]/*[1][self::DIV]',
'isLCP' => true,
'intersectionRatio' => 1,
'resizedBoundingClientRect' => array_merge( $test_case->get_sample_dom_rect(), array( 'height' => 500 ) ),
),
array(
'xpath' => '/HTML/BODY/DIV[@class=\'wp-site-blocks\']/*[3][self::FIGURE]/*[1][self::DIV]',
'isLCP' => true,
'intersectionRatio' => 1,
'resizedBoundingClientRect' => array_merge( $test_case->get_sample_dom_rect(), array( 'height' => 500 ) ),
),
)
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php
/**
* Tests for Embed_Optimizer_Tag_Visitor.
*
* @package embed-optimizer
*
* @coversDefaultClass Embed_Optimizer_Tag_Visitor
*/
class Test_Embed_Optimizer_Tag_Visitor_Escape_CSS extends WP_UnitTestCase {

/**
* Runs the routine before each test is executed.
*/
public function set_up(): void {
parent::set_up();
require_once dirname( __DIR__ ) . '/class-embed-optimizer-tag-visitor.php';
}

/**
* Tests escape_css().
*
* @covers ::escape_css
*
* @dataProvider data_escape_css
*
* @param string $ident Identifier to escape.
* @param string $expected Expected escaped identifier.
*/
public function test_escape_css( string $ident, string $expected ): void {
$visitor = new Embed_Optimizer_Tag_Visitor();
$method = new ReflectionMethod( $visitor, 'escape_css' );
$method->setAccessible( true );

$this->assertSame( $expected, $method->invoke( $visitor, $ident ) );
}

/**
* Data provider for test_escape_css.
*
* @return array<string, array{string, string}> Test cases.
*/
public function data_escape_css(): array {
return array(
'empty' => array( '', '' ),
'simple' => array( 'foo', 'foo' ),
'brackets' => array( 'foo[bar][baz]', 'foo\[bar\]\[baz\]' ),
'hyphen' => array( '-', '\-' ),
'unicode' => array( '🌈', '🌈' ),
'double-hyphen' => array( '--', '--' ),
'hyphen-digit' => array( '-1', '-\31 ' ),
'digit' => array( '1', '\31 ' ),
'digit-alpha' => array( '1a', '\31 a' ),
'dot' => array( '.foo', '\.foo' ),
'hash' => array( '#bar', '\#bar' ),
'space' => array( ' ', '\ ' ),
'null' => array( "\0", "\xEF\xBF\xBD" ), // U+FFFD is EF BF BD in UTF-8.
'control' => array( "\x1F", '\1f ' ),
'delete' => array( "\x7F", '\7f ' ),
'backslash' => array( '\\', '\\\\' ),
'underscore' => array( '_', '_' ),
'mixed' => array( 'foo-bar_baz', 'foo-bar_baz' ),
'unicode-multibyte' => array( 'é', 'é' ),
'complex' => array( '1.2#3-4_5', '\31 \.2\#3-4_5' ),
);
}
}
Loading