PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 10.8.2
Jetpack – WP Security, Backup, Speed, & Growth v10.8.2
16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 All 501 releases
jetpack / modules / shortcodes / class.filter-embedded-html-objects.php

class.filter-embedded-html-objects.php in Jetpack – WP Security, Backup, Speed, & Growth 10.8.2, at modules/shortcodes/class.filter-embedded-html-objects.php

406 lines 13.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 add_filter( 'pre_kses', array( 'Filter_Embedded_HTML_Objects', 'filter' ), 11 );
12 add_filter( 'pre_kses', array( 'Filter_Embedded_HTML_Objects', 'maybe_create_links' ), 100 ); // See WPCom_Embed_Stats::init().
13
14 /**
15 * Helper class for identifying and parsing known HTML embeds (iframe, object, embed, etc. elements), then converting them to shortcodes.
16 * 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.
17 *
18 * @since 4.5.0
19 */
20 class Filter_Embedded_HTML_Objects {
21 /**
22 * Array of patterns to search for via strpos().
23 * Keys are patterns, values are callback functions that implement the HTML -> shortcode replacement.
24 * Patterns are matched against URLs (src or movie HTML attributes).
25 *
26 * @var array
27 */
28 public static $strpos_filters = array();
29 /**
30 * Array of patterns to search for via preg_match().
31 * Keys are patterns, values are callback functions that implement the HTML -> shortcode replacement.
32 * Patterns are matched against URLs (src or movie HTML attributes).
33 *
34 * @var array
35 */
36 public static $regexp_filters = array();
37 /**
38 * HTML element being processed.
39 *
40 * @var string
41 */
42 public static $current_element = false;
43 /**
44 * Array of patterns to search for via strpos().
45 * Keys are patterns, values are callback functions that implement the HTML -> shortcode replacement.
46 * Patterns are matched against full HTML elements.
47 *
48 * @var array
49 */
50 public static $html_strpos_filters = array();
51 /**
52 * Array of patterns to search for via preg_match().
53 * Keys are patterns, values are callback functions that implement the HTML -> shortcode replacement.
54 * Patterns are matched against full HTML elements.
55 *
56 * @var array
57 */
58 public static $html_regexp_filters = array();
59 /**
60 * Failed embeds (stripped)
61 *
62 * @var array
63 */
64 public static $failed_embeds = array();
65
66 /**
67 * Store tokens found in Syntax Highlighter.
68 *
69 * @since 4.5.0
70 *
71 * @var array
72 */
73 private static $sh_unfiltered_content_tokens;
74
75 /**
76 * Capture tokens found in Syntax Highlighter and collect them in self::$sh_unfiltered_content_tokens.
77 *
78 * @since 4.5.0
79 *
80 * @param array $match Array of Syntax Highlighter matches.
81 *
82 * @return string
83 */
84 public static function sh_regexp_callback( $match ) {
85 $token = sprintf(
86 '[prekses-filter-token-%1$d-%2$s-%1$d]',
87 wp_rand(),
88 md5( $match[0] )
89 );
90 self::$sh_unfiltered_content_tokens[ $token ] = $match[0];
91 return $token;
92 }
93
94 /**
95 * Look for HTML elements that match the registered patterns.
96 * Replace them with the HTML generated by the registered replacement callbacks.
97 *
98 * @param string $html Post content.
99 */
100 public static function filter( $html ) {
101 if ( ! $html || ! is_string( $html ) ) {
102 return $html;
103 }
104
105 $regexps = array(
106 'object' => '%<object[^>]*+>(?>[^<]*+(?><(?!/object>)[^<]*+)*)</object>%i',
107 'embed' => '%<embed[^>]*+>(?:\s*</embed>)?%i',
108 'iframe' => '%<iframe[^>]*+>(?>[^<]*+(?><(?!/iframe>)[^<]*+)*)</iframe>%i',
109 'div' => '%<div[^>]*+>(?>[^<]*+(?><(?!/div>)[^<]*+)*+)(?:</div>)+%i',
110 'script' => '%<script[^>]*+>(?>[^<]*+(?><(?!/script>)[^<]*+)*)</script>%i',
111 );
112
113 $unfiltered_content_tokens = array();
114 self::$sh_unfiltered_content_tokens = array();
115
116 // Check here to make sure that SyntaxHighlighter is still used. (Just a little future proofing).
117 if ( class_exists( 'SyntaxHighlighter' ) ) {
118 /*
119 * Replace any "code" shortcode blocks with a token that we'll later replace with its original text.
120 * This will keep the contents of the shortcode from being filtered.
121 */
122 global $SyntaxHighlighter; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
123
124 // Check to see if the $syntax_highlighter object has been created and is ready for use.
125 if ( isset( $SyntaxHighlighter ) && is_array( $SyntaxHighlighter->shortcodes ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
126 $shortcode_regex = implode( '|', array_map( 'preg_quote', $SyntaxHighlighter->shortcodes ) ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
127 $html = preg_replace_callback(
128 '/\[(' . $shortcode_regex . ')(\s[^\]]*)?\][\s\S]*?\[\/\1\]/m',
129 array( __CLASS__, 'sh_regexp_callback' ),
130 $html
131 );
132 $unfiltered_content_tokens = self::$sh_unfiltered_content_tokens;
133 }
134 }
135
136 foreach ( $regexps as $element => $regexp ) {
137 self::$current_element = $element;
138
139 if ( false !== stripos( $html, "<$element" ) ) {
140 $new_html = preg_replace_callback( $regexp, array( __CLASS__, 'dispatch' ), $html );
141 if ( $new_html ) {
142 $html = $new_html;
143 }
144 }
145
146 if ( false !== stripos( $html, "&lt;$element" ) ) {
147 $regexp_entities = self::regexp_entities( $regexp );
148 $new_html = preg_replace_callback( $regexp_entities, array( __CLASS__, 'dispatch_entities' ), $html );
149 if ( $new_html ) {
150 $html = $new_html;
151 }
152 }
153 }
154
155 if ( count( $unfiltered_content_tokens ) > 0 ) {
156 // Replace any tokens generated earlier with their original unfiltered text.
157 $html = str_replace( array_keys( $unfiltered_content_tokens ), $unfiltered_content_tokens, $html );
158 }
159
160 return $html;
161 }
162
163 /**
164 * Replace HTML entities in current HTML element regexp.
165 * This is useful when the content is HTML encoded by TinyMCE.
166 *
167 * @param string $regexp Selected regexp.
168 */
169 public static function regexp_entities( $regexp ) {
170 return preg_replace(
171 '/\[\^&([^\]]+)\]\*\+/',
172 '(?>[^&]*+(?>&(?!\1)[^&])*+)*+',
173 str_replace( '?&gt;', '?' . '>', htmlspecialchars( $regexp, ENT_NOQUOTES ) )
174 );
175 }
176
177 /**
178 * Register a filter to convert a matching HTML element to a shortcode.
179 *
180 * We can match the provided pattern against the source URL of the HTML element
181 * (generally the value of the src attribute of the HTML element), or against the full HTML element.
182 *
183 * The callback is passed an array containing the raw HTML of the element as well as pre-parsed attribute name/values.
184 *
185 * @param string $match Pattern to search for: either a regular expression to use with preg_match() or a search string to use with strpos().
186 * @param string $callback Function used to convert embed into shortcode.
187 * @param bool $is_regexp Is $match a regular expression? If true, match using preg_match(). If not, match using strpos(). Default false.
188 * @param bool $is_html_filter Match the pattern against the full HTML (true) or just the source URL (false)? Default false.
189 */
190 public static function register( $match, $callback, $is_regexp = false, $is_html_filter = false ) {
191 if ( $is_html_filter ) {
192 if ( $is_regexp ) {
193 self::$html_regexp_filters[ $match ] = $callback;
194 } else {
195 self::$html_strpos_filters[ $match ] = $callback;
196 }
197 } else {
198 if ( $is_regexp ) {
199 self::$regexp_filters[ $match ] = $callback;
200 } else {
201 self::$strpos_filters[ $match ] = $callback;
202 }
203 }
204 }
205
206 /**
207 * Delete an existing registered pattern/replacement filter.
208 *
209 * @param string $match Embed regexp.
210 */
211 public static function unregister( $match ) {
212 // Allow themes/plugins to remove registered embeds.
213 unset( self::$regexp_filters[ $match ] );
214 unset( self::$strpos_filters[ $match ] );
215 unset( self::$html_regexp_filters[ $match ] );
216 unset( self::$html_strpos_filters[ $match ] );
217 }
218
219 /**
220 * Filter and replace HTML element entity.
221 *
222 * @param array $matches Array of matches.
223 */
224 private static function dispatch_entities( $matches ) {
225 $orig_html = $matches[0];
226 $decoded_matches = array( html_entity_decode( $matches[0] ) );
227
228 return self::dispatch( $decoded_matches, $orig_html );
229 }
230
231 /**
232 * Filter and replace HTML element.
233 *
234 * @param array $matches Array of matches.
235 * @param string $orig_html Original html. Returned if no results are found via $matches processing.
236 */
237 private static function dispatch( $matches, $orig_html = null ) {
238 if ( null === $orig_html ) {
239 $orig_html = $matches[0];
240 }
241 $html = preg_replace( '%&#0*58;//%', '://', $matches[0] );
242 $attrs = self::get_attrs( $html );
243 if ( isset( $attrs['src'] ) ) {
244 $src = $attrs['src'];
245 } elseif ( isset( $attrs['movie'] ) ) {
246 $src = $attrs['movie'];
247 } else {
248 // no src found, search html.
249 foreach ( self::$html_strpos_filters as $match => $callback ) {
250 if ( false !== strpos( $html, $match ) ) {
251 return call_user_func( $callback, $attrs );
252 }
253 }
254
255 foreach ( self::$html_regexp_filters as $match => $callback ) {
256 if ( preg_match( $match, $html ) ) {
257 return call_user_func( $callback, $attrs );
258 }
259 }
260
261 return $orig_html;
262 }
263
264 $src = trim( $src );
265
266 // check source filter.
267 foreach ( self::$strpos_filters as $match => $callback ) {
268 if ( false !== strpos( $src, $match ) ) {
269 return call_user_func( $callback, $attrs );
270 }
271 }
272
273 foreach ( self::$regexp_filters as $match => $callback ) {
274 if ( preg_match( $match, $src ) ) {
275 return call_user_func( $callback, $attrs );
276 }
277 }
278
279 // check html filters.
280 foreach ( self::$html_strpos_filters as $match => $callback ) {
281 if ( false !== strpos( $html, $match ) ) {
282 return call_user_func( $callback, $attrs );
283 }
284 }
285
286 foreach ( self::$html_regexp_filters as $match => $callback ) {
287 if ( preg_match( $match, $html ) ) {
288 return call_user_func( $callback, $attrs );
289 }
290 }
291
292 // Log the strip.
293 if ( function_exists( 'wp_kses_reject' ) ) {
294 wp_kses_reject(
295 sprintf(
296 /* translators: placeholder is an HTML tag. */
297 __( '<code>%s</code> HTML tag removed as it is not allowed', 'jetpack' ),
298 '&lt;' . self::$current_element . '&gt;'
299 ),
300 array( self::$current_element => $attrs )
301 );
302 }
303
304 // Keep the failed match so we can later replace it with a link,
305 // but return the original content to give others a chance too.
306 self::$failed_embeds[] = array(
307 'match' => $orig_html,
308 'src' => esc_url( $src ),
309 );
310
311 return $orig_html;
312 }
313
314 /**
315 * Failed embeds are stripped, so let's convert them to links at least.
316 *
317 * @param string $string Failed embed string.
318 *
319 * @return string $string Linkified string.
320 */
321 public static function maybe_create_links( $string ) {
322 if ( empty( self::$failed_embeds ) ) {
323 return $string;
324 }
325
326 foreach ( self::$failed_embeds as $entry ) {
327 $html = sprintf( '<a href="%s">%s</a>', esc_url( $entry['src'] ), esc_url( $entry['src'] ) );
328 // Check if the string doesn't contain iframe, before replace.
329 if ( ! preg_match( '/<iframe /', $string ) ) {
330 $string = str_replace( $entry['match'], $html, $string );
331 }
332 }
333
334 self::$failed_embeds = array();
335
336 return $string;
337 }
338
339 /**
340 * Parse post HTML for HTML tags.
341 *
342 * @param string $html Post HTML.
343 */
344 public static function get_attrs( $html ) {
345 if (
346 ! ( class_exists( 'DOMDocument' ) && function_exists( 'libxml_use_internal_errors' ) && function_exists( 'simplexml_load_string' ) ) ) {
347 trigger_error( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
348 esc_html__( 'PHP’s XML extension is not available. Please contact your hosting provider to enable PHP’s XML extension.', 'jetpack' )
349 );
350 return array();
351 }
352 // We have to go through DOM, since it can load non-well-formed XML (i.e. HTML). SimpleXML cannot.
353 $dom = new DOMDocument();
354 // The @ is not enough to suppress errors when dealing with libxml,
355 // we have to tell it directly how we want to handle errors.
356 libxml_use_internal_errors( true );
357 // Suppress parser warnings.
358 @$dom->loadHTML( $html ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
359 libxml_use_internal_errors( false );
360 $xml = false;
361 // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
362 foreach ( $dom->childNodes as $node ) {
363 // find the root node (html).
364 if ( XML_ELEMENT_NODE === $node->nodeType ) {
365 /*
366 * Use simplexml_load_string rather than simplexml_import_dom
367 * as the later doesn't cope well if the XML is malformmed in the DOM
368 * See #1688-wpcom.
369 */
370 libxml_use_internal_errors( true );
371 // html->body->object.
372 $xml = simplexml_load_string( $dom->saveXML( $node->firstChild->firstChild ) );
373 libxml_clear_errors();
374 break;
375 }
376 }
377 // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
378
379 if ( ! $xml ) {
380 return array();
381 }
382
383 $attrs = array();
384 $attrs['_raw_html'] = $html;
385
386 // <param> elements
387 foreach ( $xml->param as $param ) {
388 $attrs[ (string) $param['name'] ] = (string) $param['value'];
389 }
390
391 // <object> attributes
392 foreach ( $xml->attributes() as $name => $attr ) {
393 $attrs[ $name ] = (string) $attr;
394 }
395
396 // <embed> attributes
397 if ( $xml->embed ) {
398 foreach ( $xml->embed->attributes() as $name => $attr ) {
399 $attrs[ $name ] = (string) $attr;
400 }
401 }
402
403 return $attrs;
404 }
405 }
406