class-wp-embed.php 14.4 KB
Newer Older
lechuck's avatar
lechuck committed
1 2 3 4 5 6 7 8 9
<?php
/**
 * API for easily embedding rich media such as videos and images into content.
 *
 * @package WordPress
 * @subpackage Embed
 * @since 2.9.0
 */
class WP_Embed {
lucha's avatar
lucha committed
10 11
	public $handlers = array();
	public $post_ID;
samba's avatar
samba committed
12
	public $usecache      = true;
lucha's avatar
lucha committed
13
	public $linkifunknown = true;
samba's avatar
samba committed
14 15
	public $last_attr     = array();
	public $last_url      = '';
lucha's avatar
lucha committed
16 17

	/**
lechuck's avatar
lechuck committed
18
	 * When a URL cannot be embedded, return false instead of returning a link
lucha's avatar
lucha committed
19 20 21 22 23
	 * or the URL.
	 *
	 * Bypasses the {@see 'embed_maybe_make_link'} filter.
	 *
	 * @var bool
lucha's avatar
lucha committed
24 25
	 */
	public $return_false_on_fail = false;
lechuck's avatar
lechuck committed
26 27 28 29

	/**
	 * Constructor
	 */
lucha's avatar
lucha committed
30
	public function __construct() {
lechuck's avatar
lechuck committed
31 32
		// Hack to get the [embed] shortcode to run before wpautop()
		add_filter( 'the_content', array( $this, 'run_shortcode' ), 8 );
lucha's avatar
lucha committed
33
		add_filter( 'widget_text_content', array( $this, 'run_shortcode' ), 8 );
lechuck's avatar
lechuck committed
34 35 36 37 38 39

		// 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 );
lucha's avatar
lucha committed
40
		add_filter( 'widget_text_content', array( $this, 'autoembed' ), 8 );
lechuck's avatar
lechuck committed
41

lucha's avatar
lucha committed
42
		// After a post is saved, cache oEmbed items via Ajax
lechuck's avatar
lechuck committed
43
		add_action( 'edit_form_advanced', array( $this, 'maybe_run_ajax_cache' ) );
lechuck's avatar
lechuck committed
44
		add_action( 'edit_page_form', array( $this, 'maybe_run_ajax_cache' ) );
lechuck's avatar
lechuck committed
45 46 47 48 49 50 51
	}

	/**
	 * Process the [embed] shortcode.
	 *
	 * Since the [embed] shortcode needs to be run earlier than other shortcodes,
	 * this function removes all existing shortcodes, registers the [embed] shortcode,
lucha's avatar
lucha committed
52
	 * calls do_shortcode(), and then re-registers the old shortcodes.
lechuck's avatar
lechuck committed
53
	 *
lechuck's avatar
lechuck committed
54
	 * @global array $shortcode_tags
lechuck's avatar
lechuck committed
55 56 57 58
	 *
	 * @param string $content Content to parse
	 * @return string Content with shortcode parsed
	 */
lucha's avatar
lucha committed
59
	public function run_shortcode( $content ) {
lechuck's avatar
lechuck committed
60 61 62 63 64 65 66 67 68
		global $shortcode_tags;

		// Back up current registered shortcodes and clear them all out
		$orig_shortcode_tags = $shortcode_tags;
		remove_all_shortcodes();

		add_shortcode( 'embed', array( $this, 'shortcode' ) );

		// Do the shortcode (only the [embed] one is registered)
ale's avatar
ale committed
69
		$content = do_shortcode( $content, true );
lechuck's avatar
lechuck committed
70 71 72 73 74 75 76 77 78

		// Put the original shortcodes back
		$shortcode_tags = $orig_shortcode_tags;

		return $content;
	}

