Ticket #40951: 40951.10.diff
File 40951.10.diff, 31.4 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..19327b5e79 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 /* 95 * If a shortcode is present (with support added by a plugin), assume legacy mode 96 * since shortcodes would apply at the widget_text filter and thus be applied 97 * before wpautop runs at the widget_text_content filter. 98 */ 99 if ( preg_match( '/' . get_shortcode_regex() . '/', $instance['text'] ) ) { 100 return true; 101 } 102 103 // In the rare case that DOMDocument is not available we cannot reliably sniff content and so we assume legacy. 104 if ( ! class_exists( 'DOMDocument' ) ) { 105 // @codeCoverageIgnoreStart 106 return true; 107 // @codeCoverageIgnoreEnd 108 } 109 110 $doc = new DOMDocument(); 111 $doc->loadHTML( sprintf( 112 '<html><head><meta charset="%s"></head><body>%s</body></html>', 113 esc_attr( get_bloginfo( 'charset' ) ), 114 $instance['text'] 115 ) ); 116 $body = $doc->getElementsByTagName( 'body' )->item( 0 ); 117 118 // See $allowedposttags. 119 $safe_elements_attributes = array( 120 'strong' => array(), 121 'em' => array(), 122 'b' => array(), 123 'i' => array(), 124 'u' => array(), 125 's' => array(), 126 'ul' => array(), 127 'ol' => array(), 128 'li' => array(), 129 'hr' => array(), 130 'abbr' => array(), 131 'acronym' => array(), 132 'code' => array(), 133 'dfn' => array(), 134 'a' => array( 135 'href' => true, 136 ), 137 'img' => array( 138 'src' => true, 139 'alt' => true, 140 ), 141 ); 142 $safe_empty_elements = array( 'img', 'hr', 'iframe' ); 143 144 foreach ( $body->getElementsByTagName( '*' ) as $element ) { 145 /** @var DOMElement $element */ 146 $tag_name = strtolower( $element->nodeName ); 147 148 // If the element is not safe, then the instance is legacy. 149 if ( ! isset( $safe_elements_attributes[ $tag_name ] ) ) { 150 return true; 151 } 152 153 // If the element is not safely empty and it has empty contents, then legacy mode. 154 if ( ! in_array( $tag_name, $safe_empty_elements, true ) && '' === trim( $element->textContent ) ) { 155 return true; 156 } 157 158 // If an attribute is not recognized as safe, then the instance is legacy. 159 foreach ( $element->attributes as $attribute ) { 160 /** @var DOMAttr $attribute */ 161 $attribute_name = strtolower( $attribute->nodeName ); 162 163 if ( ! isset( $safe_elements_attributes[ $tag_name ][ $attribute_name ] ) ) { 164 return true; 165 } 166 } 167 } 168 169 // Otherwise, the text contains no elements/attributes that TinyMCE could drop, and therefore the widget does not need legacy mode. 170 return false; 171 } 172 173 /** 56 174 * Outputs the content for the current Text widget instance. 57 175 * 58 176 * @since 2.8.0 … … class WP_Widget_Text extends WP_Widget { 68 186 $title = apply_filters( 'widget_title', empty( $instance['title'] ) ? '' : $instance['title'], $instance, $this->id_base ); 69 187 70 188 $text = ! empty( $instance['text'] ) ? $instance['text'] : ''; 189 $is_visual_text_widget = ( isset( $instance['filter'] ) && 'content' === $instance['filter'] ); 190 191 /* 192 * Just-in-time temporarily upgrade Visual Text widget shortcode handling 193 * (with support added by plugin) from the widget_text filter to 194 * widget_text_content:11 to prevent wpautop from corrupting HTML output 195 * added by the shortcode. 196 */ 197 $widget_text_do_shortcode_priority = has_filter( 'widget_text', 'do_shortcode' ); 198 $should_upgrade_shortcode_handling = ( $is_visual_text_widget && false !== $widget_text_do_shortcode_priority ); 199 if ( $should_upgrade_shortcode_handling ) { 200 remove_filter( 'widget_text', 'do_shortcode', $widget_text_do_shortcode_priority ); 201 add_filter( 'widget_text_content', 'do_shortcode', 11 ); 202 } 71 203 72 204 /** 73 205 * Filters the content of the Text widget. … … class WP_Widget_Text extends WP_Widget { 102 234 } 103 235 } 104 236 237 // Undo temporary upgrade of the plugin-supplied shortcode handling. 238 if ( $should_upgrade_shortcode_handling ) { 239 remove_filter( 'widget_text_content', 'do_shortcode', 11 ); 240 add_filter( 'widget_text', 'do_shortcode', $widget_text_do_shortcode_priority ); 241 } 242 105 243 echo $args['before_widget']; 106 244 if ( ! empty( $title ) ) { 107 245 echo $args['before_title'] . $title . $args['after_title']; … … class WP_Widget_Text extends WP_Widget { 134 272 } 135 273 136 274 /* 137 * Re-use legacy 'filter' (wpautop) property to now indicate content filters will always apply. 275 * If the Text widget is in legacy mode, then a hidden input will indicate this 276 * and the new content value for the filter prop will by bypassed. Otherwise, 277 * re-use legacy 'filter' (wpautop) property to now indicate content filters will always apply. 138 278 * Prior to 4.8, this is a boolean value used to indicate whether or not wpautop should be 139 279 * applied. By re-using this property, downgrading WordPress from 4.8 to 4.7 will ensure 140 280 * that the content for Text widgets created with TinyMCE will continue to get wpautop. 141 281 */ 142 $instance['filter'] = 'content'; 282 if ( isset( $new_instance['legacy'] ) || isset( $old_instance['legacy'] ) || ( isset( $new_instance['filter'] ) && 'content' !== $new_instance['filter'] ) ) { 283 $instance['filter'] = ! empty( $new_instance['filter'] ); 284 $instance['legacy'] = true; 285 } else { 286 $instance['filter'] = 'content'; 287 unset( $instance['legacy'] ); 288 } 143 289 144 290 return $instance; 145 291 } … … class WP_Widget_Text extends WP_Widget { 153 299 public function enqueue_admin_scripts() { 154 300 wp_enqueue_editor(); 155 301 wp_enqueue_script( 'text-widgets' ); 302 wp_enqueue_style( 'wp-pointer' ); 156 303 } 157 304 158 305 /** … … class WP_Widget_Text extends WP_Widget { 160 307 * 161 308 * @since 2.8.0 162 309 * @since 4.8.0 Form only contains hidden inputs which are synced with JS template. 310 * @since 4.8.1 Restored original form to be displayed when in legacy mode. 163 311 * @access public 164 312 * @see WP_Widget_Visual_Text::render_control_template_scripts() 165 313 * … … class WP_Widget_Text extends WP_Widget { 175 323 ) 176 324 ); 177 325 ?> 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 326 <?php if ( ! $this->is_legacy_instance( $instance ) ) : ?> 327 <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'] ); ?>"> 328 <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'] ); ?>"> 329 <?php else : ?> 330 <input name="<?php echo $this->get_field_name( 'legacy' ); ?>" type="hidden" class="legacy" value="true"> 331 <p> 332 <label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'Title:' ); ?></label> 333 <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'] ); ?>"/> 334 </p> 335 <div class="notice inline notice-info notice-alt"> 336 <p><?php _e( 'This widget contains code that may work better in the new “Custom HTML” widget. How about trying that widget instead?' ); ?></p> 337 </div> 338 <p> 339 <label for="<?php echo $this->get_field_id( 'text' ); ?>"><?php _e( 'Content:' ); ?></label> 340 <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> 341 </p> 342 <p> 343 <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> 344 </p> 345 <?php endif; 181 346 } 182 347 183 348 /** … … class WP_Widget_Text extends WP_Widget { 187 352 * @access public 188 353 */ 189 354 public function render_control_template_scripts() { 355 $dismissed_pointers = explode( ',', (string) get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true ) ); 190 356 ?> 191 357 <script type="text/html" id="tmpl-widget-text-control-fields"> 192 358 <# var elementIdPrefix = 'el' + String( Math.random() ).replace( /\D/g, '' ) + '_' #> … … class WP_Widget_Text extends WP_Widget { 194 360 <label for="{{ elementIdPrefix }}title"><?php esc_html_e( 'Title:' ); ?></label> 195 361 <input id="{{ elementIdPrefix }}title" type="text" class="widefat title"> 196 362 </p> 363 364 <?php if ( ! in_array( 'text_widget_custom_html', $dismissed_pointers ) ) : ?> 365 <div hidden class="wp-pointer custom-html-widget-pointer wp-pointer-top"> 366 <div class="wp-pointer-content"> 367 <h3><?php _e( 'New Custom HTML Widget' ); ?></h3> 368 <?php if ( is_customize_preview() ) : ?> 369 <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> 370 <?php else : ?> 371 <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> 372 <?php endif; ?> 373 <div class="wp-pointer-buttons"> 374 <a class="close" href="#"><?php _e( 'Dismiss' ); ?></a> 375 </div> 376 </div> 377 <div class="wp-pointer-arrow"> 378 <div class="wp-pointer-arrow-inner"></div> 379 </div> 380 </div> 381 <?php endif; ?> 382 383 <?php if ( ! in_array( 'text_widget_paste_html', $dismissed_pointers ) ) : ?> 384 <div hidden class="wp-pointer paste-html-pointer wp-pointer-top"> 385 <div class="wp-pointer-content"> 386 <h3><?php _e( 'Did you just paste HTML?' ); ?></h3> 387 <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> 388 <div class="wp-pointer-buttons"> 389 <a class="close" href="#"><?php _e( 'Dismiss' ); ?></a> 390 </div> 391 </div> 392 <div class="wp-pointer-arrow"> 393 <div class="wp-pointer-arrow-inner"></div> 394 </div> 395 </div> 396 <?php endif; ?> 397 197 398 <p> 198 399 <label for="{{ elementIdPrefix }}text" class="screen-reader-text"><?php esc_html_e( 'Content:' ); ?></label> 199 400 <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..40b5c387eb 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' => 'Here is a [gallery]', 294 'filter' => true, 295 ) ); 296 $this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy mode when a shortcode is present.' ); 297 298 // Check text examples that will not migrate to TinyMCE. 299 $legacy_text_examples = array( 300 '<span class="hello"></span>', 301 '<span></span>', 302 "<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>", 303 '<a href="#" class="map"></a>', 304 "<script>\n\\Line one\n\n\\Line two</script>", 305 "<style>body {\ncolor:red;\n}</style>", 306 '<span class="fa fa-cc-discover fa-2x" aria-hidden="true"></span>', 307 "<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>", 308 '<span class="sectiondown"><a href="#front-page-3"><i class="fa fa-chevron-circle-down"></i></a></span>', 309 ); 310 foreach ( $legacy_text_examples as $legacy_text_example ) { 311 $instance = array_merge( $base_instance, array( 312 'text' => $legacy_text_example, 313 'filter' => true, 314 ) ); 315 $this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy when wpautop and there is HTML that is not liable to be mutated.' ); 316 317 $instance = array_merge( $base_instance, array( 318 'text' => $legacy_text_example, 319 'filter' => false, 320 ) ); 321 $this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy when not-wpautop and there is HTML that is not liable to be mutated.' ); 322 } 323 324 // Check text examples that will migrate to TinyMCE, where elements and attributes are not in whitelist. 325 $migratable_text_examples = array( 326 'Check out <a href="http://example.com">Example</a>', 327 '<img src="http://example.com/img.jpg" alt="Img">', 328 '<strong><em>Hello</em></strong>', 329 '<b><i><u><s>Hello</s></u></i></b>', 330 "<ul>\n<li>One</li>\n<li>One</li>\n<li>One</li>\n</ul>", 331 "<ol>\n<li>One</li>\n<li>One</li>\n<li>One</li>\n</ol>", 332 "Text\n<hr>\nAddendum", 333 "Look at this code:\n\n<code>echo 'Hello World!';</code>", 334 ); 335 foreach ( $migratable_text_examples as $migratable_text_example ) { 336 $instance = array_merge( $base_instance, array( 337 'text' => $migratable_text_example, 338 'filter' => true, 339 ) ); 340 $this->assertFalse( $widget->is_legacy_instance( $instance ), 'Legacy when wpautop and there is HTML that is not liable to be mutated.' ); 341 } 342 } 343 344 /** 345 * Test update method. 346 * 347 * @covers WP_Widget_Text::form 348 */ 349 function test_form() { 350 $widget = new WP_Widget_Text(); 351 $instance = array( 352 'title' => 'Title', 353 'text' => 'Text', 354 'filter' => false, 355 'legacy' => true, 356 ); 357 $this->assertTrue( $widget->is_legacy_instance( $instance ) ); 358 ob_start(); 359 $widget->form( $instance ); 360 $form = ob_get_clean(); 361 $this->assertContains( 'class="legacy"', $form ); 362 363 $instance = array( 364 'title' => 'Title', 365 'text' => 'Text', 366 'filter' => 'content', 367 ); 368 $this->assertFalse( $widget->is_legacy_instance( $instance ) ); 369 ob_start(); 370 $widget->form( $instance ); 371 $form = ob_get_clean(); 372 $this->assertNotContains( 'class="legacy"', $form ); 373 } 374 375 /** 155 376 * Test update method. 156 377 * 157 378 * @covers WP_Widget_Text::update … … class Test_WP_Widget_Text extends WP_UnitTestCase { 161 382 $instance = array( 162 383 'title' => "The\nTitle", 163 384 'text' => "The\n\nText", 164 'filter' => false,385 'filter' => 'content', 165 386 ); 166 387 167 388 wp_set_current_user( $this->factory()->user->create( array( 168 389 'role' => 'administrator', 169 390 ) ) ); 170 391 171 // Should return valid instance .392 // Should return valid instance in legacy mode since filter=false and there are line breaks. 172 393 $expected = array( 173 394 'title' => sanitize_text_field( $instance['title'] ), 174 395 'text' => $instance['text'], 175 396 'filter' => 'content', 176 397 ); 177 398 $result = $widget->update( $instance, array() ); 178 $this->assertEquals( $ result, $expected);399 $this->assertEquals( $expected, $result ); 179 400 $this->assertTrue( ! empty( $expected['filter'] ), 'Expected filter prop to be truthy, to handle case where 4.8 is downgraded to 4.7.' ); 180 401 181 402 // Make sure KSES is applying as expected. … … class Test_WP_Widget_Text extends WP_UnitTestCase { 184 405 $instance['text'] = '<script>alert( "Howdy!" );</script>'; 185 406 $expected['text'] = $instance['text']; 186 407 $result = $widget->update( $instance, array() ); 187 $this->assertEquals( $ result, $expected);408 $this->assertEquals( $expected, $result ); 188 409 remove_filter( 'map_meta_cap', array( $this, 'grant_unfiltered_html_cap' ) ); 189 410 190 411 add_filter( 'map_meta_cap', array( $this, 'revoke_unfiltered_html_cap' ), 10, 2 ); … … class Test_WP_Widget_Text extends WP_UnitTestCase { 192 413 $instance['text'] = '<script>alert( "Howdy!" );</script>'; 193 414 $expected['text'] = wp_kses_post( $instance['text'] ); 194 415 $result = $widget->update( $instance, array() ); 195 $this->assertEquals( $ result, $expected);416 $this->assertEquals( $expected, $result ); 196 417 remove_filter( 'map_meta_cap', array( $this, 'revoke_unfiltered_html_cap' ), 10 ); 197 418 } 198 419 199 420 /** 421 * Test update for legacy widgets. 422 * 423 * @covers WP_Widget_Text::update 424 */ 425 function test_update_legacy() { 426 $widget = new WP_Widget_Text(); 427 428 // Updating a widget with explicit filter=true persists with legacy mode. 429 $instance = array( 430 'title' => 'Legacy', 431 'text' => 'Text', 432 'filter' => true, 433 ); 434 $result = $widget->update( $instance, array() ); 435 $expected = array_merge( $instance, array( 436 'legacy' => true, 437 'filter' => true, 438 ) ); 439 $this->assertEquals( $expected, $result ); 440 441 // Updating a widget with explicit filter=false persists with legacy mode. 442 $instance['filter'] = false; 443 $result = $widget->update( $instance, array() ); 444 $expected = array_merge( $instance, array( 445 'legacy' => true, 446 'filter' => false, 447 ) ); 448 $this->assertEquals( $expected, $result ); 449 450 // Updating a widget in legacy form results in filter=false when checkbox not checked. 451 $instance['filter'] = true; 452 $result = $widget->update( $instance, array() ); 453 $expected = array_merge( $instance, array( 454 'legacy' => true, 455 'filter' => true, 456 ) ); 457 $this->assertEquals( $expected, $result ); 458 459 // Updating a widget that previously had legacy form results in filter persisting. 460 unset( $instance['legacy'] ); 461 $instance['filter'] = true; 462 $result = $widget->update( $instance, array( 463 'legacy' => true, 464 ) ); 465 $expected = array_merge( $instance, array( 466 'legacy' => true, 467 'filter' => true, 468 ) ); 469 $this->assertEquals( $expected, $result ); 470 } 471 472 /** 200 473 * Grant unfiltered_html cap via map_meta_cap. 201 474 * 202 475 * @param array $caps Returns the user's actual capabilities.