PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 7.5.1
Jetpack – WP Security, Backup, Speed, & Growth v7.5.1
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
401 lines 12.8 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 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 $matches[0] = html_entity_decode( $matches[0] );
226
227 return self::dispatch( $matches );
228 }
229
230 /**
231 * Filter and replace HTML element.
232 *
233 * @param array $matches Array of matches.
234 */
235 private static function dispatch( $matches ) {
236 $html = preg_replace( '%&#0*58;//%', '://', $matches[0] );
237 $attrs = self::get_attrs( $html );
238 if ( isset( $attrs['src'] ) ) {
239 $src = $attrs['src'];
240 } elseif ( isset( $attrs['movie'] ) ) {
241 $src = $attrs['movie'];
242 } else {
243 // no src found, search html.
244 foreach ( self::$html_strpos_filters as $match => $callback ) {
245 if ( false !== strpos( $html, $match ) ) {
246 return call_user_func( $callback, $attrs );
247 }
248 }
249
250 foreach ( self::$html_regexp_filters as $match => $callback ) {
251 if ( preg_match( $match, $html ) ) {
252 return call_user_func( $callback, $attrs );
253 }
254 }
255
256 return $matches[0];
257 }
258
259 $src = trim( $src );
260
261 // check source filter.
262 foreach ( self::$strpos_filters as $match => $callback ) {
263 if ( false !== strpos( $src, $match ) ) {
264 return call_user_func( $callback, $attrs );
265 }
266 }
267
268 foreach ( self::$regexp_filters as $match => $callback ) {
269 if ( preg_match( $match, $src ) ) {
270 return call_user_func( $callback, $attrs );
271 }
272 }
273
274 // check html filters.
275 foreach ( self::$html_strpos_filters as $match => $callback ) {
276 if ( false !== strpos( $html, $match ) ) {
277 return call_user_func( $callback, $attrs );
278 }
279 }
280
281 foreach ( self::$html_regexp_filters as $match => $callback ) {
282 if ( preg_match( $match, $html ) ) {
283 return call_user_func( $callback, $attrs );
284 }
285 }
286
287 // Log the strip.
288 if ( function_exists( 'wp_kses_reject' ) ) {
289 wp_kses_reject(
290 sprintf(
291 /* translators: placeholder is an HTML tag. */
292 __( '<code>%s</code> HTML tag removed as it is not allowed', 'jetpack' ),
293 '&lt;' . self::$current_element . '&gt;'
294 ),
295 array( self::$current_element => $attrs )
296 );
297 }
298
299 // Keep the failed match so we can later replace it with a link,
300 // but return the original content to give others a chance too.
301 self::$failed_embeds[] = array(
302 'match' => $matches[0],
303 'src' => esc_url( $src ),
304 );
305
306 return $matches[0];
307 }
308
309 /**
310 * Failed embeds are stripped, so let's convert them to links at least.
311 *
312 * @param string $string Failed embed string.
313 *
314 * @return string $string Linkified string.
315 */
316 public static function maybe_create_links( $string ) {
317 if ( empty( self::$failed_embeds ) ) {
318 return $string;
319 }
320
321 foreach ( self::$failed_embeds as $entry ) {
322 $html = sprintf( '<a href="%s">%s</a>', esc_url( $entry['src'] ), esc_url( $entry['src'] ) );
323 // Check if the string doesn't contain iframe, before replace.
324 if ( ! preg_match( '/<iframe /', $string ) ) {
325 $string = str_replace( $entry['match'], $html, $string );
326 }
327 }
328
329 self::$failed_embeds = array();
330
331 return $string;
332 }
333
334 /**
335 * Parse post HTML for HTML tags.
336 *
337 * @param string $html Post HTML.
338 */
339 public static function get_attrs( $html ) {
340 if (
341 ! ( class_exists( 'DOMDocument' ) && function_exists( 'libxml_use_internal_errors' ) && function_exists( 'simplexml_load_string' ) ) ) {
342 trigger_error( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
343 esc_html__( 'PHP’s XML extension is not available. Please contact your hosting provider to enable PHP’s XML extension.', 'jetpack' )
344 );
345 return array();
346 }
347 // We have to go through DOM, since it can load non-well-formed XML (i.e. HTML). SimpleXML cannot.
348 $dom = new DOMDocument();
349 // The @ is not enough to suppress errors when dealing with libxml,
350 // we have to tell it directly how we want to handle errors.
351 libxml_use_internal_errors( true );
352 // Suppress parser warnings.
353 @$dom->loadHTML( $html ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
354 libxml_use_internal_errors( false );
355 $xml = false;
356 // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
357 foreach ( $dom->childNodes as $node ) {
358 // find the root node (html).
359 if ( XML_ELEMENT_NODE === $node->nodeType ) {
360 /*
361 * Use simplexml_load_string rather than simplexml_import_dom
362 * as the later doesn't cope well if the XML is malformmed in the DOM
363 * See #1688-wpcom.
364 */
365 libxml_use_internal_errors( true );
366 // html->body->object.
367 $xml = simplexml_load_string( $dom->saveXML( $node->firstChild->firstChild ) );
368 libxml_clear_errors();
369 break;
370 }
371 }
372 // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
373
374 if ( ! $xml ) {
375 return array();
376 }
377
378 $attrs = array();
379 $attrs['_raw_html'] = $html;
380
381 // <param> elements
382 foreach ( $xml->param as $param ) {
383 $attrs[ (string) $param['name'] ] = (string) $param['value'];
384 }
385
386 // <object> attributes
387 foreach ( $xml->attributes() as $name => $attr ) {
388 $attrs[ $name ] = (string) $attr;
389 }
390
391 // <embed> attributes
392 if ( $xml->embed ) {
393 foreach ( $xml->embed->attributes() as $name => $attr ) {
394 $attrs[ $name ] = (string) $attr;
395 }
396 }
397
398 return $attrs;
399 }
400 }
401