Skip to content

Commit 73ed971

Browse files
committed
Embeds: Validate registered oEmbed providers.
A plugin may register an oEmbed provider through the `oembed_providers` filter using a malformed structure, such as an associative array rather than the expected tuple of a provider endpoint URL string at index 0 and an optional boolean regex flag at index 1. This previously produced `Undefined array key` PHP warnings when `WP_oEmbed::get_provider()` destructured the entry. Introduce a private `sanitize_provider()` method that validates the match pattern and provider data, normalizing the optional regex flag to a boolean. The constructor now skips malformed entries and reports each one via `_doing_it_wrong()`, and `get_provider()` likewise ignores any invalid entries it encounters at runtime. Developed in #11568. Props sukhendu2002, westonruter, bradshawtm, rollybueno. Fixes #65068. git-svn-id: https://develop.svn.wordpress.org/trunk@62501 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 5d1944c commit 73ed971

2 files changed

Lines changed: 104 additions & 10 deletions

File tree

src/wp-includes/class-wp-oembed.php

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ class WP_oEmbed {
2323
* A list of oEmbed providers.
2424
*
2525
* @since 2.9.0
26-
* @var array
26+
* @var array<string, array{ 0: string, 1: bool }> An associative array mapping URL patterns to provider data.
27+
* Each entry's value is an array with the provider endpoint URL
28+
* string at index 0 and a boolean at index 1 indicating whether
29+
* the URL pattern (array key) is a regular expression.
2730
*/
2831
public $providers = array();
2932

@@ -221,9 +224,29 @@ public function __construct() {
221224
*
222225
* @since 2.9.0
223226
*
224-
* @param array[] $providers An array of arrays containing data about popular oEmbed providers.
227+
* @param array<string, array{ 0: string, 1?: bool }> $providers An associative array mapping URL patterns to
228+
* provider data. Each value must be an array
229+
* with a provider endpoint URL string at index 0
230+
* and an optional boolean regex flag at index 1.
225231
*/
226-
$this->providers = apply_filters( 'oembed_providers', $providers );
232+
$providers = (array) apply_filters( 'oembed_providers', $providers );
233+
foreach ( $providers as $match_mask => $data ) {
234+
$provider = $this->sanitize_provider( $match_mask, $data );
235+
if ( null === $provider ) {
236+
_doing_it_wrong(
237+
__METHOD__,
238+
sprintf(
239+
/* translators: 1: oembed_providers, 2: The oEmbed provider URL pattern. */
240+
__( '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.' ),
241+
'<code>oembed_providers</code>',
242+
'<code>' . esc_html( (string) $match_mask ) . '</code>'
243+
),
244+
'7.1.0'
245+
);
246+
} else {
247+
$this->providers[ $provider['match_mask'] ] = array( $provider['endpoint'], $provider['is_regex'] );
248+
}
249+
}
227250

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

272+
/**
273+
* Sanitizes and normalizes a single oEmbed provider entry.
274+
*
275+
* Validates that the match mask is a non-empty string and that the provider data
276+
* is an array with a non-empty string endpoint URL at index 0. Normalizes the
277+
* optional regex flag at index 1 to a boolean.
278+
*
279+
* @since 7.1.0
280+
*
281+
* @param array-key $match_mask The URL pattern used to match against URLs.
282+
* @param mixed $data The raw provider data to sanitize.
283+
* @return array{ match_mask: non-empty-string, endpoint: non-empty-string, is_regex: bool }|null Normalized provider array, or null if malformed.
284+
*/
285+
private function sanitize_provider( $match_mask, $data ): ?array {
286+
if (
287+
is_string( $match_mask ) &&
288+
'' !== $match_mask &&
289+
is_array( $data ) &&
290+
isset( $data[0] ) &&
291+
is_string( $data[0] ) &&
292+
'' !== $data[0]
293+
) {
294+
return array(
295+
'match_mask' => $match_mask,
296+
'endpoint' => $data[0],
297+
'is_regex' => (bool) ( $data[1] ?? false ),
298+
);
299+
}
300+
return null;
301+
}
302+
249303
/**
250304
* Takes a URL and returns the corresponding oEmbed provider's URL, if there is one.
251305
*
@@ -272,17 +326,21 @@ public function get_provider( $url, $args = '' ) {
272326
$args['discover'] = true;
273327
}
274328

275-
foreach ( $this->providers as $matchmask => $data ) {
276-
list( $providerurl, $regex ) = $data;
329+
foreach ( $this->providers as $match_mask => $data ) {
330+
$provider_data = $this->sanitize_provider( $match_mask, $data );
331+
if ( null === $provider_data ) {
332+
continue;
333+
}
334+
$match_mask = $provider_data['match_mask'];
277335

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

284-
if ( preg_match( $matchmask, $url ) ) {
285-
$provider = str_replace( '{format}', 'json', $providerurl ); // JSON is easier to deal with than XML.
342+
if ( preg_match( $match_mask, $url ) ) {
343+
$provider = str_replace( '{format}', 'json', $provider_data['endpoint'] ); // JSON is easier to deal with than XML.
286344
break;
287345
}
288346
}

tests/phpunit/tests/oembed/wpOembed.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,4 +276,40 @@ public function test_wp_filter_pre_oembed_result_multisite_restores_state_if_no_
276276
$this->assertFalse( $actual );
277277
$this->assertSame( $current_blog_id, get_current_blog_id() );
278278
}
279+
280+
/**
281+
* @ticket 65068
282+
* @covers WP_oEmbed::__construct
283+
*/
284+
public function test_malformed_provider_triggers_doing_it_wrong(): void {
285+
$filter = static function ( array $providers ): array {
286+
$providers['bad_provider'] = array(
287+
'url' => '#https?://example\.site/.*#i',
288+
'endpoint' => 'https://example.site/api/oembed',
289+
);
290+
return $providers;
291+
};
292+
293+
add_filter( 'oembed_providers', $filter );
294+
$this->setExpectedIncorrectUsage( 'WP_oEmbed::__construct' );
295+
$oembed = new WP_oEmbed();
296+
297+
$this->assertArrayNotHasKey( 'bad_provider', $oembed->providers );
298+
}
299+
300+
/**
301+
* @ticket 65068
302+
* @covers ::get_provider
303+
*/
304+
public function test_get_provider_handles_provider_without_regex_flag(): void {
305+
// Use a dedicated instance to avoid leaking the test provider into the shared singleton.
306+
$oembed = new WP_oEmbed();
307+
308+
// Provider with only index 0 set (no regex flag) — should default $regex to false.
309+
$oembed->providers['https://example.site/*'] = array( 'https://example.site/api/oembed' ); // @phpstan-ignore assign.propertyType (Intentionally omitted second item of array.)
310+
311+
$result = $oembed->get_provider( 'https://example.site/video/123' );
312+
313+
$this->assertSame( 'https://example.site/api/oembed', $result );
314+
}
279315
}

0 commit comments

Comments
 (0)