PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 6.4
Jetpack – WP Security, Backup, Speed, & Growth v6.4
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
294 lines 9.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
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
9 add_filter( 'pre_kses', array( 'Filter_Embedded_HTML_Objects', 'filter' ), 11 );
10 add_filter( 'pre_kses', array( 'Filter_Embedded_HTML_Objects', 'maybe_create_links' ), 100 ); // See WPCom_Embed_Stats::init()
11
12 /**
13 * Helper class for identifying and parsing known HTML blocks
14 *
15 * @since 4.5.0
16 *
17 * @author mdawaffe
18 *
19 * Not completely done, but seems to work okay
20 * Stolen from Mike's Seaside presentation:
21 * @link http://mdawaffepresents.wordpress.com/?p=36
22 */
23
24 class Filter_Embedded_HTML_Objects {
25 static public $strpos_filters = array();
26 static public $regexp_filters = array();
27 static public $current_element = false;
28 static public $html_strpos_filters = array();
29 static public $html_regexp_filters = array();
30 static public $failed_embeds = array();
31
32 /**
33 * Store tokens found in Syntax Highlighter.
34 *
35 * @since 4.5.0
36 *
37 * @var array
38 */
39 static private $sh_unfiltered_content_tokens;
40
41 /**
42 * Capture tokens found in Syntax Highlighter and collect them in self::$sh_unfiltered_content_tokens.
43 *
44 * @since 4.5.0
45 *
46 * @param array $match
47 *
48 * @return string
49 */
50 static public function sh_regexp_callback( $match ) {
51 $token = '[prekses-filter-token-' . mt_rand() . '-' . md5( $match[0] ) . '-' . mt_rand() . ']';
52 self::$sh_unfiltered_content_tokens[$token] = $match[0];
53 return $token;
54 }
55
56 static public function filter( $html ) {
57 if ( ! $html || ! is_string( $html ) ) {
58 return $html;
59 }
60
61 $regexps = array(
62 'object' => '%<object[^>]*+>(?>[^<]*+(?><(?!/object>)[^<]*+)*)</object>%i',
63 'embed' => '%<embed[^>]*+>(?:\s*</embed>)?%i',
64 'iframe' => '%<iframe[^>]*+>(?>[^<]*+(?><(?!/iframe>)[^<]*+)*)</iframe>%i',
65 'div' => '%<div[^>]*+>(?>[^<]*+(?><(?!/div>)[^<]*+)*+)(?:</div>)+%i',
66 'script' => '%<script[^>]*+>(?>[^<]*+(?><(?!/script>)[^<]*+)*)</script>%i',
67 );
68
69 $unfiltered_content_tokens = array();
70 self::$sh_unfiltered_content_tokens = array();
71
72 // Check here to make sure that SyntaxHighlighter is still used. (Just a little future proofing)
73 if ( class_exists( 'SyntaxHighlighter' ) ) {
74 // Replace any "code" shortcode blocks with a token that we'll later replace with its original text.
75 // This will keep the contents of the shortcode from being filtered
76
77 global $SyntaxHighlighter;
78
79 // Check to see if the $SyntaxHighlighter object has been created and is ready for use
80 if ( isset( $SyntaxHighlighter ) && is_array( $SyntaxHighlighter->shortcodes ) ) {
81 $shortcode_regex = implode( '|', array_map( 'preg_quote', $SyntaxHighlighter->shortcodes ) );
82 $html = preg_replace_callback(
83 '/\[(' . $shortcode_regex . ')(\s[^\]]*)?\][\s\S]*?\[\/\1\]/m', array( __CLASS__, 'sh_regexp_callback' ), $html
84 );
85 $unfiltered_content_tokens = self::$sh_unfiltered_content_tokens;
86 }
87 }
88
89 foreach ( $regexps as $element => $regexp ) {
90 self::$current_element = $element;
91
92 if ( false !== stripos( $html, "<$element" ) ) {
93 if ( $new_html = preg_replace_callback( $regexp, array( __CLASS__, 'dispatch' ), $html ) ) {
94 $html = $new_html;
95 }
96 }
97
98 if ( false !== stripos( $html, "&lt;$element" ) ) {
99 $regexp_entities = self::regexp_entities( $regexp );
100 if ( $new_html = preg_replace_callback( $regexp_entities, array( __CLASS__, 'dispatch_entities' ), $html ) ) {
101 $html = $new_html;
102 }
103 }
104 }
105
106 if ( count( $unfiltered_content_tokens ) > 0 ) {
107 // Replace any tokens generated earlier with their original unfiltered text
108 $html = str_replace( array_keys( $unfiltered_content_tokens ), $unfiltered_content_tokens, $html );
109 }
110
111 return $html;
112 }
113
114 static public function regexp_entities( $regexp ) {
115 return preg_replace(
116 '/\[\^&([^\]]+)\]\*\+/',
117 '(?>[^&]*+(?>&(?!\1)[^&])*+)*+',
118 str_replace( '?&gt;', '?' . '>', htmlspecialchars( $regexp, ENT_NOQUOTES ) )
119 );
120 }
121
122 static public function register( $match, $callback, $is_regexp = false, $is_html_filter = false ) {
123 if ( $is_html_filter ) {
124 if ( $is_regexp ) {
125 self::$html_regexp_filters[$match] = $callback;
126 } else {
127 self::$html_strpos_filters[$match] = $callback;
128 }
129 } else {
130 if ( $is_regexp ) {
131 self::$regexp_filters[$match] = $callback;
132 } else {
133 self::$strpos_filters[$match] = $callback;
134 }
135 }
136 }
137
138 static public function unregister( $match ) {
139 // Allow themes/plugins to remove registered embeds
140 unset( self::$regexp_filters[$match] );
141 unset( self::$strpos_filters[$match] );
142 unset( self::$html_regexp_filters[$match] );
143 unset( self::$html_strpos_filters[$match] );
144 }
145
146 static function dispatch_entities( $matches ) {
147 $matches[0] = html_entity_decode( $matches[0] );
148
149 return self::dispatch( $matches );
150 }
151
152 static function dispatch( $matches ) {
153 $html = preg_replace( '%&#0*58;//%', '://', $matches[0] );
154 $attrs = self::get_attrs( $html );
155 if ( isset( $attrs['src'] ) ) {
156 $src = $attrs['src'];
157 } else if ( isset( $attrs['movie'] ) ) {
158 $src = $attrs['movie'];
159 } else {
160 // no src found, search html
161 foreach ( self::$html_strpos_filters as $match => $callback ) {
162 if ( false !== strpos( $html, $match ) ) {
163 return call_user_func( $callback, $attrs );
164 }
165 }
166
167 foreach ( self::$html_regexp_filters as $match => $callback ) {
168 if ( preg_match( $match, $html ) ) {
169 return call_user_func( $callback, $attrs );
170 }
171 }
172
173 return $matches[0];
174 }
175
176 $src = trim( $src );
177
178 // check source filter
179 foreach ( self::$strpos_filters as $match => $callback ) {
180 if ( false !== strpos( $src, $match ) ) {
181 return call_user_func( $callback, $attrs );
182 }
183 }
184
185 foreach ( self::$regexp_filters as $match => $callback ) {
186 if ( preg_match( $match, $src ) ) {
187 return call_user_func( $callback, $attrs );
188 }
189 }
190
191 // check html filters
192 foreach ( self::$html_strpos_filters as $match => $callback ) {
193 if ( false !== strpos( $html, $match ) ) {
194 return call_user_func( $callback, $attrs );
195 }
196 }
197
198 foreach ( self::$html_regexp_filters as $match => $callback ) {
199 if ( preg_match( $match, $html ) ) {
200 return call_user_func( $callback, $attrs );
201 }
202 }
203
204 // Log the strip
205 if ( function_exists( 'wp_kses_reject' ) ) {
206 wp_kses_reject( sprintf( __( '<code>%s</code> HTML tag removed as it is not allowed', 'jetpack' ), '&lt;' . self::$current_element . '&gt;' ), array( self::$current_element => $attrs ) );
207 }
208
209 // Keep the failed match so we can later replace it with a link,
210 // but return the original content to give others a chance too.
211 self::$failed_embeds[] = array(
212 'match' => $matches[0],
213 'src' => esc_url( $src ),
214 );
215
216 return $matches[0];
217 }
218
219 /**
220 * Failed embeds are stripped, so let's convert them to links at least.
221 *
222 * @param string $string Failed embed string.
223 *
224 * @return string $string Linkified string.
225 */
226 public static function maybe_create_links( $string ) {
227 if ( empty( self::$failed_embeds ) ) {
228 return $string;
229 }
230
231 foreach ( self::$failed_embeds as $entry ) {
232 $html = sprintf( '<a href="%s">%s</a>', esc_url( $entry['src'] ), esc_url( $entry['src'] ) );
233 // Check if the string doesn't contain iframe, before replace.
234 if ( ! preg_match( '/<iframe /', $string ) ) {
235 $string = str_replace( $entry['match'], $html, $string );
236 }
237 }
238
239 self::$failed_embeds = array();
240
241 return $string;
242 }
243
244 static function get_attrs( $html ) {
245 if ( ! ( class_exists( 'DOMDocument' ) && function_exists( 'libxml_use_internal_errors' ) && function_exists( 'simplexml_load_string' ) ) ) {
246 trigger_error( __( "PHP's XML extension is not available. Please contact your hosting provider to enable PHP's XML extension." ) );
247 return array();
248 }
249 // We have to go through DOM, since it can load non-well-formed XML (i.e. HTML). SimpleXML cannot.
250 $dom = new DOMDocument;
251 // The @ is not enough to suppress errors when dealing with libxml,
252 // we have to tell it directly how we want to handle errors.
253 libxml_use_internal_errors( TRUE );
254 @$dom->loadHTML( $html ); // suppress parser warnings
255 libxml_use_internal_errors( FALSE );
256 $xml = false;
257 foreach ( $dom->childNodes as $node ) {
258 // find the root node (html)
259 if ( XML_ELEMENT_NODE == $node->nodeType ) {
260 // Use simplexml_load_string rather than simplexml_import_dom as the later doesn't cope well if the XML is malformmed in the DOM See #1688-wpcom
261 libxml_use_internal_errors( true );
262 $xml = simplexml_load_string( $dom->saveXML( $node->firstChild->firstChild ) ); // html->body->object
263 libxml_clear_errors();
264 break;
265 }
266 }
267 if ( ! $xml ) {
268 return array();
269 }
270
271 $attrs = array();
272 $attrs['_raw_html'] = $html;
273
274 // <param> elements
275 foreach ( $xml->param as $param ) {
276 $attrs[(string) $param['name']] = (string) $param['value'];
277 }
278
279 // <object> attributes
280 foreach ( $xml->attributes() as $name => $attr ) {
281 $attrs[$name] = (string) $attr;
282 }
283
284 // <embed> attributes
285 if ( $xml->embed ) {
286 foreach ( $xml->embed->attributes() as $name => $attr ) {
287 $attrs[$name] = (string) $attr;
288 }
289 }
290
291 return $attrs;
292 }
293 }
294