	/**
	 * If a post/page was saved, then output JavaScript to make
lucha's avatar
lucha committed
79
	 * an Ajax request that will call WP_Embed::cache_oembed().
lechuck's avatar
lechuck committed
80
	 */
lucha's avatar
lucha committed
81
	public function maybe_run_ajax_cache() {
lechuck's avatar
lechuck committed
82 83
		$post = get_post();

samba's avatar
samba committed
84
		if ( ! $post || empty( $_GET['message'] ) ) {
lechuck's avatar
lechuck committed
85
			return;
samba's avatar
samba committed
86
		}
lechuck's avatar
lechuck committed
87

samba's avatar
samba committed
88
		?>
lechuck's avatar
lechuck committed
89 90 91 92 93
<script type="text/javascript">
	jQuery(document).ready(function($){
		$.get("<?php echo admin_url( 'admin-ajax.php?action=oembed-cache&post=' . $post->ID, 'relative' ); ?>");
	});
</script>
samba's avatar
samba committed
94
		<?php
lechuck's avatar
lechuck committed
95 96 97
	}

	/**
lucha's avatar
lucha committed
98 99 100 101
	 * Registers an embed handler.
	 *
	 * Do not use this function directly, use wp_embed_register_handler() instead.
	 *
lechuck's avatar
lechuck committed
102 103 104 105
	 * This function should probably also only be used for sites that do not support oEmbed.
	 *
	 * @param string $id An internal ID/name for the handler. Needs to be unique.
	 * @param string $regex The regex that will be used to see if this handler should be used for a URL.
lechuck's avatar
lechuck committed
106
	 * @param callable $callback The callback function that will be called if the regex is matched.
lechuck's avatar
lechuck committed
107 108
	 * @param int $priority Optional. Used to specify the order in which the registered handlers will be tested (default: 10). Lower numbers correspond with earlier testing, and handlers with the same priority are tested in the order in which they were added to the action.
	 */
lucha's avatar
lucha committed
109
	public function register_handler( $id, $regex, $callback, $priority = 10 ) {
samba's avatar
samba committed
110
		$this->handlers[ $priority ][ $id ] = array(
lechuck's avatar
lechuck committed
111 112 113 114 115 116
			'regex'    => $regex,
			'callback' => $callback,
		);
	}

	/**
lucha's avatar
lucha committed
117 118 119
	 * Unregisters a previously-registered embed handler.
	 *
	 * Do not use this function directly, use wp_embed_unregister_handler() instead.
lechuck's avatar
lechuck committed
120 121 122 123
	 *
	 * @param string $id The handler ID that should be removed.
	 * @param int $priority Optional. The priority of the handler to be removed (default: 10).
	 */
lucha's avatar
lucha committed
124
	public function unregister_handler( $id, $priority = 10 ) {
lechuck's avatar
lechuck committed
125
		unset( $this->handlers[ $priority ][ $id ] );
lechuck's avatar
lechuck committed
126 127 128
	}

