Ticket #40951: 40951.11.diff
File 40951.11.diff, 31.7 KB (added by , 8 years ago) |
---|
-
src/wp-admin/css/widgets.css
diff --git src/wp-admin/css/widgets.css src/wp-admin/css/widgets.css index d06fc29f70..c622a2a51c 100644
div#widgets-right .widget-top:hover, 619 619 cursor: move; 620 620 } 621 621 622 /* =Specific widget styling 623 -------------------------------------------------------------- */ 624 .text-widget-fields { 625 position: relative; 626 } 627 .text-widget-fields .wp-pointer.wp-pointer-top { 628 position: absolute; 629 z-index: 3; 630 top: 100px; 631 right: 10px; 632 left: 10px; 633 } 634 .text-widget-fields .wp-pointer .wp-pointer-arrow { 635 left: auto; 636 right: 15px; 637 } 638 .text-widget-fields .wp-pointer .wp-pointer-buttons { 639 line-height: 1.4em; 640 } 641 622 642 /* =Media Queries 623 643 -------------------------------------------------------------- */ 624 644 -
src/wp-admin/js/widgets/text-widgets.js
diff --git src/wp-admin/js/widgets/text-widgets.js src/wp-admin/js/widgets/text-widgets.js index 7932606e3d..a7ba7f0c06 100644
3 3 wp.textWidgets = ( function( $ ) { 4 4 'use strict'; 5 5 6 var component = {}; 6 var component = { 7 dismissedPointers: [] 8 }; 7 9 8 10 /** 9 11 * Text widget control. … … wp.textWidgets = ( function( $ ) { 45 47 control.$el.addClass( 'text-widget-fields' ); 46 48 control.$el.html( wp.template( 'widget-text-control-fields' ) ); 47 49 50 control.customHtmlWidgetPointer = control.$el.find( '.wp-pointer.custom-html-widget-pointer' ); 51 if ( control.customHtmlWidgetPointer.length ) { 52 control.customHtmlWidgetPointer.find( '.close' ).on( 'click', function( event ) { 53 event.preventDefault(); 54 control.customHtmlWidgetPointer.remove(); 55 control.dismissPointers( [ 'text_widget_custom_html' ] ); 56 }); 57 control.customHtmlWidgetPointer.find( '.add-widget' ).on( 'click', function( event ) { 58 event.preventDefault(); 59 control.customHtmlWidgetPointer.remove(); 60 control.openAvailableWidgetsPanel(); 61 }); 62 } 63 64 control.pasteHtmlPointer = control.$el.find( '.wp-pointer.paste-html-pointer' ); 65 if ( control.pasteHtmlPointer.length ) { 66 control.pasteHtmlPointer.find( '.close' ).on( 'click', function( event ) { 67 event.preventDefault(); 68 control.pasteHtmlPointer.remove(); 69 control.dismissPointers( [ 'text_widget_custom_html', 'text_widget_paste_html' ] ); 70 }); 71 } 72 48 73 control.fields = { 49 74 title: control.$el.find( '.title' ), 50 75 text: control.$el.find( '.text' ) … … wp.textWidgets = ( function( $ ) { 66 91 }, 67 92 68 93 /** 94 * Dismiss pointers for Custom HTML widget. 95 * 96 * @since 4.8.1 97 * 98 * @param {Array} pointers Pointer IDs to dismiss. 99 * @returns {void} 100 */ 101 dismissPointers: function dismissPointers( pointers ) { 102 _.each( pointers, function( pointer ) { 103 wp.ajax.post( 'dismiss-wp-pointer', { 104 pointer: pointer 105 }); 106 component.dismissedPointers.push( pointer ); 107 }); 108 }, 109 110 /** 111 * Open available widgets panel. 112 * 113 * @since 4.8.1 114 * @returns {void} 115 */ 116 openAvailableWidgetsPanel: function openAvailableWidgetsPanel() { 117 var sidebarControl; 118 wp.customize.section.each( function( section ) { 119 if ( section.extended( wp.customize.Widgets.SidebarSection ) && section.expanded() ) { 120 sidebarControl = wp.customize.control( 'sidebars_widgets[' + section.params.sidebarId + ']' ); 121 } 122 }); 123 if ( ! sidebarControl ) { 124 return; 125 } 126 setTimeout( function() { // Timeout to prevent click event from causing panel to immediately collapse. 127 wp.customize.Widgets.availableWidgetsPanel.open( sidebarControl ); 128 wp.customize.Widgets.availableWidgetsPanel.$search.val( 'HTML' ).trigger( 'keyup' ); 129 }); 130 }, 131 132 /** 69 133 * Update input fields from the sync fields. 70 134 * 71 135 * This function is called at the widget-updated and widget-synced events. … … wp.textWidgets = ( function( $ ) { 152 216 if ( restoreTextMode ) { 153 217 switchEditors.go( id, 'toggle' ); 154 218 } 219 220 // Show the pointer. 221 $( '#' + id + '-html' ).on( 'click', function() { 222 control.pasteHtmlPointer.hide(); // Hide the HTML pasting pointer. 223 224 if ( -1 !== component.dismissedPointers.indexOf( 'text_widget_custom_html' ) ) { 225 return; 226 } 227 control.customHtmlWidgetPointer.show(); 228 }); 229 230 // Hide the pointer when switching tabs. 231 $( '#' + id + '-tmce' ).on( 'click', function() { 232 control.customHtmlWidgetPointer.hide(); 233 }); 234 235 // Show pointer when pasting HTML. 236 editor.on( 'pastepreprocess', function( event ) { 237 var content = event.content; 238 if ( -1 !== component.dismissedPointers.indexOf( 'text_widget_paste_html' ) || ! content || ! /<\w+.*?>/.test( content ) ) { 239 return; 240 } 241 242 // Show the pointer after a slight delay so the user sees what they pasted. 243 _.delay( function() { 244 control.pasteHtmlPointer.show(); 245 }, 250 ); 246 }); 155 247 }; 156 248 157 249 if ( editor.initialized ) { … … wp.textWidgets = ( function( $ ) { 233 325 return; 234 326 } 235 327 328 // Bypass using TinyMCE when widget is in legacy mode. 329 if ( widgetForm.find( '.legacy' ).length > 0 ) { 330 return; 331 } 332 236 333 /* 237 334 * Create a container element for the widget control fields. 238 335 * This is inserted into the DOM immediately before the the .widget-content … … wp.textWidgets = ( function( $ ) { 337 434 * When WordPress enqueues this script, it should have an inline script 338 435 * attached which calls wp.textWidgets.init(). 339 436 * 437 * @param {object} exports Server exports. 438 * @param {object} exports.l10n Translations. 340 439 * @returns {void} 341 440 */ 342 component.init = function init( ) {441 component.init = function init( exports ) { 343 442 var $document = $( document ); 443 component.data = exports; 344 444 $document.on( 'widget-added', component.handleWidgetAdded ); 345 445 $document.on( 'widget-synced widget-updated', component.handleWidgetUpdated ); 346 446 -
src/wp-includes/script-loader.php
diff --git src/wp-includes/script-loader.php src/wp-includes/script-loader.php index 7562e2839b..2cd280937c 100644
function wp_default_styles( &$styles ) { 845 845 $styles->add( 'themes', "/wp-admin/css/themes$suffix.css" ); 846 846 $styles->add( 'about', "/wp-admin/css/about$suffix.css" ); 847 847 $styles->add( 'nav-menus', "/wp-admin/css/nav-menus$suffix.css" ); 848 $styles->add( 'widgets', "/wp-admin/css/widgets$suffix.css" );848 $styles->add( 'widgets', "/wp-admin/css/widgets$suffix.css", array( 'wp-pointer' ) ); 849 849 $styles->add( 'site-icon', "/wp-admin/css/site-icon$suffix.css" ); 850 850 $styles->add( 'l10n', "/wp-admin/css/l10n$suffix.css" ); 851 851 -
src/wp-includes/widgets/class-wp-widget-text.php
diff --git src/wp-includes/widgets/class-wp-widget-text.php src/wp-includes/widgets/class-wp-widget-text.php index 7d149c0a0f..210814d98b 100644
class WP_Widget_Text extends WP_Widget { 53 53 } 54 54 55 55 /** 56 * Determines whether a given instance is legacy and should bypass using TinyMCE. 57 * 58 * @since 4.8.1 59 * 60 * @param array $instance { 61 * Instance data. 62 * 63 * @type string $text Content. 64 * @type bool|string $filter Whether autop or content filters should apply. 65 * @type bool $legacy Whether widget is in legacy mode. 66 * } 67 * @return bool Whether Text widget instance contains legacy data. 68 */ 69 public function is_legacy_instance( $instance ) { 70 71 // If the widget has been updated while in legacy mode, it stays in legacy mode. 72 if ( ! empty( $instance['legacy'] ) ) { 73 return true; 74 } 75 76 // If the widget has been added/updated in 4.8 then filter prop is 'content' and it is no longer legacy. 77 if ( isset( $instance['filter'] ) && 'content' === $instance['filter'] ) { 78 return false; 79 } 80 81 // If the text is empty, then nothing is preventing migration to TinyMCE. 82 if ( empty( $instance['text'] ) ) { 83 return false; 84 } 85 86 $wpautop = ! empty( $instance['filter'] ); 87 $has_line_breaks = ( false !== strpos( $instance['text'], "\n" ) ); 88 89 // If auto-paragraphs are not enabled and there are line breaks, then ensure legacy mode. 90 if ( ! $wpautop && $has_line_breaks ) { 91 return true; 92 } 93 94 // If an HTML comment is present, assume legacy mode. 95 if ( false !== strpos( $instance['text'], '<!--' ) ) { 96 return true; 97 } 98 99 /* 100 * If a shortcode is present (with support added by a plugin), assume legacy mode 101 * since shortcodes would apply at the widget_text filter and thus be applied 102 * before wpautop runs at the widget_text_content filter. 103 */ 104 if ( preg_match( '/' . get_shortcode_regex() . '/', $instance['text'] ) ) { 105 return true; 106 } 107 108 // In the rare case that DOMDocument is not available we cannot reliably sniff content and so we assume legacy. 109 if ( ! class_exists( 'DOMDocument' ) ) { 110 // @codeCoverageIgnoreStart 111 return true; 112 // @codeCoverageIgnoreEnd 113 } 114 115 $doc = new DOMDocument(); 116 $doc->loadHTML( sprintf( 117 '<html><head><meta charset="%s"></head><body>%s</body></html>', 118 esc_attr( get_bloginfo( 'charset' ) ), 119 $instance['text'] 120 ) ); 121 $body = $doc->getElementsByTagName( 'body' )->item( 0 ); 122 123 // See $allowedposttags. 124 $safe_elements_attributes = array( 125 'strong' => array(), 126 'em' => array(), 127 'b' => array(), 128 'i' => array(), 129 'u' => array(), 130 's' => array(), 131 'ul' => array(), 132 'ol' => array(), 133 'li' => array(), 134 'hr' => array(), 135 'abbr' => array(), 136 'acronym' => array(), 137 'code' => array(), 138 'dfn' => array(), 139 'a' => array( 140 'href' => true, 141 ), 142 'img' => array( 143 'src' => true, 144 'alt' => true, 145 ), 146 ); 147 $safe_empty_elements = array( 'img', 'hr', 'iframe' ); 148 149 foreach ( $body->getElementsByTagName( '*' ) as $element ) { 150 /** @var DOMElement $element */ 151 $tag_name = strtolower( $element->nodeName ); 152 153 // If the element is not safe, then the instance is legacy. 154 if ( ! isset( $safe_elements_attributes[ $tag_name ] ) ) { 155 return true; 156 } 157 158 // If the element is not safely empty and it has empty contents, then legacy mode. 159 if ( ! in_array( $tag_name, $safe_empty_elements, true ) && '' === trim( $element->textContent ) ) { 160 return true; 161 } 162 163 // If an attribute is not recognized as safe, then the instance is legacy. 164 foreach ( $element->attributes as $attribute ) { 165 /** @var DOMAttr $attribute */ 166 $attribute_name = strtolower( $attribute->nodeName ); 167 168 if ( ! isset( $safe_elements_attributes[ $tag_name ][ $attribute_name ] ) ) { 169 return true; 170 } 171 } 172 } 173 174 // Otherwise, the text contains no elements/attributes that TinyMCE could drop, and therefore the widget does not need legacy mode. 175 return false; 176 } 177 178 /** 56 179 * Outputs the content for the current Text widget instance. 57 180 * 58 181 * @since 2.8.0 … … class WP_Widget_Text extends WP_Widget { 68 191 $title = apply_filters( 'widget_title', empty( $instance['title'] ) ? '' : $instance['title'], $instance, $this->id_base ); 69 192 70 193 $text = ! empty( $instance['text'] ) ? $instance['text'] : ''; 194 $is_visual_text_widget = ( isset( $instance['filter'] ) && 'content' === $instance['filter'] ); 195 196 /* 197 * Just-in-time temporarily upgrade Visual Text widget shortcode handling 198 * (with support added by plugin) from the widget_text filter to 199 * widget_text_content:11 to prevent wpautop from corrupting HTML output 200 * added by the shortcode. 201 */ 202 $widget_text_do_shortcode_priority = has_filter( 'widget_text', 'do_shortcode' ); 203 $should_upgrade_shortcode_handling = ( $is_visual_text_widget && false !== $widget_text_do_shortcode_priority ); 204 if ( $should_upgrade_shortcode_handling ) { 205 remove_filter( 'widget_text', 'do_shortcode', $widget_text_do_shortcode_priority ); 206 add_filter( 'widget_text_content', 'do_shortcode', 11 ); 207 } 71 208 72 209 /** 73 210 * Filters the content of the Text widget. … … class WP_Widget_Text extends WP_Widget { 102 239 } 103 240 } 104 241 242 // Undo temporary upgrade of the plugin-supplied shortcode handling. 243 if ( $should_upgrade_shortcode_handling ) { 244 remove_filter( 'widget_text_content', 'do_shortcode', 11 ); 245 add_filter( 'widget_text', 'do_shortcode', $widget_text_do_shortcode_priority ); 246 } 247 105 248 echo $args['before_widget']; 106 249 if ( ! empty( $title ) ) { 107 250 echo $args['before_title'] . $title . $args['after_title']; … … class WP_Widget_Text extends WP_Widget { 134 277 } 135 278 136 279 /* 137 * Re-use legacy 'filter' (wpautop) property to now indicate content filters will always apply. 280 * If the Text widget is in legacy mode, then a hidden input will indicate this 281 * and the new content value for the filter prop will by bypassed. Otherwise, 282 * re-use legacy 'filter' (wpautop) property to now indicate content filters will always apply. 138 283 * Prior to 4.8, this is a boolean value used to indicate whether or not wpautop should be 139 284 * applied. By re-using this property, downgrading WordPress from 4.8 to 4.7 will ensure 140 285 * that the content for Text widgets created with TinyMCE will continue to get wpautop. 141 286 */ 142 $instance['filter'] = 'content'; 287 if ( isset( $new_instance['legacy'] ) || isset( $old_instance['legacy'] ) || ( isset( $new_instance['filter'] ) && 'content' !== $new_instance['filter'] ) ) { 288 $instance['filter'] = ! empty( $new_instance['filter'] ); 289 $instance['legacy'] = true; 290 } else { 291 $instance['filter'] = 'content'; 292 unset( $instance['legacy'] ); 293 } 143 294 144 295 return $instance; 145 296 } … … class WP_Widget_Text extends WP_Widget { 153 304 public function enqueue_admin_scripts() { 154 305 wp_enqueue_editor(); 155 306 wp_enqueue_script( 'text-widgets' ); 307 wp_enqueue_style( 'wp-pointer' ); 156 308 } 157 309 158 310 /** … … class WP_Widget_Text extends WP_Widget { 160 312 * 161 313 * @since 2.8.0 162 314 * @since 4.8.0 Form only contains hidden inputs which are synced with JS template. 315 * @since 4.8.1 Restored original form to be displayed when in legacy mode. 163 316 * @access public 164 317 * @see WP_Widget_Visual_Text::render_control_template_scripts() 165 318 * … … class WP_Widget_Text extends WP_Widget { 175 328 ) 176 329 ); 177 330 ?> 178 <input id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" class="title" type="hidden" value="<?php echo esc_attr( $instance['title'] ); ?>"> 179 <input id="<?php echo $this->get_field_id( 'text' ); ?>" name="<?php echo $this->get_field_name( 'text' ); ?>" class="text" type="hidden" value="<?php echo esc_attr( $instance['text'] ); ?>"> 180 <?php 331 <?php if ( ! $this->is_legacy_instance( $instance ) ) : ?> 332 <input id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" class="title" type="hidden" value="<?php echo esc_attr( $instance['title'] ); ?>"> 333 <input id="<?php echo $this->get_field_id( 'text' ); ?>" name="<?php echo $this->get_field_name( 'text' ); ?>" class="text" type="hidden" value="<?php echo esc_attr( $instance['text'] ); ?>"> 334 <?php else : ?> 335 <input name="<?php echo $this->get_field_name( 'legacy' ); ?>" type="hidden" class="legacy" value="true"> 336 <p> 337 <label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'Title:' ); ?></label> 338 <input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $instance['title'] ); ?>"/> 339 </p> 340 <div class="notice inline notice-info notice-alt"> 341 <p><?php _e( 'This widget contains code that may work better in the new “Custom HTML” widget. How about trying that widget instead?' ); ?></p> 342 </div> 343 <p> 344 <label for="<?php echo $this->get_field_id( 'text' ); ?>"><?php _e( 'Content:' ); ?></label> 345 <textarea class="widefat" rows="16" cols="20" id="<?php echo $this->get_field_id( 'text' ); ?>" name="<?php echo $this->get_field_name( 'text' ); ?>"><?php echo esc_textarea( $instance['text'] ); ?></textarea> 346 </p> 347 <p> 348 <input id="<?php echo $this->get_field_id( 'filter' ); ?>" name="<?php echo $this->get_field_name( 'filter' ); ?>" type="checkbox"<?php checked( ! empty( $instance['filter'] ) ); ?> /> <label for="<?php echo $this->get_field_id( 'filter' ); ?>"><?php _e( 'Automatically add paragraphs' ); ?></label> 349 </p> 350 <?php endif; 181 351 } 182 352 183 353 /** … … class WP_Widget_Text extends WP_Widget { 187 357 * @access public 188 358 */ 189 359 public function render_control_template_scripts() { 360 $dismissed_pointers = explode( ',', (string) get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true ) ); 190 361 ?> 191 362 <script type="text/html" id="tmpl-widget-text-control-fields"> 192 363 <# var elementIdPrefix = 'el' + String( Math.random() ).replace( /\D/g, '' ) + '_' #> … … class WP_Widget_Text extends WP_Widget { 194 365 <label for="{{ elementIdPrefix }}title"><?php esc_html_e( 'Title:' ); ?></label> 195 366 <input id="{{ elementIdPrefix }}title" type="text" class="widefat title"> 196 367 </p> 368 369 <?php if ( ! in_array( 'text_widget_custom_html', $dismissed_pointers ) ) : ?> 370 <div hidden class="wp-pointer custom-html-widget-pointer wp-pointer-top"> 371 <div class="wp-pointer-content"> 372 <h3><?php _e( 'New Custom HTML Widget' ); ?></h3> 373 <?php if ( is_customize_preview() ) : ?> 374 <p><?php _e( 'Hey, did you hear we have a “Custom HTML” widget now? You can find it by pressing the “<a class="add-widget" href="#">Add a Widget</a>” button and searching for “HTML”. Check it out to add some custom code to your site!' ); ?></p> 375 <?php else : ?> 376 <p><?php _e( 'Hey, did you hear we have a “Custom HTML” widget now? You can find it by scanning the list of available widgets on this screen. Check it out to add some custom code to your site!' ); ?></p> 377 <?php endif; ?> 378 <div class="wp-pointer-buttons"> 379 <a class="close" href="#"><?php _e( 'Dismiss' ); ?></a> 380 </div> 381 </div> 382 <div class="wp-pointer-arrow"> 383 <div class="wp-pointer-arrow-inner"></div> 384 </div> 385 </div> 386 <?php endif; ?> 387 388 <?php if ( ! in_array( 'text_widget_paste_html', $dismissed_pointers ) ) : ?> 389 <div hidden class="wp-pointer paste-html-pointer wp-pointer-top"> 390 <div class="wp-pointer-content"> 391 <h3><?php _e( 'Did you just paste HTML?' ); ?></h3> 392 <p><?php _e( 'Hey there, looks like you just pasted HTML into the “Visual” tab of the Text widget. You may want to paste your code into the “Text” tab instead. Alternately, try out the new “Custom HTML” widget!' ); ?></p> 393 <div class="wp-pointer-buttons"> 394 <a class="close" href="#"><?php _e( 'Dismiss' ); ?></a> 395 </div> 396 </div> 397 <div class="wp-pointer-arrow"> 398 <div class="wp-pointer-arrow-inner"></div> 399 </div> 400 </div> 401 <?php endif; ?> 402 197 403 <p> 198 404 <label for="{{ elementIdPrefix }}text" class="screen-reader-text"><?php esc_html_e( 'Content:' ); ?></label> 199 405 <textarea id="{{ elementIdPrefix }}text" class="widefat text wp-editor-area" style="height: 200px" rows="16" cols="20"></textarea> -
tests/phpunit/tests/widgets/text-widget.php
diff --git tests/phpunit/tests/widgets/text-widget.php tests/phpunit/tests/widgets/text-widget.php index a28e6a668a..af3e2755f5 100644
class Test_WP_Widget_Text extends WP_UnitTestCase { 40 40 } 41 41 42 42 /** 43 * Test constructor method. 44 * 45 * @covers WP_Widget_Text::__construct 46 */ 47 function test_construct() { 48 $widget = new WP_Widget_Text(); 49 $this->assertEquals( 'text', $widget->id_base ); 50 $this->assertEquals( 'widget_text', $widget->widget_options['classname'] ); 51 $this->assertTrue( $widget->widget_options['customize_selective_refresh'] ); 52 $this->assertEquals( 400, $widget->control_options['width'] ); 53 $this->assertEquals( 350, $widget->control_options['height'] ); 54 } 55 56 /** 43 57 * Test enqueue_admin_scripts method. 44 58 * 45 59 * @covers WP_Widget_Text::_register … … class Test_WP_Widget_Text extends WP_UnitTestCase { 122 136 } 123 137 124 138 /** 139 * Example shortcode content to test for wpautop corruption. 140 * 141 * @var string 142 */ 143 protected $example_shortcode_content = "<p>One\nTwo\n\nThree</p>\n<script>\ndocument.write('Test1');\n\ndocument.write('Test2');\n</script>"; 144 145 /** 146 * Do example shortcode. 147 * 148 * @return string Shortcode content. 149 */ 150 function do_example_shortcode() { 151 return $this->example_shortcode_content; 152 } 153 154 /** 155 * Test widget method when a plugin has added shortcode support. 156 * 157 * @covers WP_Widget_Text::widget 158 */ 159 function test_widget_shortcodes() { 160 $args = array( 161 'before_title' => '<h2>', 162 'after_title' => "</h2>\n", 163 'before_widget' => '<section>', 164 'after_widget' => "</section>\n", 165 ); 166 $widget = new WP_Widget_Text(); 167 add_filter( 'widget_text', 'do_shortcode' ); 168 add_shortcode( 'example', array( $this, 'do_example_shortcode' ) ); 169 170 $base_instance = array( 171 'title' => 'Example', 172 'text' => "This is an example:\n\n[example]", 173 'filter' => false, 174 ); 175 176 // Legacy Text Widget. 177 $instance = array_merge( $base_instance, array( 178 'filter' => false, 179 ) ); 180 ob_start(); 181 $widget->widget( $args, $instance ); 182 $output = ob_get_clean(); 183 $this->assertContains( $this->example_shortcode_content, $output, 'Shortcode was applied without wpautop corrupting it.' ); 184 $this->assertEquals( 10, has_filter( 'widget_text', 'do_shortcode' ), 'Filter was restored.' ); 185 186 // Visual Text Widget. 187 $instance = array_merge( $base_instance, array( 188 'filter' => 'content', 189 ) ); 190 ob_start(); 191 $widget->widget( $args, $instance ); 192 $output = ob_get_clean(); 193 $this->assertContains( $this->example_shortcode_content, $output, 'Shortcode was applied without wpautop corrupting it.' ); 194 $this->assertEquals( 10, has_filter( 'widget_text', 'do_shortcode' ), 'Filter was restored.' ); 195 $this->assertFalse( has_filter( 'widget_text_content', 'do_shortcode' ), 'Filter was removed.' ); 196 197 // Visual Text Widget with properly-used widget_text_content filter. 198 remove_filter( 'widget_text', 'do_shortcode' ); 199 add_filter( 'widget_text_content', 'do_shortcode', 11 ); 200 $instance = array_merge( $base_instance, array( 201 'filter' => 'content', 202 ) ); 203 ob_start(); 204 $widget->widget( $args, $instance ); 205 $output = ob_get_clean(); 206 $this->assertContains( $this->example_shortcode_content, $output, 'Shortcode was applied without wpautop corrupting it.' ); 207 $this->assertFalse( has_filter( 'widget_text', 'do_shortcode' ), 'Filter was not erroneously restored.' ); 208 } 209 210 /** 125 211 * Filters the content of the Text widget. 126 212 * 127 213 * @param string $widget_text The widget content. … … class Test_WP_Widget_Text extends WP_UnitTestCase { 152 238 } 153 239 154 240 /** 241 * Test is_legacy_instance method. 242 * 243 * @covers WP_Widget_Text::is_legacy_instance 244 */ 245 function test_is_legacy_instance() { 246 $widget = new WP_Widget_Text(); 247 $base_instance = array( 248 'title' => 'Title', 249 'text' => "Hello\n\nWorld", 250 ); 251 252 $instance = array_merge( $base_instance, array( 253 'legacy' => true, 254 ) ); 255 $this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy when legacy prop is present.' ); 256 257 $instance = array_merge( $base_instance, array( 258 'filter' => 'content', 259 ) ); 260 $this->assertFalse( $widget->is_legacy_instance( $instance ), 'Not legacy when filter is explicitly content.' ); 261 262 $instance = array_merge( $base_instance, array( 263 'text' => '', 264 'filter' => true, 265 ) ); 266 $this->assertFalse( $widget->is_legacy_instance( $instance ), 'Not legacy when text is empty.' ); 267 268 $instance = array_merge( $base_instance, array( 269 'text' => "One\nTwo", 270 'filter' => false, 271 ) ); 272 $this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy when not-wpautop and there are line breaks.' ); 273 274 $instance = array_merge( $base_instance, array( 275 'text' => "One\n\nTwo", 276 'filter' => false, 277 ) ); 278 $this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy when not-wpautop and there are paragraph breaks.' ); 279 280 $instance = array_merge( $base_instance, array( 281 'text' => "One\nTwo", 282 'filter' => true, 283 ) ); 284 $this->assertFalse( $widget->is_legacy_instance( $instance ), 'Not automatically legacy when wpautop and there are line breaks.' ); 285 286 $instance = array_merge( $base_instance, array( 287 'text' => "One\n\nTwo", 288 'filter' => true, 289 ) ); 290 $this->assertFalse( $widget->is_legacy_instance( $instance ), 'Not automatically legacy when wpautop and there are paragraph breaks.' ); 291 292 $instance = array_merge( $base_instance, array( 293 'text' => 'Test<!-- comment -->', 294 'filter' => true, 295 ) ); 296 $this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy when HTML comment is present.' ); 297 298 $instance = array_merge( $base_instance, array( 299 'text' => 'Here is a [gallery]', 300 'filter' => true, 301 ) ); 302 $this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy mode when a shortcode is present.' ); 303 304 // Check text examples that will not migrate to TinyMCE. 305 $legacy_text_examples = array( 306 '<span class="hello"></span>', 307 '<span></span>', 308 "<ul>\n<li><a href=\"#\" class=\"location\"></a>List Item 1</li>\n<li><a href=\"#\" class=\"location\"></a>List Item 2</li>\n</ul>", 309 '<a href="#" class="map"></a>', 310 "<script>\n\\Line one\n\n\\Line two</script>", 311 "<style>body {\ncolor:red;\n}</style>", 312 '<span class="fa fa-cc-discover fa-2x" aria-hidden="true"></span>', 313 "<p>\nStay updated with our latest news and specials. We never sell your information and you can unsubscribe at any time.\n</p>\n\n<div class=\"custom-form-class\">\n\t<form action=\"#\" method=\"post\" name=\"mc-embedded-subscribe-form\">\n\n\t\t<label class=\"screen-reader-text\" for=\"mce-EMAIL-b\">Email </label>\n\t\t<input id=\"mce-EMAIL-b\" class=\"required email\" name=\"EMAIL\" required=\"\" type=\"email\" value=\"\" placeholder=\"Email Address*\" />\n\n\t\t<input class=\"button\" name=\"subscribe\" type=\"submit\" value=\"Go!\" />\n\n\t</form>\n</div>", 314 '<span class="sectiondown"><a href="#front-page-3"><i class="fa fa-chevron-circle-down"></i></a></span>', 315 ); 316 foreach ( $legacy_text_examples as $legacy_text_example ) { 317 $instance = array_merge( $base_instance, array( 318 'text' => $legacy_text_example, 319 'filter' => true, 320 ) ); 321 $this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy when wpautop and there is HTML that is not liable to be mutated.' ); 322 323 $instance = array_merge( $base_instance, array( 324 'text' => $legacy_text_example, 325 'filter' => false, 326 ) ); 327 $this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy when not-wpautop and there is HTML that is not liable to be mutated.' ); 328 } 329 330 // Check text examples that will migrate to TinyMCE, where elements and attributes are not in whitelist. 331 $migratable_text_examples = array( 332 'Check out <a href="http://example.com">Example</a>', 333 '<img src="http://example.com/img.jpg" alt="Img">', 334 '<strong><em>Hello</em></strong>', 335 '<b><i><u><s>Hello</s></u></i></b>', 336 "<ul>\n<li>One</li>\n<li>One</li>\n<li>One</li>\n</ul>", 337 "<ol>\n<li>One</li>\n<li>One</li>\n<li>One</li>\n</ol>", 338 "Text\n<hr>\nAddendum", 339 "Look at this code:\n\n<code>echo 'Hello World!';</code>", 340 ); 341 foreach ( $migratable_text_examples as $migratable_text_example ) { 342 $instance = array_merge( $base_instance, array( 343 'text' => $migratable_text_example, 344 'filter' => true, 345 ) ); 346 $this->assertFalse( $widget->is_legacy_instance( $instance ), 'Legacy when wpautop and there is HTML that is not liable to be mutated.' ); 347 } 348 } 349 350 /** 351 * Test update method. 352 * 353 * @covers WP_Widget_Text::form 354 */ 355 function test_form() { 356 $widget = new WP_Widget_Text(); 357 $instance = array( 358 'title' => 'Title', 359 'text' => 'Text', 360 'filter' => false, 361 'legacy' => true, 362 ); 363 $this->assertTrue( $widget->is_legacy_instance( $instance ) ); 364 ob_start(); 365 $widget->form( $instance ); 366 $form = ob_get_clean(); 367 $this->assertContains( 'class="legacy"', $form ); 368 369 $instance = array( 370 'title' => 'Title', 371 'text' => 'Text', 372 'filter' => 'content', 373 ); 374 $this->assertFalse( $widget->is_legacy_instance( $instance ) ); 375 ob_start(); 376 $widget->form( $instance ); 377 $form = ob_get_clean(); 378 $this->assertNotContains( 'class="legacy"', $form ); 379 } 380 381 /** 155 382 * Test update method. 156 383 * 157 384 * @covers WP_Widget_Text::update … … class Test_WP_Widget_Text extends WP_UnitTestCase { 161 388 $instance = array( 162 389 'title' => "The\nTitle", 163 390 'text' => "The\n\nText", 164 'filter' => false,391 'filter' => 'content', 165 392 ); 166 393 167 394 wp_set_current_user( $this->factory()->user->create( array( 168 395 'role' => 'administrator', 169 396 ) ) ); 170 397 171 // Should return valid instance .398 // Should return valid instance in legacy mode since filter=false and there are line breaks. 172 399 $expected = array( 173 400 'title' => sanitize_text_field( $instance['title'] ), 174 401 'text' => $instance['text'], 175 402 'filter' => 'content', 176 403 ); 177 404 $result = $widget->update( $instance, array() ); 178 $this->assertEquals( $ result, $expected);405 $this->assertEquals( $expected, $result ); 179 406 $this->assertTrue( ! empty( $expected['filter'] ), 'Expected filter prop to be truthy, to handle case where 4.8 is downgraded to 4.7.' ); 180 407 181 408 // Make sure KSES is applying as expected. … … class Test_WP_Widget_Text extends WP_UnitTestCase { 184 411 $instance['text'] = '<script>alert( "Howdy!" );</script>'; 185 412 $expected['text'] = $instance['text']; 186 413 $result = $widget->update( $instance, array() ); 187 $this->assertEquals( $ result, $expected);414 $this->assertEquals( $expected, $result ); 188 415 remove_filter( 'map_meta_cap', array( $this, 'grant_unfiltered_html_cap' ) ); 189 416 190 417 add_filter( 'map_meta_cap', array( $this, 'revoke_unfiltered_html_cap' ), 10, 2 ); … … class Test_WP_Widget_Text extends WP_UnitTestCase { 192 419 $instance['text'] = '<script>alert( "Howdy!" );</script>'; 193 420 $expected['text'] = wp_kses_post( $instance['text'] ); 194 421 $result = $widget->update( $instance, array() ); 195 $this->assertEquals( $ result, $expected);422 $this->assertEquals( $expected, $result ); 196 423 remove_filter( 'map_meta_cap', array( $this, 'revoke_unfiltered_html_cap' ), 10 ); 197 424 } 198 425 199 426 /** 427 * Test update for legacy widgets. 428 * 429 * @covers WP_Widget_Text::update 430 */ 431 function test_update_legacy() { 432 $widget = new WP_Widget_Text(); 433 434 // Updating a widget with explicit filter=true persists with legacy mode. 435 $instance = array( 436 'title' => 'Legacy', 437 'text' => 'Text', 438 'filter' => true, 439 ); 440 $result = $widget->update( $instance, array() ); 441 $expected = array_merge( $instance, array( 442 'legacy' => true, 443 'filter' => true, 444 ) ); 445 $this->assertEquals( $expected, $result ); 446 447 // Updating a widget with explicit filter=false persists with legacy mode. 448 $instance['filter'] = false; 449 $result = $widget->update( $instance, array() ); 450 $expected = array_merge( $instance, array( 451 'legacy' => true, 452 'filter' => false, 453 ) ); 454 $this->assertEquals( $expected, $result ); 455 456 // Updating a widget in legacy form results in filter=false when checkbox not checked. 457 $instance['filter'] = true; 458 $result = $widget->update( $instance, array() ); 459 $expected = array_merge( $instance, array( 460 'legacy' => true, 461 'filter' => true, 462 ) ); 463 $this->assertEquals( $expected, $result ); 464 465 // Updating a widget that previously had legacy form results in filter persisting. 466 unset( $instance['legacy'] ); 467 $instance['filter'] = true; 468 $result = $widget->update( $instance, array( 469 'legacy' => true, 470 ) ); 471 $expected = array_merge( $instance, array( 472 'legacy' => true, 473 'filter' => true, 474 ) ); 475 $this->assertEquals( $expected, $result ); 476 } 477 478 /** 200 479 * Grant unfiltered_html cap via map_meta_cap. 201 480 * 202 481 * @param array $caps Returns the user's actual capabilities.