Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2a76573
Fix: Add validation for oEmbed provider data to prevent PHP warnings
Sukhendu2002 Apr 14, 2026
506e752
fix: Default regex to false for oEmbed providers missing index 1
Sukhendu2002 Apr 15, 2026
92839da
Merge branch 'trunk' into fix/65068-oembed-provider-validation
Sukhendu2002 Apr 15, 2026
f826ddb
Merge branch 'trunk' into fix/65068-oembed-provider-validation
Sukhendu2002 Apr 21, 2026
1684c57
fix: Validate oEmbed provider data in constructor with _doing_it_wron…
Sukhendu2002 Apr 21, 2026
b301211
fix(oembed): validate provider data before populating list
Sukhendu2002 Apr 22, 2026
0f32baf
Merge branch 'trunk' into fix/65068-oembed-provider-validation
Sukhendu2002 Apr 22, 2026
e98037a
update PHPDoc for tests
Sukhendu2002 Apr 22, 2026
8427433
Merge branch 'trunk' into fix/65068-oembed-provider-validation
Sukhendu2002 Apr 23, 2026
de917d2
fix: Introduce sanitize_provider() to validate and normalize oEmbed p…
Sukhendu2002 Apr 23, 2026
cfd617e
Merge branch 'trunk' into fix/65068-oembed-provider-validation
Sukhendu2002 Apr 24, 2026
d7868e4
fix: Validate and normalize both match_mask and provider data in sani…
Sukhendu2002 Apr 24, 2026
e269848
Add unserscore to matchmask variable
westonruter Apr 24, 2026
cb226d0
Clarify _doing_it_wrong() message
westonruter Apr 24, 2026
de6d12b
Merge branch 'trunk' into fix/65068-oembed-provider-validation
westonruter Jun 15, 2026
e494345
Fix PHPStan rule level 10 errors
westonruter Jun 15, 2026
833fad9
Use more appropriate type for arg
westonruter Jun 15, 2026
ab1eacf
Remove unneessary remove_filter()
westonruter Jun 15, 2026
539c53a
Remove another unnecessary remove_filter()
westonruter Jun 15, 2026
de13121
Revert "Remove another unnecessary remove_filter()"
westonruter Jun 15, 2026
f51d63e
Use a dedicated WP_oEmbed instance in provider test
westonruter Jun 15, 2026
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
78 changes: 68 additions & 10 deletions src/wp-includes/class-wp-oembed.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ class WP_oEmbed {
* A list of oEmbed providers.
*
* @since 2.9.0
* @var array
* @var array<string, array{ 0: string, 1: bool }> An associative array mapping URL patterns to provider data.
* Each entry's value is an array with the provider endpoint URL
* string at index 0 and a boolean at index 1 indicating whether
* the URL pattern (array key) is a regular expression.
*/
public $providers = array();

Expand Down Expand Up @@ -221,9 +224,29 @@ public function __construct() {
*
* @since 2.9.0
*
* @param array[] $providers An array of arrays containing data about popular oEmbed providers.
* @param array<string, array{ 0: string, 1?: bool }> $providers An associative array mapping URL patterns to
Comment thread
Sukhendu2002 marked this conversation as resolved.
* provider data. Each value must be an array
* with a provider endpoint URL string at index 0
* and an optional boolean regex flag at index 1.
*/
$this->providers = apply_filters( 'oembed_providers', $providers );
$providers = (array) apply_filters( 'oembed_providers', $providers );
foreach ( $providers as $match_mask => $data ) {
$provider = $this->sanitize_provider( $match_mask, $data );
if ( null === $provider ) {
_doing_it_wrong(
__METHOD__,
sprintf(
/* translators: 1: oembed_providers, 2: The oEmbed provider URL pattern. */
__( 'The oEmbed provider data returned by the %1$s filter at key %2$s is malformed. The providers array must be a mapping of provider URL patterns to a tuple array consisting of a provider endpoint URL string at index 0 and an optional boolean regex flag at index 1.' ),
'<code>oembed_providers</code>',
'<code>' . esc_html( (string) $match_mask ) . '</code>'
),
'7.1.0'
);
} else {
$this->providers[ $provider['match_mask'] ] = array( $provider['endpoint'], $provider['is_regex'] );
}
}

// Fix any embeds that contain new lines in the middle of the HTML which breaks wpautop().
add_filter( 'oembed_dataparse', array( $this, '_strip_newlines' ), 10, 3 );
Expand All @@ -246,6 +269,37 @@ public function __call( $name, $arguments ) {
return false;
}

/**
* Sanitizes and normalizes a single oEmbed provider entry.
*
* Validates that the match mask is a non-empty string and that the provider data
* is an array with a non-empty string endpoint URL at index 0. Normalizes the
* optional regex flag at index 1 to a boolean.
*
* @since 7.1.0
*
* @param array-key $match_mask The URL pattern used to match against URLs.
* @param mixed $data The raw provider data to sanitize.
* @return array{ match_mask: non-empty-string, endpoint: non-empty-string, is_regex: bool }|null Normalized provider array, or null if malformed.
*/
private function sanitize_provider( $match_mask, $data ): ?array {
if (
is_string( $match_mask ) &&
'' !== $match_mask &&
is_array( $data ) &&
isset( $data[0] ) &&
is_string( $data[0] ) &&
'' !== $data[0]
) {
return array(
'match_mask' => $match_mask,
'endpoint' => $data[0],
'is_regex' => (bool) ( $data[1] ?? false ),
);
}
return null;
}

/**
* Takes a URL and returns the corresponding oEmbed provider's URL, if there is one.
*
Expand All @@ -272,17 +326,21 @@ public function get_provider( $url, $args = '' ) {
$args['discover'] = true;
}

foreach ( $this->providers as $matchmask => $data ) {
list( $providerurl, $regex ) = $data;
foreach ( $this->providers as $match_mask => $data ) {
$provider_data = $this->sanitize_provider( $match_mask, $data );
if ( null === $provider_data ) {
continue;
}
$match_mask = $provider_data['match_mask'];

// Turn the asterisk-type provider URLs into regex.
if ( ! $regex ) {
$matchmask = '#' . str_replace( '___wildcard___', '(.+)', preg_quote( str_replace( '*', '___wildcard___', $matchmask ), '#' ) ) . '#i';
$matchmask = preg_replace( '|^#http\\\://|', '#https?\://', $matchmask );
if ( ! $provider_data['is_regex'] ) {
$match_mask = '#' . str_replace( '___wildcard___', '(.+)', preg_quote( str_replace( '*', '___wildcard___', $match_mask ), '#' ) ) . '#i';
$match_mask = (string) preg_replace( '|^#http\\\://|', '#https?\://', $match_mask );
}

if ( preg_match( $matchmask, $url ) ) {
$provider = str_replace( '{format}', 'json', $providerurl ); // JSON is easier to deal with than XML.
if ( preg_match( $match_mask, $url ) ) {
$provider = str_replace( '{format}', 'json', $provider_data['endpoint'] ); // JSON is easier to deal with than XML.
break;
}
}
Expand Down
36 changes: 36 additions & 0 deletions tests/phpunit/tests/oembed/wpOembed.php
Original file line number Diff line number Diff line change
Expand Up @@ -276,4 +276,40 @@ public function test_wp_filter_pre_oembed_result_multisite_restores_state_if_no_
$this->assertFalse( $actual );
$this->assertSame( $current_blog_id, get_current_blog_id() );
}

/**
* @ticket 65068
* @covers WP_oEmbed::__construct
*/
public function test_malformed_provider_triggers_doing_it_wrong(): void {
$filter = static function ( array $providers ): array {
$providers['bad_provider'] = array(
'url' => '#https?://example\.site/.*#i',
'endpoint' => 'https://example.site/api/oembed',
);
return $providers;
};

add_filter( 'oembed_providers', $filter );
$this->setExpectedIncorrectUsage( 'WP_oEmbed::__construct' );
$oembed = new WP_oEmbed();

$this->assertArrayNotHasKey( 'bad_provider', $oembed->providers );
}

/**
* @ticket 65068
* @covers ::get_provider
*/
public function test_get_provider_handles_provider_without_regex_flag(): void {
// Use a dedicated instance to avoid leaking the test provider into the shared singleton.
$oembed = new WP_oEmbed();

// Provider with only index 0 set (no regex flag) — should default $regex to false.
$oembed->providers['https://example.site/*'] = array( 'https://example.site/api/oembed' ); // @phpstan-ignore assign.propertyType (Intentionally omitted second item of array.)

$result = $oembed->get_provider( 'https://example.site/video/123' );

$this->assertSame( 'https://example.site/api/oembed', $result );
}
}
Loading