	/**
lucha's avatar
lucha committed
129
	 * The do_shortcode() callback function.
lechuck's avatar
lechuck committed
130
	 *
lucha's avatar
lucha committed
131 132 133
	 * Attempts to convert a URL into embed HTML. Starts by checking the URL against the regex of
	 * the registered embed handlers. If none of the regex matches and it's enabled, then the URL
	 * will be given to the WP_oEmbed class.
lechuck's avatar
lechuck committed
134
	 *
lechuck's avatar
lechuck committed
135 136 137 138 139 140
	 * @param array $attr {
	 *     Shortcode attributes. Optional.
	 *
	 *     @type int $width  Width of the embed in pixels.
	 *     @type int $height Height of the embed in pixels.
	 * }
lechuck's avatar
lechuck committed
141
	 * @param string $url The URL attempting to be embedded.
ale's avatar
ale committed
142 143
	 * @return string|false The embed HTML on success, otherwise the original URL.
	 *                      `->maybe_make_link()` can return false on failure.
lechuck's avatar
lechuck committed
144
	 */
lucha's avatar
lucha committed
145
	public function shortcode( $attr, $url = '' ) {
lechuck's avatar
lechuck committed
146 147
		$post = get_post();

lucha's avatar
lucha committed
148 149 150 151
		if ( empty( $url ) && ! empty( $attr['src'] ) ) {
			$url = $attr['src'];
		}

lechuck's avatar
lechuck committed
152
		$this->last_url = $url;
ale's avatar
ale committed
153

lechuck's avatar
lechuck committed
154 155
		if ( empty( $url ) ) {
			$this->last_attr = $attr;
lechuck's avatar
lechuck committed
156
			return '';
lechuck's avatar
lechuck committed
157
		}
lechuck's avatar
lechuck committed
158 159

		$rawattr = $attr;
samba's avatar
samba committed
160
		$attr    = wp_parse_args( $attr, wp_embed_defaults( $url ) );
lechuck's avatar
lechuck committed
161

lechuck's avatar
lechuck committed
162 163
		$this->last_attr = $attr;

lechuck's avatar
lechuck committed
164
		// kses converts & into &amp; and we need to undo this
lechuck's avatar
lechuck committed
165
		// See https://core.trac.wordpress.org/ticket/11311
lechuck's avatar
lechuck committed
166 167 168 169 170 171 172
		$url = str_replace( '&amp;', '&', $url );

		// Look for known internal handlers
		ksort( $this->handlers );
		foreach ( $this->handlers as $priority => $handlers ) {
			foreach ( $handlers as $id => $handler ) {
				if ( preg_match( $handler['regex'], $url, $matches ) && is_callable( $handler['callback'] ) ) {
samba's avatar
samba committed
173
					if ( false !== $return = call_user_func( $handler['callback'], $matches, $attr, $url, $rawattr ) ) {
lucha's avatar
lucha committed
174
						/**
lucha's avatar
lucha committed
175
						 * Filters the returned embed handler.
lucha's avatar
lucha committed
176 177 178
						 *
						 * @since 2.9.0
						 *
lechuck's avatar
lechuck committed
179 180
						 * @see WP_Embed::shortcode()
						 *
lucha's avatar
lucha committed
181 182 183 184
						 * @param mixed  $return The shortcode callback function to call.
						 * @param string $url    The attempted embed URL.
						 * @param array  $attr   An array of shortcode attributes.
						 */
lechuck's avatar
lechuck committed
185
						return apply_filters( 'embed_handler_html', $return, $url, $attr );
samba's avatar
samba committed
186
					}
lechuck's avatar
lechuck committed
187 188 189 190 191
				}
			}
		}

		$post_ID = ( ! empty( $post->ID ) ) ? $post->ID : null;
lucha's avatar
lucha committed
192 193 194

		// Potentially set by WP_Embed::cache_oembed().
		if ( ! empty( $this->post_ID ) ) {
lechuck's avatar
lechuck committed
195
			$post_ID = $this->post_ID;
lucha's avatar
lucha committed
196
		}
lechuck's avatar
lechuck committed
197

lucha's avatar
lucha committed
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216
		// Check for a cached result (stored as custom post or in the post meta).
		$key_suffix    = md5( $url . serialize( $attr ) );
		$cachekey      = '_oembed_' . $key_suffix;
		$cachekey_time = '_oembed_time_' . $key_suffix;

		/**
		 * Filters the oEmbed TTL value (time to live).
		 *
		 * @since 4.0.0
		 *
		 * @param int    $time    Time to live (in seconds).
		 * @param string $url     The attempted embed URL.
		 * @param array  $attr    An array of shortcode attributes.
		 * @param int    $post_ID Post ID.
		 */
		$ttl = apply_filters( 'oembed_ttl', DAY_IN_SECONDS, $url, $attr, $post_ID );

		$cache      = '';
		$cache_time = 0;
lechuck's avatar
lechuck committed
217

lucha's avatar
lucha committed
218
		$cached_post_id = $this->find_oembed_post_id( $key_suffix );
lechuck's avatar
lechuck committed
219

lucha's avatar
lucha committed
220
		if ( $post_ID ) {
samba's avatar
samba committed
221
			$cache      = get_post_meta( $post_ID, $cachekey, true );
lucha's avatar
lucha committed
222 223 224 225 226
			$cache_time = get_post_meta( $post_ID, $cachekey_time, true );

			if ( ! $cache_time ) {
				$cache_time = 0;
			}
lucha's avatar
lucha committed
227 228
		} elseif ( $cached_post_id ) {
			$cached_post = get_post( $cached_post_id );
lucha's avatar
lucha committed
229

lucha's avatar
lucha committed
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
			$cache      = $cached_post->post_content;
			$cache_time = strtotime( $cached_post->post_modified_gmt );
		}

		$cached_recently = ( time() - $cache_time ) < $ttl;

		if ( $this->usecache || $cached_recently ) {
			// Failures are cached. Serve one if we're using the cache.
			if ( '{{unknown}}' === $cache ) {
				return $this->maybe_make_link( $url );
			}

			if ( ! empty( $cache ) ) {
				/**
				 * Filters the cached oEmbed HTML.
				 *
				 * @since 2.9.0
				 *
				 * @see WP_Embed::shortcode()
				 *
				 * @param mixed  $cache   The cached HTML result, stored in post meta.
				 * @param string $url     The attempted embed URL.
				 * @param array  $attr    An array of shortcode attributes.
				 * @param int    $post_ID Post ID.
				 */
				return apply_filters( 'embed_oembed_html', $cache, $url, $attr, $post_ID );
lechuck's avatar
lechuck committed
256
			}
lucha's avatar
lucha committed
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272
		}

		/**
		 * Filters whether to inspect the given URL for discoverable link tags.
		 *
		 * @since 2.9.0
		 * @since 4.4.0 The default value changed to true.
		 *
		 * @see WP_oEmbed::discover()
		 *
		 * @param bool $enable Whether to enable `<link>` tag discovery. Default true.
		 */
		$attr['discover'] = apply_filters( 'embed_oembed_discover', true );

		// Use oEmbed to get the HTML.
		$html = wp_oembed_get( $url, $attr );
lechuck's avatar
lechuck committed
273

lucha's avatar
lucha committed
274
		if ( $post_ID ) {
lucha's avatar
lucha committed
275 276 277 278 279 280
			if ( $html ) {
				update_post_meta( $post_ID, $cachekey, $html );
				update_post_meta( $post_ID, $cachekey_time, time() );
			} elseif ( ! $cache ) {
				update_post_meta( $post_ID, $cachekey, '{{unknown}}' );
			}
lucha's avatar
lucha committed
281 282 283 284 285 286 287 288 289
		} else {
			$has_kses = false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' );

			if ( $has_kses ) {
				// Prevent KSES from corrupting JSON in post_content.
				kses_remove_filters();
			}

			$insert_post_args = array(
samba's avatar
samba committed
290
				'post_name'   => $key_suffix,
lucha's avatar
lucha committed
291
				'post_status' => 'publish',
samba's avatar
samba committed
292
				'post_type'   => 'oembed_cache',
lucha's avatar
lucha committed
293
			);
lechuck's avatar
lechuck committed
294

lucha's avatar
lucha committed
295
			if ( $html ) {
lucha's avatar
lucha committed
296
				if ( $cached_post_id ) {
samba's avatar
samba committed
297 298 299 300 301 302 303 304
					wp_update_post(
						wp_slash(
							array(
								'ID'           => $cached_post_id,
								'post_content' => $html,
							)
						)
					);
lucha's avatar
lucha committed
305
				} else {
samba's avatar
samba committed
306 307 308 309 310 311 312 313
					wp_insert_post(
						wp_slash(
							array_merge(
								$insert_post_args,
								array(
									'post_content' => $html,
								)
							)
lucha's avatar
lucha committed
314
						)
samba's avatar
samba committed
315
					);
lucha's avatar
lucha committed
316 317
				}
			} elseif ( ! $cache ) {
samba's avatar
samba committed
318 319 320 321 322 323 324 325
				wp_insert_post(
					wp_slash(
						array_merge(
							$insert_post_args,
							array(
								'post_content' => '{{unknown}}',
							)
						)
lucha's avatar
lucha committed
326
					)
samba's avatar
samba committed
327
				);
lucha's avatar
lucha committed
328 329 330 331
			}

			if ( $has_kses ) {
				kses_init_filters();
lucha's avatar
lucha committed
332
			}
lechuck's avatar
lechuck committed
333 334
		}

lucha's avatar
lucha committed
335 336 337 338 339 340
		// If there was a result, return it.
		if ( $html ) {
			/** This filter is documented in wp-includes/class-wp-embed.php */
			return apply_filters( 'embed_oembed_html', $html, $url, $attr, $post_ID );
		}

lechuck's avatar
lechuck committed
341 342 343 344 345
		// Still unknown
		return $this->maybe_make_link( $url );
	}

