oEmbed in WordPress

oEmbed is an open format created to simplify embedding content from one web page into another. Content can include: photos, videos, links, and other types of content.

oEmbed Content is video, audio, HTML, and other codes on your site that have been embedded from another site. For example, if you insert a link to a YouTube video in WP, it will be transformed into an iframe with the video.

How It Works

The WP_Embed{} class is based on the WP_oEmbed{} class - it contains the list of registered oEmbed providers and is responsible for requesting (retrieving) and processing the Discovery link in the HTML head section.

At a very early stage, even before the mu_plugin_loaded hook, the WP_Embed class is initialized:

$GLOBALS['wp_embed'] = new WP_Embed();

The following hooks are created in the constructor WP_Embed::__construct():

function __construct() {
	// Hack to get the [embed] shortcode to run before wpautop().
	add_filter( 'the_content', array( $this, 'run_shortcode' ), 8 );
	add_filter( 'widget_text_content', array( $this, 'run_shortcode' ), 8 );

	// Shortcode placeholder for strip_shortcodes().
	add_shortcode( 'embed', '__return_false' );

	// Attempts to embed all URLs in a post.
	add_filter( 'the_content', array( $this, 'autoembed' ), 8 );
	add_filter( 'widget_text_content', array( $this, 'autoembed' ), 8 );

	// After a post is saved, cache oEmbed items via Ajax.
	add_action( 'edit_form_advanced', array( $this, 'maybe_run_ajax_cache' ) );
	add_action( 'edit_page_form', array( $this, 'maybe_run_ajax_cache' ) );
}

Frontend oEmbed URL Processing

