class.filter-embedded-html-objects.php
| 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, "<$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( '?>', '?' . '>', 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 | $orig_html = $matches[0]; |
| 148 | $decoded_matches = array( html_entity_decode( $matches[0] ) ); |
| 149 | |
| 150 | return self::dispatch( $decoded_matches, $orig_html ); |
| 151 | } |
| 152 | |
| 153 | static function dispatch( $matches, $orig_html = null ) { |
| 154 | if ( null === $orig_html ) { |
| 155 | $orig_html = $matches[0]; |
| 156 | } |
| 157 | |
| 158 | $html = preg_replace( '%�*58;//%', '://', $matches[0] ); |
| 159 | $attrs = self::get_attrs( $html ); |
| 160 | if ( isset( $attrs['src'] ) ) { |
| 161 | $src = $attrs['src']; |
| 162 | } else if ( isset( $attrs['movie'] ) ) { |
| 163 | $src = $attrs['movie']; |
| 164 | } else { |
| 165 | // no src found, search html |
| 166 | foreach ( self::$html_strpos_filters as $match => $callback ) { |
| 167 | if ( false !== strpos( $html, $match ) ) { |
| 168 | return call_user_func( $callback, $attrs ); |
| 169 | } |
| 170 | } |
| 171 | |
| 172 | foreach ( self::$html_regexp_filters as $match => $callback ) { |
| 173 | if ( preg_match( $match, $html ) ) { |
| 174 | return call_user_func( $callback, $attrs ); |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | return $orig_html; |
| 179 | } |
| 180 | |
| 181 | $src = trim( $src ); |
| 182 | |
| 183 | // check source filter |
| 184 | foreach ( self::$strpos_filters as $match => $callback ) { |
| 185 | if ( false !== strpos( $src, $match ) ) { |
| 186 | return call_user_func( $callback, $attrs ); |
| 187 | } |
| 188 | } |
| 189 | |
| 190 | foreach ( self::$regexp_filters as $match => $callback ) { |
| 191 | if ( preg_match( $match, $src ) ) { |
| 192 | return call_user_func( $callback, $attrs ); |
| 193 | } |
| 194 | } |
| 195 | |
| 196 | // check html filters |
| 197 | foreach ( self::$html_strpos_filters as $match => $callback ) { |
| 198 | if ( false !== strpos( $html, $match ) ) { |
| 199 | return call_user_func( $callback, $attrs ); |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | foreach ( self::$html_regexp_filters as $match => $callback ) { |
| 204 | if ( preg_match( $match, $html ) ) { |
| 205 | return call_user_func( $callback, $attrs ); |
| 206 | } |
| 207 | } |
| 208 | |
| 209 | // Log the strip |
| 210 | if ( function_exists( 'wp_kses_reject' ) ) { |
| 211 | wp_kses_reject( sprintf( __( '<code>%s</code> HTML tag removed as it is not allowed', 'jetpack' ), '<' . self::$current_element . '>' ), array( self::$current_element => $attrs ) ); |
| 212 | } |
| 213 | |
| 214 | // Keep the failed match so we can later replace it with a link, |
| 215 | // but return the original content to give others a chance too. |
| 216 | self::$failed_embeds[] = array( |
| 217 | 'match' => $orig_html, |
| 218 | 'src' => esc_url( $src ), |
| 219 | ); |
| 220 | |
| 221 | return $orig_html; |
| 222 | } |
| 223 | |
| 224 | /** |
| 225 | * Failed embeds are stripped, so let's convert them to links at least. |
| 226 | * |
| 227 | * @param string $string Failed embed string. |
| 228 | * |
| 229 | * @return string $string Linkified string. |
| 230 | */ |
| 231 | public static function maybe_create_links( $string ) { |
| 232 | if ( empty( self::$failed_embeds ) ) { |
| 233 | return $string; |
| 234 | } |
| 235 | |
| 236 | foreach ( self::$failed_embeds as $entry ) { |
| 237 | $html = sprintf( '<a href="%s">%s</a>', esc_url( $entry['src'] ), esc_url( $entry['src'] ) ); |
| 238 | // Check if the string doesn't contain iframe, before replace. |
| 239 | if ( ! preg_match( '/<iframe /', $string ) ) { |
| 240 | $string = str_replace( $entry['match'], $html, $string ); |
| 241 | } |
| 242 | } |
| 243 | |
| 244 | self::$failed_embeds = array(); |
| 245 | |
| 246 | return $string; |
| 247 | } |
| 248 | |
| 249 | static function get_attrs( $html ) { |
| 250 | if ( ! ( class_exists( 'DOMDocument' ) && function_exists( 'libxml_use_internal_errors' ) && function_exists( 'simplexml_load_string' ) ) ) { |
| 251 | trigger_error( __( "PHP's XML extension is not available. Please contact your hosting provider to enable PHP's XML extension." ) ); |
| 252 | return array(); |
| 253 | } |
| 254 | // We have to go through DOM, since it can load non-well-formed XML (i.e. HTML). SimpleXML cannot. |
| 255 | $dom = new DOMDocument; |
| 256 | // The @ is not enough to suppress errors when dealing with libxml, |
| 257 | // we have to tell it directly how we want to handle errors. |
| 258 | libxml_use_internal_errors( TRUE ); |
| 259 | @$dom->loadHTML( $html ); // suppress parser warnings |
| 260 | libxml_use_internal_errors( FALSE ); |
| 261 | $xml = false; |
| 262 | foreach ( $dom->childNodes as $node ) { |
| 263 | // find the root node (html) |
| 264 | if ( XML_ELEMENT_NODE == $node->nodeType ) { |
| 265 | // 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 |
| 266 | libxml_use_internal_errors( true ); |
| 267 | $xml = simplexml_load_string( $dom->saveXML( $node->firstChild->firstChild ) ); // html->body->object |
| 268 | libxml_clear_errors(); |
| 269 | break; |
| 270 | } |
| 271 | } |
| 272 | if ( ! $xml ) { |
| 273 | return array(); |
| 274 | } |
| 275 | |
| 276 | $attrs = array(); |
| 277 | $attrs['_raw_html'] = $html; |
| 278 | |
| 279 | // <param> elements |
| 280 | foreach ( $xml->param as $param ) { |
| 281 | $attrs[(string) $param['name']] = (string) $param['value']; |
| 282 | } |
| 283 | |
| 284 | // <object> attributes |
| 285 | foreach ( $xml->attributes() as $name => $attr ) { |
| 286 | $attrs[$name] = (string) $attr; |
| 287 | } |
| 288 | |
| 289 | // <embed> attributes |
| 290 | if ( $xml->embed ) { |
| 291 | foreach ( $xml->embed->attributes() as $name => $attr ) { |
| 292 | $attrs[$name] = (string) $attr; |
| 293 | } |
| 294 | } |
| 295 | |
| 296 | return $attrs; |
| 297 | } |
| 298 | } |
| 299 |