	/**
lucha's avatar
lucha committed
346
	 * Delete all oEmbed caches. Unused by core as of 4.0.0.
lechuck's avatar
lechuck committed
347 348 349
	 *
	 * @param int $post_ID Post ID to delete the caches for.
	 */
lucha's avatar
lucha committed
350
	public function delete_oembed_caches( $post_ID ) {
lechuck's avatar
lechuck committed
351
		$post_metas = get_post_custom_keys( $post_ID );
samba's avatar
samba committed
352
		if ( empty( $post_metas ) ) {
lechuck's avatar
lechuck committed
353
			return;
samba's avatar
samba committed
354
		}
lechuck's avatar
lechuck committed
355

lechuck's avatar
lechuck committed
356
		foreach ( $post_metas as $post_meta_key ) {
samba's avatar
samba committed
357
			if ( '_oembed_' == substr( $post_meta_key, 0, 8 ) ) {
lechuck's avatar
lechuck committed
358
				delete_post_meta( $post_ID, $post_meta_key );
samba's avatar
samba committed
359
			}
lechuck's avatar
lechuck committed
360 361 362 363 364 365 366 367
		}
	}

	/**
	 * Triggers a caching of all oEmbed results.
	 *
	 * @param int $post_ID Post ID to do the caching for.
	 */
lucha's avatar
lucha committed
368
	public function cache_oembed( $post_ID ) {
lechuck's avatar
lechuck committed
369 370
		$post = get_post( $post_ID );

lucha's avatar
lucha committed
371
		$post_types = get_post_types( array( 'show_ui' => true ) );
lucha's avatar
lucha committed
372
		/**
lucha's avatar
lucha committed
373
		 * Filters the array of post types to cache oEmbed results for.
lucha's avatar
lucha committed
374 375 376
		 *
		 * @since 2.9.0
		 *
samba's avatar
samba committed
377
		 * @param string[] $post_types Array of post type names to cache oEmbed results for. Defaults to post types with `show_ui` set to true.
lucha's avatar
lucha committed
378
		 */
samba's avatar
samba committed
379
		if ( empty( $post->ID ) || ! in_array( $post->post_type, apply_filters( 'embed_cache_oembed_types', $post_types ) ) ) {
lechuck's avatar
lechuck committed
380
			return;
lucha's avatar
lucha committed
381
		}
lechuck's avatar
lechuck committed
382 383

		// Trigger a caching
lucha's avatar
lucha committed
384
		if ( ! empty( $post->post_content ) ) {
samba's avatar
samba committed
385
			$this->post_ID  = $post->ID;
lechuck's avatar
lechuck committed
386 387 388 389 390 391 392 393 394 395
			$this->usecache = false;

			$content = $this->run_shortcode( $post->post_content );
			$this->autoembed( $content );

			$this->usecache = true;
		}
	}

