PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.0-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.0-beta
16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / modules / shortcodes / class.filter-embedded-html-objects.php
jetpack / modules / shortcodes Last commit date
css 1 month ago images 1 year ago img 2 months ago js 7 months ago archiveorg-book.php 7 months ago archiveorg.php 1 month ago archives.php 1 week ago bandcamp.php 7 months ago brightcove.php 6 months ago cartodb.php 7 months ago class.filter-embedded-html-objects.php 7 months ago codepen.php 7 months ago crowdsignal.php 6 months ago dailymotion.php 7 months ago descript.php 7 months ago facebook.php 7 months ago flatio.php 7 months ago flickr.php 7 months ago getty.php 7 months ago gist.php 7 months ago googleapps.php 7 months ago googlemaps.php 1 month ago googleplus.php 7 months ago gravatar.php 7 months ago houzz.php 7 months ago inline-pdfs.php 7 months ago instagram.php 7 months ago kickstarter.php 7 months ago mailchimp.php 1 month ago medium.php 7 months ago mixcloud.php 7 months ago others.php 7 months ago pinterest.php 7 months ago presentations.php 7 months ago quiz.php 7 months ago recipe.php 2 weeks ago scribd.php 7 months ago shortcode-utils.php 7 months ago sitemap.php 7 months ago slideshare.php 7 months ago slideshow.php 2 weeks ago smartframe.php 7 months ago soundcloud.php 1 month ago spotify.php 7 months ago ted.php 7 months ago tweet.php 7 months ago twitchtv.php 2 weeks ago twitter-timeline.php 7 months ago twitter.php 7 months ago unavailable.php 7 months ago untappd-menu.php 7 months ago upcoming-events.php 7 months ago ustream.php 7 months ago videopress.php 7 months ago vimeo.php 1 month ago vine.php 7 months ago vr.php 1 month ago wufoo.php 7 months ago youtube.php 4 months ago
class.filter-embedded-html-objects.php
409 lines
1 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2 /**
3 * The companion file to shortcodes.php
4 *
5 * This file contains the code that converts HTML embeds into shortcodes
6 * for when the user copy/pastes in HTML.
7 *
8 * @package automattic/jetpack
9 */
10
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit( 0 );
13 }
14
15 // Run these everywhere to preserve some content, even in cases of display for improper code saved to the DB.
16 add_filter( 'pre_kses', array( 'Filter_Embedded_HTML_Objects', 'filter' ), 11 );
17 add_filter( 'pre_kses', array( 'Filter_Embedded_HTML_Objects', 'maybe_create_links' ), 100 ); // See WPCom_Embed_Stats::init().
18
19 /**
20 * Helper class for identifying and parsing known HTML embeds (iframe, object, embed, etc. elements), then converting them to shortcodes.
21 * For unknown HTML embeds, the class still tries to convert them to plain links so that at least something is preserved instead of having the entire element stripped by KSES.
22 *
23 * @since 4.5.0
24 */
25 class Filter_Embedded_HTML_Objects {
26 /**
27 * Array of patterns to search for via strpos().
28 * Keys are patterns, values are callback functions that implement the HTML -> shortcode replacement.
29 * Patterns are matched against URLs (src or movie HTML attributes).
30 *
31 * @var array
32 */
33 public static $strpos_filters = array();
34 /**
35 * Array of patterns to search for via preg_match().
36 * Keys are patterns, values are callback functions that implement the HTML -> shortcode replacement.
37 * Patterns are matched against URLs (src or movie HTML attributes).
38 *
39 * @var array
40 */
41 public static $regexp_filters = array();
42 /**
43 * HTML element being processed.
44 *
45 * @var string
46 */
47 public static $current_element = false;
48 /**
49 * Array of patterns to search for via strpos().
50 * Keys are patterns, values are callback functions that implement the HTML -> shortcode replacement.
51 * Patterns are matched against full HTML elements.
52 *
53 * @var array
54 */
55 public static $html_strpos_filters = array();
56 /**
57 * Array of patterns to search for via preg_match().
58 * Keys are patterns, values are callback functions that implement the HTML -> shortcode replacement.
59 * Patterns are matched against full HTML elements.
60 *
61 * @var array
62 */
63 public static $html_regexp_filters = array();
64 /**
65 * Failed embeds (stripped)
66 *
67 * @var array
68 */
69 public static $failed_embeds = array();
70
71 /**
72 * Store tokens found in Syntax Highlighter.
73 *
74 * @since 4.5.0
75 *
76 * @var array
77 */
78 private static $sh_unfiltered_content_tokens;
79
80 /**
81 * Capture tokens found in Syntax Highlighter and collect them in self::$sh_unfiltered_content_tokens.
82 *
83 * @since 4.5.0
84 *
85 * @param array $match Array of Syntax Highlighter matches.
86 *
87 * @return string
88 */
89 public static function sh_regexp_callback( $match ) {
90 $token = sprintf(
91 '[prekses-filter-token-%1$d-%2$s-%1$d]',
92 wp_rand(),
93 md5( $match[0] )
94 );
95 self::$sh_unfiltered_content_tokens[ $token ] = $match[0];
96 return $token;
97 }
98
99 /**
100 * Look for HTML elements that match the registered patterns.
101 * Replace them with the HTML generated by the registered replacement callbacks.
102 *
103 * @param string $html Post content.
104 */
105 public static function filter( $html ) {
106 if ( ! $html || ! is_string( $html ) ) {
107 return $html;
108 }
109
110 $regexps = array(
111 'object' => '%<object[^>]*+>(?>[^<]*+(?><(?!/object>)[^<]*+)*)</object>%i',
112 'embed' => '%<embed[^>]*+>(?:\s*</embed>)?%i',
113 'iframe' => '%<iframe[^>]*+>(?>[^<]*+(?><(?!/iframe>)[^<]*+)*)</iframe>%i',
114 'div' => '%<div[^>]*+>(?>[^<]*+(?><(?!/div>)[^<]*+)*+)(?:</div>)+%i',
115 'script' => '%<script[^>]*+>(?>[^<]*+(?><(?!/script>)[^<]*+)*)</script>%i',
116 );
117
118 $unfiltered_content_tokens = array();
119 self::$sh_unfiltered_content_tokens = array();
120
121 // Check here to make sure that SyntaxHighlighter is still used. (Just a little future proofing).
122 if ( class_exists( 'SyntaxHighlighter' ) ) {
123 /*
124 * Replace any "code" shortcode blocks with a token that we'll later replace with its original text.
125 * This will keep the contents of the shortcode from being filtered.
126 */
127 global $SyntaxHighlighter; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
128
129 // Check to see if the $syntax_highlighter object has been created and is ready for use.
130 if ( isset( $SyntaxHighlighter ) && is_array( $SyntaxHighlighter->shortcodes ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
131 $shortcode_regex = implode( '|', array_map( 'preg_quote', $SyntaxHighlighter->shortcodes ) ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
132 $html = preg_replace_callback(
133 '/\[(' . $shortcode_regex . ')(\s[^\]]*)?\][\s\S]*?\[\/\1\]/m',
134 array( __CLASS__, 'sh_regexp_callback' ),
135 $html
136 );
137 $unfiltered_content_tokens = self::$sh_unfiltered_content_tokens;
138 }
139 }
140
141 foreach ( $regexps as $element => $regexp ) {
142 self::$current_element = $element;
143
144 if ( false !== stripos( $html, "<$element" ) ) {
145 $new_html = preg_replace_callback( $regexp, array( __CLASS__, 'dispatch' ), $html );
146 if ( $new_html ) {
147 $html = $new_html;
148 }
149 }
150
151 if ( false !== stripos( $html, "&lt;$element" ) ) {
152 $regexp_entities = self::regexp_entities( $regexp );
153 $new_html = preg_replace_callback( $regexp_entities, array( __CLASS__, 'dispatch_entities' ), $html );
154 if ( $new_html ) {
155 $html = $new_html;
156 }
157 }
158 }
159
160 if ( $unfiltered_content_tokens !== array() ) {
161 // Replace any tokens generated earlier with their original unfiltered text.
162 $html = str_replace( array_keys( $unfiltered_content_tokens ), $unfiltered_content_tokens, $html );
163 }
164
165 return $html;
166 }
167
168 /**
169 * Replace HTML entities in current HTML element regexp.
170 * This is useful when the content is HTML encoded by TinyMCE.
171 *
172 * @param string $regexp Selected regexp.
173 */
174 public static function regexp_entities( $regexp ) {
175 return preg_replace(
176 '/\[\^&([^\]]+)\]\*\+/',
177 '(?>[^&]*+(?>&(?!\1)[^&])*+)*+',
178 str_replace( '?&gt;', '?' . '>', htmlspecialchars( $regexp, ENT_NOQUOTES ) )
179 );
180 }
181
182 /**
183 * Register a filter to convert a matching HTML element to a shortcode.
184 *
185 * We can match the provided pattern against the source URL of the HTML element
186 * (generally the value of the src attribute of the HTML element), or against the full HTML element.
187 *
188 * The callback is passed an array containing the raw HTML of the element as well as pre-parsed attribute name/values.
189 *
190 * @param string $match Pattern to search for: either a regular expression to use with preg_match() or a search string to use with strpos().
191 * @param string $callback Function used to convert embed into shortcode.
192 * @param bool $is_regexp Is $match a regular expression? If true, match using preg_match(). If not, match using strpos(). Default false.
193 * @param bool $is_html_filter Match the pattern against the full HTML (true) or just the source URL (false)? Default false.
194 */
195 public static function register( $match, $callback, $is_regexp = false, $is_html_filter = false ) {
196 if ( $is_html_filter ) {
197 if ( $is_regexp ) {
198 self::$html_regexp_filters[ $match ] = $callback;
199 } else {
200 self::$html_strpos_filters[ $match ] = $callback;
201 }
202 } elseif ( $is_regexp ) {
203 self::$regexp_filters[ $match ] = $callback;
204 } else {
205 self::$strpos_filters[ $match ] = $callback;
206 }
207 }
208
209 /**
210 * Delete an existing registered pattern/replacement filter.
211 *
212 * @param string $match Embed regexp.
213 */
214 public static function unregister( $match ) {
215 // Allow themes/plugins to remove registered embeds.
216 unset( self::$regexp_filters[ $match ] );
217 unset( self::$strpos_filters[ $match ] );
218 unset( self::$html_regexp_filters[ $match ] );
219 unset( self::$html_strpos_filters[ $match ] );
220 }
221
222 /**
223 * Filter and replace HTML element entity.
224 *
225 * @param array $matches Array of matches.
226 */
227 private static function dispatch_entities( $matches ) {
228 $orig_html = $matches[0];
229 $decoded_matches = array( html_entity_decode( $matches[0], ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 ) );
230
231 return self::dispatch( $decoded_matches, $orig_html );
232 }
233
234 /**
235 * Filter and replace HTML element.
236 *
237 * @param array $matches Array of matches.
238 * @param string $orig_html Original html. Returned if no results are found via $matches processing.
239 */
240 private static function dispatch( $matches, $orig_html = null ) {
241 if ( null === $orig_html ) {
242 $orig_html = $matches[0];
243 }
244 $html = preg_replace( '%&#0*58;//%', '://', $matches[0] );
245 $attrs = self::get_attrs( $html );
246 if ( isset( $attrs['src'] ) ) {
247 $src = $attrs['src'];
248 } elseif ( isset( $attrs['movie'] ) ) {
249 $src = $attrs['movie'];
250 } else {
251 // no src found, search html.
252 foreach ( self::$html_strpos_filters as $match => $callback ) {
253 if ( str_contains( $html, $match ) ) {
254 return call_user_func( $callback, $attrs );
255 }
256 }
257
258 foreach ( self::$html_regexp_filters as $match => $callback ) {
259 if ( preg_match( $match, $html ) ) {
260 return call_user_func( $callback, $attrs );
261 }
262 }
263
264 return $orig_html;
265 }
266
267 $src = trim( $src );
268
269 // check source filter.
270 foreach ( self::$strpos_filters as $match => $callback ) {
271 if ( str_contains( $src, $match ) ) {
272 return call_user_func( $callback, $attrs );
273 }
274 }
275
276 foreach ( self::$regexp_filters as $match => $callback ) {
277 if ( preg_match( $match, $src ) ) {
278 return call_user_func( $callback, $attrs );
279 }
280 }
281
282 // check html filters.
283 foreach ( self::$html_strpos_filters as $match => $callback ) {
284 if ( str_contains( $html, $match ) ) {
285 return call_user_func( $callback, $attrs );
286 }
287 }
288
289 foreach ( self::$html_regexp_filters as $match => $callback ) {
290 if ( preg_match( $match, $html ) ) {
291 return call_user_func( $callback, $attrs );
292 }
293 }
294
295 // Log the strip.
296 if ( function_exists( 'wp_kses_reject' ) ) {
297 wp_kses_reject(
298 sprintf(
299 /* translators: placeholder is an HTML tag. */
300 __( '<code>%s</code> HTML tag removed as it is not allowed', 'jetpack' ),
301 '&lt;' . self::$current_element . '&gt;'
302 ),
303 array( self::$current_element => $attrs )
304 );
305 }
306
307 // Keep the failed match so we can later replace it with a link,
308 // but return the original content to give others a chance too.
309 self::$failed_embeds[] = array(
310 'match' => $orig_html,
311 'src' => esc_url( $src ),
312 );
313
314 return $orig_html;
315 }
316
317 /**
318 * Failed embeds are stripped, so let's convert them to links at least.
319 *
320 * @param string $string Failed embed string.
321 *
322 * @return string $string Linkified string.
323 */
324 public static function maybe_create_links( $string ) {
325 if ( empty( self::$failed_embeds ) ) {
326 return $string;
327 }
328
329 foreach ( self::$failed_embeds as $entry ) {
330 $html = sprintf( '<a href="%s">%s</a>', esc_url( $entry['src'] ), esc_url( $entry['src'] ) );
331 // Check if the string doesn't contain iframe, before replace.
332 if ( ! str_contains( $string, '<iframe ' ) ) {
333 $string = str_replace( $entry['match'], $html, $string );
334 }
335 }
336
337 self::$failed_embeds = array();
338
339 return $string;
340 }
341
342 /**
343 * Parse post HTML for HTML tags.
344 *
345 * @param string $html Post HTML.
346 */
347 public static function get_attrs( $html ) {
348 if (
349 ! ( class_exists( 'DOMDocument' ) && function_exists( 'simplexml_load_string' ) ) ) {
350 trigger_error( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
351 esc_html__( 'PHP’s XML extension is not available. Please contact your hosting provider to enable PHP’s XML extension.', 'jetpack' )
352 );
353 return array();
354 }
355 // We have to go through DOM, since it can load non-well-formed XML (i.e. HTML). SimpleXML cannot.
356 $dom = new DOMDocument();
357 // The @ is not enough to suppress errors when dealing with libxml,
358 // we have to tell it directly how we want to handle errors.
359 libxml_use_internal_errors( true );
360 // Suppress parser warnings.
361 @$dom->loadHTML( $html ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
362 libxml_use_internal_errors( false );
363 $xml = false;
364 // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
365 foreach ( $dom->childNodes as $node ) {
366 // find the root node (html).
367 if ( XML_ELEMENT_NODE === $node->nodeType ) {
368 /*
369 * Use simplexml_load_string rather than simplexml_import_dom
370 * as the later doesn't cope well if the XML is malformmed in the DOM
371 * See #1688-wpcom.
372 */
373 libxml_use_internal_errors( true );
374 // html->body->object.
375 $xml = simplexml_load_string( $dom->saveXML( $node->firstChild->firstChild ) );
376 libxml_clear_errors();
377 break;
378 }
379 }
380 // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
381
382 if ( ! $xml ) {
383 return array();
384 }
385
386 $attrs = array();
387 $attrs['_raw_html'] = $html;
388
389 // <param> elements
390 foreach ( $xml->param as $param ) {
391 $attrs[ (string) $param['name'] ] = (string) $param['value'];
392 }
393
394 // <object> attributes
395 foreach ( $xml->attributes() as $name => $attr ) {
396 $attrs[ $name ] = (string) $attr;
397 }
398
399 // <embed> attributes
400 if ( $xml->embed ) {
401 foreach ( $xml->embed->attributes() as $name => $attr ) {
402 $attrs[ $name ] = (string) $attr;
403 }
404 }
405
406 return $attrs;
407 }
408 }
409