The post or widget text content is checked for the presence of the shortcode [embed] or a URL on a separate line. For this, as seen from the code above, two methods are triggered on the the_content hook:

  1. WP_Embed::run_shortcode( $post_content ) — processes the [embed] shortcode.

  2. WP_Embed::autoembed( $post_content ) — processes the URL (on a separate line) as the [embed] shortcode.

    Then, each found URL (on a separate line or in a shortcode) is passed to the method WP_Embed::shortcode( $attr, $url ). Then:

    1. Internal Handler is Checked | Embed

      The URL is passed to the method WP_Embed::get_embed_handler_html( $rawattr, $url ). This method checks the passed URL for internal handlers, which are registered by the function wp_embed_register_handler().

      1. If a handler is found, the URL is passed to the handler function and the result is returned. The WP_Embed::shortcode() method's operation ends here.

      2. If no handler is found, WP_Embed::shortcode() continues to check for oEmbed (external) handlers.
    2. External Handler is Checked | oEmbed

      1. If there is a cache for the URL, the parsing is interrupted and the result is taken from the cache.
      2. If there is no cache, the URL is passed to the function wp_oembed_get( $url, $attr ), then to the method WP_oEmbed::get_provider(), and then all registered providers (WP_oEmbed::$providers) are compared with this URL. If there is a match, a request is made to obtain the oEmbed insertion from another site (the WP_oEmbed class is responsible for this request). In the end, regardless of the result, the URL processing is cached.

        With this approach, the cache is only created on the first request (when it doesn't exist) and is not updated. Cache updating only occurs when the post is updated in the admin panel.

Admin Panel oEmbed URL Processing

oEmbed caching is also created and (important) updated when the post is updated on the post editing page in the admin panel. This is done via an AJAX request. Such an AJAX request is created only when the post is updated in the admin panel - see WP_Embed::maybe_run_ajax_cache().

For example, let's consider how WordPress processes links to other sites on WordPress to embed content from another site. The general principle of all this is described above, and below we'll look at how it works in the admin panel.

Block Editor

When a link is inserted into the block editor, a JS event triggers an AJAX GET request to the route domain/wp-json/oembed/1.0/proxy, which registers the method WP_oEmbed_Controller::register_routes(). Example of the sent data:

url: https://wp-punk.com/how-to-deal-with-date-and-time-in-wordpress/
_locale: user

The response is generated by the method WP_oEmbed_Controller::get_proxy_item():

{
	"version": "1.0",
	"provider_name": "WP Punk",
	"provider_url": "https://wp-punk.com",
	"author_name": "Max Denisenko",
	"author_url": "https://wp-punk.com/author/mxadmin/",
	"title": "How to deal with Date and Time in WordPress",
	"type": "rich",
	"width": 600,
	"height": 338,
	"html": "
		<blockquote class=\"wp-embedded-content\" data-secret=\"DGdp4yAkb8\">
			<a href=\"https://wp-punk.com/how-to-deal-with-date-and-time-in-wordpress/\">How to deal with Date and Time in WordPress</a>
		</blockquote>
		<iframe
			sandbox=\"allow-scripts\"
			security=\"restricted\"
			src=\"https://wp-punk.com/how-to-deal-with-date-and-time-in-wordpress/embed/#?secret=DGdp4yAkb8\"
			width=\"600\"
			height=\"338\"
			title=\"“How to deal with Date and Time in WordPress” — WP Punk\"
			data-secret=\"DGdp4yAkb8\"
			frameborder=\"0\"
			marginwidth=\"0\"
			marginheight=\"0\"
			scrolling=\"no\"
			class=\"wp-embedded-content\">
		</iframe>
	",
	"thumbnail_url": "https://wp-punk.com/wp-content/uploads/2023/01/Date-and-Time.jpg",
	"thumbnail_width": 600,
	"thumbnail_height": 338
}

The data is cached in the *_options table.

Visual Editor

When using the visual editor and inserting a link, a JS event triggers a POST AJAX request to the file admin-ajax.php, where the function wp_ajax_parse_embed() works on the wp_ajax_parse_embed hook.

Example of sent data:

post_ID: 31
type: embed
shortcode: [embed]https://oddstyle.ru/instrukciya-po-rabote-s-wordpress-rukovodstvo-dlya-novichkov[/embed]
maxwidth: 549
action: parse-embed

The content of the shortcode field is passed to WP_Embed::run_shortcode(). Read above in the section "How It Works" for what happens next.

Example of returned data:

{
	"success":true,
	"data":{
		"body":"
			<blockquote class=\"wp-embedded-content\" data-secret=\"DGdp4yAkb8\">
				<a href=\"https://wp-punk.com/how-to-deal-with-date-and-time-in-wordpress/\">How to deal with Date and Time in WordPress</a>
			</blockquote>
			<iframe
				sandbox=\"allow-scripts\"
				security=\"restricted\"
				src=\"https://wp-punk.com/how-to-deal-with-date-and-time-in-wordpress/embed/#?secret=DGdp4yAkb8\"
				width=\"600\"
				height=\"338\"
				title=\"“How to deal with Date and Time in WordPress” — WP Punk\"
				data-secret=\"DGdp4yAkb8\"
				frameborder=\"0\"
				marginwidth=\"0\"
				marginheight=\"0\"
				scrolling=\"no\"
				class=\"wp-embedded-content\">
			</iframe>
		",
		"attr":{
			"width":600,
			"height":338
		},
		"head":"<script src=\"https://wp-test.ru/wp-includes/js/wp-embed.js\"></script>",
		"sandbox":true
	}
}

The data is cached in the wp_postmeta or wp_posts table.

oEmbed Request Caching

Post meta fields or the wp_posts table can be used for caching requests.

If the shortcode or link is called from the post content, the cache will be stored in the meta fields of the current post.

If the current post does not exist, the cache will be stored in the wp_posts table under the post type oembed_cache.

The cache is valid for 1 day (86400 seconds). This value can be changed using the oembed_ttl hook.

IMPORTANT! Checking the cache expiration time and updating the cache only occurs when the post is updated from the post editing page in the admin panel. The following hooks initialize the cache update in the admin panel:

add_action( 'edit_form_advanced', array( $this, 'maybe_run_ajax_cache' ) );
add_action( 'edit_page_form', array( $this, 'maybe_run_ajax_cache' ) );

If for some reason it was not possible to obtain the embedding HTML, for example, a 404 response was returned or something other than 200, then the marker {{unknown}} will be added to the cache instead of the HTML.

oEmbed Security

Embedding someone else's code on your site opens up the possibility of an XSS attack. For example, the received code may contain malicious code that can access your site's cookies and can do a lot of other things.

To protect yourself from such security vulnerabilities, WordPress outputs the embedding in the <iframe> tag. It also cleans the received iframe code from unwanted attributes. In particular, the embedding code allows only three HTML tags, which are checked by the function wp_kses():

$allowed_html = array(
	'a'          => array(
		'href' => true,
	),
	'blockquote' => array(),
	'iframe'     => array(
		'src'          => true,
		'width'        => true,
		'height'       => true,
		'frameborder'  => true,
		'marginwidth'  => true,
		'marginheight' => true,
		'scrolling'    => true,
		'title'        => true,
	),
);

$html = wp_kses( $html, $allowed_html );

As we can see, the embedding code can only contain three tags and a limited number of parameters for these tags.

See the code of the function wp_filter_oembed_result(), which by default is hooked to oembed_dataparse and is applied to all received embedding codes.

WordPress as an oEmbed Provider

Since version 4.4, WordPress has become an oEmbed provider. Now, posts from one WordPress site can be embedded into another website.

To do this, you need to add the post URL to the post content. This can be done in two ways:

  1. Add the URL on a separate line.
  2. Insert the URL into the shortcode [embed]. For example, [embed]http://dom.com/adress[/embed#93;.

WordPress will automatically process the specified URL (determine if it is an oEmbed provider), embed the data, and create a request cache to avoid making this request every time the page loads.

Embeds represent a brief version of the page. For example, if you add /embed to the end of the URL of any post, you will be taken to the embed page (which should appear on another site when embedded). Here's an example of such a page: https://wp-kama.com/handbook/codex/oembed.

Here's an example of what an embed looks like:

oEmbed in WordPress

How to Modify the HTML Embed Code?

To do this, you need to create a file called embed.php in the theme - more about template hierarchy.

If there is no such file in the theme, the embed code is controlled by the following files:

Each of these files has hooks that can be used to modify individual parts of the HTML embed page.

oEmbed Functions

See Full list of Embed functions.

oEmbed Hooks

  • embed_cache_oembed_types - allows changing the types of posts for which oembed links (shortcodes) should be processed.
  • oembed_ttl — allows changing the time to live (TTL) of the cache.
  • embed_oembed_html — allows changing the already cached HTML.
  • oembed_dataparse — allows changing the content (HTML) created when embedding supported oEmbed URLs.
  • embed_oembed_discover — allows specifying whether to follow the URL and search for the embedding <link> tag on the remote site.

See Full list of hooks.

Supported Providers

Below is a list of oEmbed providers that WordPress supports out of the box. Additional providers can be added to this list using the function wp_oembed_add_provider().

Provider Flavor Since
Dailymotion dailymotion.com 2.9.0
Flickr flickr.com 2.9.0
Scribd scribd.com 2.9.0
Vimeo vimeo.com 2.9.0
WordPress.tv wordpress.tv 2.9.0
YouTube youtube.com/watch 2.9.0
Crowdsignal polldaddy.com 3.0.0
SmugMug smugmug.com 3.0.0
YouTube youtu.be 3.0.0
Twitter twitter.com 3.4.0
Instagram instagram.com 3.5.0
Instagram instagr.am 3.5.0
Slideshare slideshare.net 3.5.0
SoundCloud soundcloud.com 3.5.0
Dailymotion dai.ly 3.6.0
Flickr flic.kr 3.6.0
Spotify spotify.com 3.6.0
Imgur imgur.com 3.9.0
Meetup.com meetup.com 3.9.0
Meetup.com meetu.ps 3.9.0
Animoto animoto.com 4.0.0
Animoto video214.com 4.0.0
Issuu issuu.com 4.0.0
Mixcloud mixcloud.com 4.0.0
Crowdsignal poll.fm 4.0.0
TED ted.com 4.0.0
YouTube youtube.com/playlist 4.0.0
Tumblr tumblr.com 4.2.0
Kickstarter kickstarter.com 4.2.0
Kickstarter kck.st 4.2.0
Cloudup cloudup.com 4.3.0
ReverbNation reverbnation.com 4.4.0
VideoPress videopress.com 4.4.0
Reddit reddit.com 4.4.0
Speaker Deck speakerdeck.com 4.4.0
Twitter twitter.com/timelines 4.5.0
Twitter twitter.com/moments 4.5.0
Facebook facebook.com 4.7.0
Twitter twitter.com/user 4.7.0
Twitter twitter.com/likes 4.7.0
Twitter twitter.com/lists 4.7.0
Screencast screencast.com 4.8.0
Amazon amazon.com (com.mx, com.br, ca) 4.9.0
Amazon amazon.de (fr, it, es, in, nl, ru, co.uk) 4.9.0
Amazon amazon.co.jp (com.au) 4.9.0
Amazon amazon.cn 4.9.0
Amazon a.co 4.9.0
Amazon amzn.to (eu, in, asia) 4.9.0
Amazon z.cn 4.9.0
Someecards someecards.com 4.9.0
Someecards some.ly 4.9.0
Crowdsignal survey.fm 5.1.0
Instagram TV instagram.com 5.1.0
Instagram TV instagr.am 5.1.0
TikTok tiktok.com 5.4.0

Removing Expired oEmbed Cache

For cleaning the database on large sites, it may make sense to periodically run such a function to delete expired cache.

/**
 * Remove expired oEmbed Cache.
 *
 * @param int $ttl The cache lifetime in seconds.
 *
 */
function kama_delete_expired_oembed_cache( $ttl = MONTH_IN_SECONDS ){
	global $wpdb;

	// META
	$query_data = $wpdb->get_results(
		"SELECT * FROM $wpdb->postmeta WHERE meta_key LIKE '_oembed_time_%' ORDER BY meta_value+0 DESC"
	);

	$res = [];
	foreach( $query_data as $data ){

		$post = get_post( $data->post_id );
		$info = date( 'd-m-Y', $data->meta_value ) ." - $post->ID: $post->post_title";

		if( time() > $data->meta_value + $ttl ){

			$oembed_meta_key = str_replace( '_oembed_time_', '_oembed_', $data->meta_key );

			delete_post_meta( $data->post_id, $data->meta_key );
			delete_post_meta( $data->post_id, $oembed_meta_key );

			$res['DELETED'][] = $info;
		}
		else {
			$res['NOT DELETED'][] = $info;
		}

	}

	// POSTS
	$min_allowed_date = date( 'Y-m-d H:i:59', time() - $ttl );

	$posts = $wpdb->get_results(
		"SELECT * FROM $wpdb->posts WHERE post_type = 'oembed_cache' AND post_modified_gmt < '$min_allowed_date'
		ORDER BY post_modified_gmt DESC"
	);

	foreach( $posts as $post ){
		$res['DELETED POSTS'][] = $post->post_modified_gmt;

		wp_delete_post( $post->ID, 'force_delete' );
	}

	return $res;

}

$res = kama_delete_expired_oembed_cache();
print_r( $res );

Internal Embed Handler (with Caching)

First, I'll note that this is a hack - WP is not designed for this. But thanks to hooks, it can be done.

To do this, use the hook pre_oembed_result from the method WP_oEmbed::get_html():

// ...
$pre = apply_filters( 'pre_oembed_result', null, $url, $args );

if ( null !== $pre ) {
	return $pre;
}
// ...

You need to register an internal handler through the function wp_embed_register_handler(). But in the handler function, return false and move the handler function to the mentioned hook pre_oembed_result.

So, the code will be something like this:

add_action( 'init', 'myembed_provider_register' );
add_filter( 'pre_oembed_result', 'myembed_provider_handler', 10, 3 );

function myembed_provider_register(){
	wp_embed_register_handler(
		'myembed',
		'~https://foo\.bar\.com/(\w+)~i',
		'__return_false'
	);
}

function myembed_provider_handler( $null, $url, $args ){

	$html = '{{unknown}}';

	// process $url, make an HTTP request. See WP HTTP API

	// Return iframe or HTML code

	return $html;
}

oEmbed for Arbitrary Text

If we need to process the shortcode [embed] or auto-embedding a link in the text, then this text will need to be processed separately. Basic processing using the do_shortcodes() or apply_shortcodes() functions does not include oEmbed. By default, such processing is only done for the the_content hook.

So, we have 2 options:

Option 1

Simple, but may not be suitable due to the excessive load on the the_content hook - it usually has a bunch of other things that may be unnecessary.

$text = '
Some text to check custom shortcode adding.

[embed]https://my-youtube.com/watch?v=lWzMBLoLIAc[/embed]

https://my-youtube.com/watch?v=uDQwKtkXV-0
';

$text = apply_filters( 'the_content', $text );

echo $text;

Option 2: Point Approach

We only do what we need with the text:

$text = '
Some text to check custom shortcode adding.

[embed]https://my-youtube.com/watch?v=lWzMBLoLIAc[/embed]

https://my-youtube.com/watch?v=uDQwKtkXV-0
';

$text = $GLOBALS['wp_embed']->run_shortcode( $text ); //  shortcode
$text = $GLOBALS['wp_embed']->autoembed( $text );     // oEmbed URLs

//$text = apply_shortcodes( $text );

$text = wpautop( $text );

echo $text;

oEmbed in WordPress Comments

The code below allows using oEmbed in WordPress comments.

GitHub

<?php

/**
 * Plugin Name: oEmbed in Comments
 * Description: Allow oEmbeds in comment text. A fork of http://wordpress.org/plugins/oembed-in-comments/
 * Version: 1.2
 * Author: Evan Solomon, modified by Shea Bunge
 */

class oEmbed_Comments {

  /**
	 * Setup filter with correct priority to do oEmbed in comments
	 * @since  1.0
	 * @uses   is_admin()   To make sure we don't do anything in the admin
	 * @uses   add_filter   To register a filter hook
	 * @uses   has_filter() To check if a filter is registered
	 * @return void
	 */
	static function add_filter() {
		if ( is_admin() )
			return;

		/* make_clickable breaks oEmbed regex, make sure we go earlier */
		$clickable = has_filter( 'comment_text', 'make_clickable' );
		$priority = ( $clickable ) ? $clickable - 1 : 10;

		add_filter( 'comment_text', array( __CLASS__, 'oembed_filter' ), $priority );
	}

	/**
	 * Safely add oEmbed media to a comment
	 * @since  1.0
	 * @param  string $comment_text The current comment test
	 * @return string               The modified comment text
	 */
	static function oembed_filter( $comment_text ) {
		global $wp_embed;

		/* Automatic discovery would be a security risk, safety first */
		add_filter( 'embed_oembed_discover', '__return_false', 999 );
		$comment_text = $wp_embed->autoembed( $comment_text );

		/* ...but don't break your posts if you use it */
		remove_filter( 'embed_oembed_discover', '__return_false', 999 );

		return $comment_text;
	}

}

add_action( 'init', array( 'oEmbed_Comments', 'add_filter' ) );

Correction for AJAX functionality:

<?php

/**
 * Class oEmbed_Comments
 * @package RusDTP
 *
 * @see     https://gist.github.com/sheabunge/6018753
 */
class oEmbed_Comments {

	public function __construct() {
		add_action( 'init', [ $this, 'add_filter' ] );
	}

	/**
	 * Setup filter with correct priority to enable oEmbed in comments
	 */
	public function add_filter() {
		global $pagenow;

		$access_page = [ 'index.php', 'admin-ajax.php' ];

		if ( ! in_array( $pagenow, $access_page ) ) {
			return;
		}

		/* make_clickable breaks oEmbed regex, make sure we go earlier */
		remove_filter( 'comment_text', 'make_clickable', 9 );
		add_filter( 'comment_text', 'make_clickable', 12 );

		add_filter( 'comment_text', [ $this, 'oembed_filter' ], 11 );
	}

	/**
	 * Safely add oEmbed media to a comment
	 *
	 * @param string $comment_text The current comment text
	 *
	 * @return string               The modified comment text
	 * @since  1.0
	 */
	public function oembed_filter( $comment_text ) {
		global $wp_embed;

		/* Automatic discovery would be a security risk, safety first */
		add_filter( 'embed_oembed_discover', '__return_false', 999 );
		$comment_text = $wp_embed->autoembed( $comment_text );

		/* ...but don't break your posts if you use it */
		remove_filter( 'embed_oembed_discover', '__return_false', 999 );

		return $comment_text;
	}

}

new oEmbed_Comments();

Disabling oEmbed

Read about it in a separate article