	/**
lucha's avatar
lucha committed
396
	 * Passes any unlinked URLs that are on their own line to WP_Embed::shortcode() for potential embedding.
lechuck's avatar
lechuck committed
397
	 *
lucha's avatar
lucha committed
398
	 * @see WP_Embed::autoembed_callback()
lechuck's avatar
lechuck committed
399 400 401 402
	 *
	 * @param string $content The content to be searched.
	 * @return string Potentially modified $content.
	 */
lucha's avatar
lucha committed
403
	public function autoembed( $content ) {
lechuck's avatar
lechuck committed
404 405
		// Replace line breaks from all HTML elements with placeholders.
		$content = wp_replace_in_html_tags( $content, array( "\n" => '<!-- wp-line-break -->' ) );
ale's avatar
ale committed
406

lucha's avatar
lucha committed
407 408 409 410 411 412
		if ( preg_match( '#(^|\s|>)https?://#i', $content ) ) {
			// Find URLs on their own line.
			$content = preg_replace_callback( '|^(\s*)(https?://[^\s<>"]+)(\s*)$|im', array( $this, 'autoembed_callback' ), $content );
			// Find URLs in their own paragraph.
			$content = preg_replace_callback( '|(<p(?: [^>]*)?>\s*)(https?://[^\s<>"]+)(\s*<\/p>)|i', array( $this, 'autoembed_callback' ), $content );
		}
lechuck's avatar
lechuck committed
413 414 415

		// Put the line breaks back.
		return str_replace( '<!-- wp-line-break -->', "\n", $content );
lechuck's avatar
lechuck committed
416 417 418
	}

