Ticket #40951: 40951.3.diff
File 40951.3.diff, 21.2 KB (added by , 8 years ago) |
---|
-
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 6f057044cf..3796c3b29d 100644
3 3 wp.textWidgets = ( function( $ ) { 4 4 'use strict'; 5 5 6 var component = {}; 6 var component = { 7 data: { 8 custom_html_pointer_dismissed: false, 9 l10n: { 10 pointer_heading: '', 11 pointer_text: '' 12 } 13 } 14 }; 7 15 8 16 /** 9 17 * Text widget control. … … wp.textWidgets = ( function( $ ) { 143 151 if ( restoreTextMode ) { 144 152 switchEditors.go( id, 'toggle' ); 145 153 } 154 155 $( '#' + id + '-html' ).one( 'click', function() { 156 var tabContainer; 157 if ( component.data.custom_html_pointer_dismissed ) { 158 return; 159 } 160 tabContainer = $( this ).parent(); 161 tabContainer.pointer({ 162 position: 'bottom', 163 align: 'right', 164 edge: 'right', 165 content: '<h3>' + component.data.l10n.pointer_heading + '</h3><p>' + component.data.l10n.pointer_text + '</p>', 166 close: function() { 167 wp.ajax.post( 'dismiss-wp-pointer', { 168 pointer: 'text_widget_custom_html' 169 }); 170 component.data.custom_html_pointer_dismissed = true; 171 } 172 }); 173 tabContainer.pointer( 'open' ); 174 tabContainer.pointer( 'widget' ).css( 'z-index', 999999 ); // Default z-index of 9999 is not enough in Customizer. 175 }); 146 176 }; 147 177 148 178 if ( editor.initialized ) { … … wp.textWidgets = ( function( $ ) { 224 254 return; 225 255 } 226 256 257 // Bypass using TinyMCE when widget is in legacy mode. 258 if ( widgetForm.find( '.legacy' ).length > 0 ) { 259 return; 260 } 261 227 262 /* 228 263 * Create a container element for the widget control fields. 229 264 * This is inserted into the DOM immediately before the the .widget-content … … wp.textWidgets = ( function( $ ) { 328 363 * When WordPress enqueues this script, it should have an inline script 329 364 * attached which calls wp.textWidgets.init(). 330 365 * 366 * @param {object} exports Server exports. 367 * @param {object} exports.l10n Translations. 331 368 * @returns {void} 332 369 */ 333 component.init = function init( ) {370 component.init = function init( exports ) { 334 371 var $document = $( document ); 372 component.data = exports; 335 373 $document.on( 'widget-added', component.handleWidgetAdded ); 336 374 $document.on( 'widget-synced widget-updated', component.handleWidgetUpdated ); 337 375 -
src/wp-includes/script-loader.php
diff --git src/wp-includes/script-loader.php src/wp-includes/script-loader.php index ba52778d2f..d48df598b9 100644
function wp_default_scripts( &$scripts ) { 608 608 $scripts->add( 'media-audio-widget', "/wp-admin/js/widgets/media-audio-widget$suffix.js", array( 'media-widgets', 'media-audiovideo' ) ); 609 609 $scripts->add( 'media-image-widget', "/wp-admin/js/widgets/media-image-widget$suffix.js", array( 'media-widgets' ) ); 610 610 $scripts->add( 'media-video-widget', "/wp-admin/js/widgets/media-video-widget$suffix.js", array( 'media-widgets', 'media-audiovideo' ) ); 611 $scripts->add( 'text-widgets', "/wp-admin/js/widgets/text-widgets$suffix.js", array( 'jquery', 'backbone', 'editor', 'wp-util' ) ); 612 $scripts->add_inline_script( 'text-widgets', 'wp.textWidgets.init();', 'after' ); 611 $scripts->add( 'text-widgets', "/wp-admin/js/widgets/text-widgets$suffix.js", array( 'jquery', 'backbone', 'editor', 'wp-util', 'wp-pointer' ) ); 612 $exports = array( 613 'custom_html_pointer_dismissed' => is_user_logged_in() && in_array( 'text_widget_custom_html', explode( ',', (string) get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true ) ) ), 614 'l10n' => array( 615 'pointer_heading' => __( 'New Custom HTML Widget' ), 616 'pointer_text' => __( 'Hey, did you hear we have a "Custom HTML" widget now? Check it out to add some custom code to your site!' ), 617 ), 618 ); 619 $scripts->add_inline_script( 'text-widgets', sprintf( 'wp.textWidgets.init( %s );', wp_json_encode( $exports ) ), 'after' ); 613 620 614 621 $scripts->add( 'theme', "/wp-admin/js/theme$suffix.js", array( 'wp-backbone', 'wp-a11y' ), false, 1 ); 615 622 -
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..c9f5badee2 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 88 // If auto-paragraphs are not enabled and there are line breaks, then ensure legacy mode. 89 if ( ! $wpautop && false === strpos( "\n", $instance['text'] ) ) { 90 return true; 91 } 92 93 // In the rare case that DOMDocument is not available we cannot reliably sniff content and so we assume legacy. 94 if ( ! class_exists( 'DOMDocument' ) ) { 95 // @codeCoverageIgnoreStart 96 return true; 97 // @codeCoverageIgnoreEnd 98 } 99 100 $doc = new DOMDocument(); 101 $doc->loadHTML( sprintf( 102 '<html><head><meta charset="%s"></head><body>%s</body></html>', 103 esc_attr( get_bloginfo( 'charset' ) ), 104 $instance['text'] 105 ) ); 106 $body = $doc->getElementsByTagName( 'body' )->item( 0 ); 107 108 // See $allowedposttags. 109 $safe_elements_attributes = array( 110 'strong' => array(), 111 'em' => array(), 112 'b' => array(), 113 'i' => array(), 114 'u' => array(), 115 's' => array(), 116 'ul' => array(), 117 'ol' => array(), 118 'li' => array(), 119 'hr' => array(), 120 'abbr' => array(), 121 'acronym' => array(), 122 'code' => array(), 123 'dfn' => array(), 124 'a' => array( 125 'href' => true, 126 ), 127 'img' => array( 128 'src' => true, 129 'alt' => true, 130 ), 131 ); 132 $safe_empty_elements = array( 'img', 'hr', 'iframe' ); 133 134 foreach ( $body->getElementsByTagName( '*' ) as $element ) { 135 /** @var DOMElement $element */ 136 $tag_name = strtolower( $element->nodeName ); 137 138 // If the element is not safe, then the instance is legacy. 139 if ( ! isset( $safe_elements_attributes[ $tag_name ] ) ) { 140 return true; 141 } 142 143 // If the element is not safely empty and it has empty contents, then legacy mode. 144 if ( ! in_array( $tag_name, $safe_empty_elements, true ) && '' === trim( $element->textContent ) ) { 145 return true; 146 } 147 148 // If an attribute is not recognized as safe, then the instance is legacy. 149 foreach ( $element->attributes as $attribute ) { 150 /** @var DOMAttr $attribute */ 151 $attribute_name = strtolower( $attribute->nodeName ); 152 153 if ( ! isset( $safe_elements_attributes[ $tag_name ][ $attribute_name ] ) ) { 154 return true; 155 } 156 } 157 } 158 159 // Otherwise, the text contains no elements/attributes that TinyMCE could drop, and therefore the widget does not need legacy mode. 160 return false; 161 } 162 163 /** 56 164 * Outputs the content for the current Text widget instance. 57 165 * 58 166 * @since 2.8.0 … … class WP_Widget_Text extends WP_Widget { 134 242 } 135 243 136 244 /* 137 * Re-use legacy 'filter' (wpautop) property to now indicate content filters will always apply. 245 * If the Text widget is in legacy mode, then a hidden input will indicate this 246 * and the new content value for the filter prop will by bypassed. Otherwise, 247 * re-use legacy 'filter' (wpautop) property to now indicate content filters will always apply. 138 248 * Prior to 4.8, this is a boolean value used to indicate whether or not wpautop should be 139 249 * applied. By re-using this property, downgrading WordPress from 4.8 to 4.7 will ensure 140 250 * that the content for Text widgets created with TinyMCE will continue to get wpautop. 141 251 */ 142 $instance['filter'] = 'content'; 252 if ( isset( $new_instance['legacy'] ) || isset( $old_instance['legacy'] ) || ( isset( $new_instance['filter'] ) && 'content' !== $new_instance['filter'] ) ) { 253 $instance['filter'] = ! empty( $new_instance['filter'] ); 254 $instance['legacy'] = true; 255 } else { 256 $instance['filter'] = 'content'; 257 unset( $instance['legacy'] ); 258 } 143 259 144 260 return $instance; 145 261 } … … class WP_Widget_Text extends WP_Widget { 153 269 public function enqueue_admin_scripts() { 154 270 wp_enqueue_editor(); 155 271 wp_enqueue_script( 'text-widgets' ); 272 wp_enqueue_style( 'wp-pointer' ); 156 273 } 157 274 158 275 /** … … class WP_Widget_Text extends WP_Widget { 160 277 * 161 278 * @since 2.8.0 162 279 * @since 4.8.0 Form only contains hidden inputs which are synced with JS template. 280 * @since 4.8.1 Restored original form to be displayed when in legacy mode. 163 281 * @access public 164 282 * @see WP_Widget_Visual_Text::render_control_template_scripts() 165 283 * … … class WP_Widget_Text extends WP_Widget { 175 293 ) 176 294 ); 177 295 ?> 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 296 <?php if ( ! $this->is_legacy_instance( $instance ) ) : ?> 297 <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'] ); ?>"> 298 <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'] ); ?>"> 299 <?php else : ?> 300 <input name="<?php echo $this->get_field_name( 'legacy' ); ?>" type="hidden" class="legacy" value="true"> 301 <p> 302 <label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'Title:' ); ?></label> 303 <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'] ); ?>"/> 304 </p> 305 <div class="notice inline notice-info notice-alt"> 306 <p><?php _e( 'This widget contains code that may work better in the new “Custom HTML” widget. How about trying that widget instead?' ); ?></p> 307 </div> 308 <p> 309 <label for="<?php echo $this->get_field_id( 'text' ); ?>"><?php _e( 'Content:' ); ?></label> 310 <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> 311 </p> 312 <p> 313 <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> 314 </p> 315 <?php endif; 181 316 } 182 317 183 318 /** -
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..074b42b201 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 { 152 166 } 153 167 154 168 /** 169 * Test is_legacy_instance method. 170 * 171 * @covers WP_Widget_Text::is_legacy_instance 172 */ 173 function test_is_legacy_instance() { 174 $widget = new WP_Widget_Text(); 175 $base_instance = array( 176 'title' => 'Title', 177 'text' => "Hello\n\nWorld", 178 ); 179 180 $instance = array_merge( $base_instance, array( 181 'legacy' => true, 182 ) ); 183 $this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy when legacy prop is present.' ); 184 185 $instance = array_merge( $base_instance, array( 186 'filter' => 'content', 187 ) ); 188 $this->assertFalse( $widget->is_legacy_instance( $instance ), 'Not legacy when filter is explicitly content.' ); 189 190 $instance = array_merge( $base_instance, array( 191 'text' => '', 192 'filter' => true, 193 ) ); 194 $this->assertFalse( $widget->is_legacy_instance( $instance ), 'Not legacy when text is empty.' ); 195 196 $instance = array_merge( $base_instance, array( 197 'text' => "One\nTwo", 198 'filter' => false, 199 ) ); 200 $this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy when not-wpautop and there are line breaks.' ); 201 202 $instance = array_merge( $base_instance, array( 203 'text' => "One\n\nTwo", 204 'filter' => false, 205 ) ); 206 $this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy when not-wpautop and there are paragraph breaks.' ); 207 208 $instance = array_merge( $base_instance, array( 209 'text' => "One\nTwo", 210 'filter' => true, 211 ) ); 212 $this->assertFalse( $widget->is_legacy_instance( $instance ), 'Not automatically legacy when wpautop and there are line breaks.' ); 213 214 $instance = array_merge( $base_instance, array( 215 'text' => "One\n\nTwo", 216 'filter' => true, 217 ) ); 218 $this->assertFalse( $widget->is_legacy_instance( $instance ), 'Not automatically legacy when wpautop and there are paragraph breaks.' ); 219 220 // Check text examples that will not migrate to TinyMCE. 221 $legacy_text_examples = array( 222 '<span class="hello"></span>', 223 '<span></span>', 224 "<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>", 225 '<a href="#" class="map"></a>', 226 "<script>\n\\Line one\n\n\\Line two</script>", 227 "<style>body {\ncolor:red;\n}</style>", 228 '<span class="fa fa-cc-discover fa-2x" aria-hidden="true"></span>', 229 "<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>", 230 '<span class="sectiondown"><a href="#front-page-3"><i class="fa fa-chevron-circle-down"></i></a></span>', 231 ); 232 foreach ( $legacy_text_examples as $legacy_text_example ) { 233 $instance = array_merge( $base_instance, array( 234 'text' => $legacy_text_example, 235 'filter' => true, 236 ) ); 237 $this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy when wpautop and there is HTML that is not liable to be mutated.' ); 238 239 $instance = array_merge( $base_instance, array( 240 'text' => $legacy_text_example, 241 'filter' => false, 242 ) ); 243 $this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy when not-wpautop and there is HTML that is not liable to be mutated.' ); 244 } 245 246 // Check text examples that will migrate to TinyMCE, where elements and attributes are not in whitelist. 247 $migratable_text_examples = array( 248 'Check out <a href="http://example.com">Example</a>', 249 '<img src="http://example.com/img.jpg" alt="Img">', 250 '<strong><em>Hello</em></strong>', 251 '<b><i><u><s>Hello</s></u></i></b>', 252 "<ul>\n<li>One</li>\n<li>One</li>\n<li>One</li>\n</ul>", 253 "<ol>\n<li>One</li>\n<li>One</li>\n<li>One</li>\n</ol>", 254 "Text\n<hr>\nAddendum", 255 "Look at this code:\n\n<code>echo 'Hello World!';</code>", 256 ); 257 foreach ( $migratable_text_examples as $migratable_text_example ) { 258 $instance = array_merge( $base_instance, array( 259 'text' => $migratable_text_example, 260 'filter' => true, 261 ) ); 262 $this->assertFalse( $widget->is_legacy_instance( $instance ), 'Legacy when wpautop and there is HTML that is not liable to be mutated.' ); 263 } 264 } 265 266 /** 267 * Test update method. 268 * 269 * @covers WP_Widget_Text::form 270 */ 271 function test_form() { 272 $widget = new WP_Widget_Text(); 273 $instance = array( 274 'title' => 'Title', 275 'text' => 'Text', 276 'filter' => false, 277 'legacy' => true, 278 ); 279 $this->assertTrue( $widget->is_legacy_instance( $instance ) ); 280 ob_start(); 281 $widget->form( $instance ); 282 $form = ob_get_clean(); 283 $this->assertContains( 'class="legacy"', $form ); 284 285 $instance = array( 286 'title' => 'Title', 287 'text' => 'Text', 288 'filter' => 'content', 289 ); 290 $this->assertFalse( $widget->is_legacy_instance( $instance ) ); 291 ob_start(); 292 $widget->form( $instance ); 293 $form = ob_get_clean(); 294 $this->assertNotContains( 'class="legacy"', $form ); 295 } 296 297 /** 155 298 * Test update method. 156 299 * 157 300 * @covers WP_Widget_Text::update … … class Test_WP_Widget_Text extends WP_UnitTestCase { 161 304 $instance = array( 162 305 'title' => "The\nTitle", 163 306 'text' => "The\n\nText", 164 'filter' => false,307 'filter' => 'content', 165 308 ); 166 309 167 310 wp_set_current_user( $this->factory()->user->create( array( 168 311 'role' => 'administrator', 169 312 ) ) ); 170 313 171 // Should return valid instance .314 // Should return valid instance in legacy mode since filter=false and there are line breaks. 172 315 $expected = array( 173 316 'title' => sanitize_text_field( $instance['title'] ), 174 317 'text' => $instance['text'], 175 318 'filter' => 'content', 176 319 ); 177 320 $result = $widget->update( $instance, array() ); 178 $this->assertEquals( $ result, $expected);321 $this->assertEquals( $expected, $result ); 179 322 $this->assertTrue( ! empty( $expected['filter'] ), 'Expected filter prop to be truthy, to handle case where 4.8 is downgraded to 4.7.' ); 180 323 181 324 // Make sure KSES is applying as expected. … … class Test_WP_Widget_Text extends WP_UnitTestCase { 184 327 $instance['text'] = '<script>alert( "Howdy!" );</script>'; 185 328 $expected['text'] = $instance['text']; 186 329 $result = $widget->update( $instance, array() ); 187 $this->assertEquals( $ result, $expected);330 $this->assertEquals( $expected, $result ); 188 331 remove_filter( 'map_meta_cap', array( $this, 'grant_unfiltered_html_cap' ) ); 189 332 190 333 add_filter( 'map_meta_cap', array( $this, 'revoke_unfiltered_html_cap' ), 10, 2 ); … … class Test_WP_Widget_Text extends WP_UnitTestCase { 192 335 $instance['text'] = '<script>alert( "Howdy!" );</script>'; 193 336 $expected['text'] = wp_kses_post( $instance['text'] ); 194 337 $result = $widget->update( $instance, array() ); 195 $this->assertEquals( $ result, $expected);338 $this->assertEquals( $expected, $result ); 196 339 remove_filter( 'map_meta_cap', array( $this, 'revoke_unfiltered_html_cap' ), 10 ); 197 340 } 198 341 199 342 /** 343 * Test update for legacy widgets. 344 * 345 * @covers WP_Widget_Text::update 346 */ 347 function test_update_legacy() { 348 $widget = new WP_Widget_Text(); 349 350 // Updating a widget with explicit filter=true persists with legacy mode. 351 $instance = array( 352 'title' => 'Legacy', 353 'text' => 'Text', 354 'filter' => true, 355 ); 356 $result = $widget->update( $instance, array() ); 357 $expected = array_merge( $instance, array( 358 'legacy' => true, 359 'filter' => true, 360 ) ); 361 $this->assertEquals( $expected, $result ); 362 363 // Updating a widget with explicit filter=false persists with legacy mode. 364 $instance['filter'] = false; 365 $result = $widget->update( $instance, array() ); 366 $expected = array_merge( $instance, array( 367 'legacy' => true, 368 'filter' => false, 369 ) ); 370 $this->assertEquals( $expected, $result ); 371 372 // Updating a widget in legacy form results in filter=false when checkbox not checked. 373 $instance['filter'] = true; 374 $result = $widget->update( $instance, array() ); 375 $expected = array_merge( $instance, array( 376 'legacy' => true, 377 'filter' => true, 378 ) ); 379 $this->assertEquals( $expected, $result ); 380 381 // Updating a widget that previously had legacy form results in filter persisting. 382 unset( $instance['legacy'] ); 383 $instance['filter'] = true; 384 $result = $widget->update( $instance, array( 385 'legacy' => true, 386 ) ); 387 $expected = array_merge( $instance, array( 388 'legacy' => true, 389 'filter' => true, 390 ) ); 391 $this->assertEquals( $expected, $result ); 392 } 393 394 /** 200 395 * Grant unfiltered_html cap via map_meta_cap. 201 396 * 202 397 * @param array $caps Returns the user's actual capabilities.