	/**
lucha's avatar
lucha committed
419
	 * Callback function for WP_Embed::autoembed().
lechuck's avatar
lechuck committed
420 421 422 423
	 *
	 * @param array $match A regex match array.
	 * @return string The embed HTML on success, otherwise the original URL.
	 */
lucha's avatar
lucha committed
424
	public function autoembed_callback( $match ) {
samba's avatar
samba committed
425
		$oldval              = $this->linkifunknown;
lechuck's avatar
lechuck committed
426
		$this->linkifunknown = false;
samba's avatar
samba committed
427
		$return              = $this->shortcode( array(), $match[2] );
lechuck's avatar
lechuck committed
428 429
		$this->linkifunknown = $oldval;

ale's avatar
ale committed
430
		return $match[1] . $return . $match[3];
lechuck's avatar
lechuck committed
431 432 433 434 435 436
	}

	/**
	 * Conditionally makes a hyperlink based on an internal class variable.
	 *
	 * @param string $url URL to potentially be linked.
lechuck's avatar
lechuck committed
437
	 * @return false|string Linked URL or the original URL. False if 'return_false_on_fail' is true.
lechuck's avatar
lechuck committed
438
	 */
lucha's avatar
lucha committed
439 440 441 442 443
	public function maybe_make_link( $url ) {
		if ( $this->return_false_on_fail ) {
			return false;
		}

samba's avatar
samba committed
444
		$output = ( $this->linkifunknown ) ? '<a href="' . esc_url( $url ) . '">' . esc_html( $url ) . '</a>' : $url;
lucha's avatar
lucha committed
445 446

		/**
lucha's avatar
lucha committed
447
		 * Filters the returned, maybe-linked embed URL.
lucha's avatar
lucha committed
448 449 450 451 452 453
		 *
		 * @since 2.9.0
		 *
		 * @param string $output The linked or original URL.
		 * @param string $url    The original URL.
		 */
lechuck's avatar
lechuck committed
454 455
		return apply_filters( 'embed_maybe_make_link', $output, $url );
	}
lucha's avatar
lucha committed
456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472

	/**
	 * Find the oEmbed cache post ID for a given cache key.
	 *
	 * @since 4.9.0
	 *
	 * @param string $cache_key oEmbed cache key.
	 * @return int|null Post ID on success, null on failure.
	 */
	public function find_oembed_post_id( $cache_key ) {
		$cache_group    = 'oembed_cache_post';
		$oembed_post_id = wp_cache_get( $cache_key, $cache_group );

		if ( $oembed_post_id && 'oembed_cache' === get_post_type( $oembed_post_id ) ) {
			return $oembed_post_id;
		}

samba's avatar
samba committed
473 474 475 476 477 478 479 480 481 482 483 484 485
		$oembed_post_query = new WP_Query(
			array(
				'post_type'              => 'oembed_cache',
				'post_status'            => 'publish',
				'name'                   => $cache_key,
				'posts_per_page'         => 1,
				'no_found_rows'          => true,
				'cache_results'          => true,
				'update_post_meta_cache' => false,
				'update_post_term_cache' => false,
				'lazy_load_term_meta'    => false,
			)
		);
lucha's avatar
lucha committed
486 487 488 489 490 491 492 493 494 495 496

		if ( ! empty( $oembed_post_query->posts ) ) {
			// Note: 'fields'=>'ids' is not being used in order to cache the post object as it will be needed.
			$oembed_post_id = $oembed_post_query->posts[0]->ID;
			wp_cache_set( $cache_key, $oembed_post_id, $cache_group );

			return $oembed_post_id;
		}

		return null;
	}
lechuck's avatar
lechuck committed
497
}