Make WordPress Core

Ticket #40951: 40951.11.diff

File 40951.11.diff, 31.7 KB (added by westonruter, 8 years ago)

Use legacy mode when HTML comment is present: https://github.com/xwp/wordpress-develop/pull/235/commits/57a24f42e464b731299d77d8be50e4c4f0101766

  • 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, 
    619619        cursor: move;
    620620}
    621621
     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
    622642/* =Media Queries
    623643-------------------------------------------------------------- */
    624644
  • 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
     
    33wp.textWidgets = ( function( $ ) {
    44        'use strict';
    55
    6         var component = {};
     6        var component = {
     7                dismissedPointers: []
     8        };
    79
    810        /**
    911         * Text widget control.
    wp.textWidgets = ( function( $ ) { 
    4547                        control.$el.addClass( 'text-widget-fields' );
    4648                        control.$el.html( wp.template( 'widget-text-control-fields' ) );
    4749
     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
    4873                        control.fields = {
    4974                                title: control.$el.find( '.title' ),
    5075                                text: control.$el.find( '.text' )
    wp.textWidgets = ( function( $ ) { 
    6691                },
    6792
    6893                /**
     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                /**
    69133                 * Update input fields from the sync fields.
    70134                 *
    71135                 * This function is called at the widget-updated and widget-synced events.
    wp.textWidgets = ( function( $ ) { 
    152216                                        if ( restoreTextMode ) {
    153217                                                switchEditors.go( id, 'toggle' );
    154218                                        }
     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                                        });
    155247                                };
    156248
    157249                                if ( editor.initialized ) {
    wp.textWidgets = ( function( $ ) { 
    233325                        return;
    234326                }
    235327
     328                // Bypass using TinyMCE when widget is in legacy mode.
     329                if ( widgetForm.find( '.legacy' ).length > 0 ) {
     330                        return;
     331                }
     332
    236333                /*
    237334                 * Create a container element for the widget control fields.
    238335                 * This is inserted into the DOM immediately before the the .widget-content
    wp.textWidgets = ( function( $ ) { 
    337434         * When WordPress enqueues this script, it should have an inline script
    338435         * attached which calls wp.textWidgets.init().
    339436         *
     437         * @param {object} exports      Server exports.
     438         * @param {object} exports.l10n Translations.
    340439         * @returns {void}
    341440         */
    342         component.init = function init() {
     441        component.init = function init( exports ) {
    343442                var $document = $( document );
     443                component.data = exports;
    344444                $document.on( 'widget-added', component.handleWidgetAdded );
    345445                $document.on( 'widget-synced widget-updated', component.handleWidgetUpdated );
    346446
  • 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 ) { 
    845845        $styles->add( 'themes',              "/wp-admin/css/themes$suffix.css" );
    846846        $styles->add( 'about',               "/wp-admin/css/about$suffix.css" );
    847847        $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' ) );
    849849        $styles->add( 'site-icon',           "/wp-admin/css/site-icon$suffix.css" );
    850850        $styles->add( 'l10n',                "/wp-admin/css/l10n$suffix.css" );
    851851
  • 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 { 
    5353        }
    5454
    5555        /**
     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        /**
    56179         * Outputs the content for the current Text widget instance.
    57180         *
    58181         * @since 2.8.0
    class WP_Widget_Text extends WP_Widget { 
    68191                $title = apply_filters( 'widget_title', empty( $instance['title'] ) ? '' : $instance['title'], $instance, $this->id_base );
    69192
    70193                $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                }
    71208
    72209                /**
    73210                 * Filters the content of the Text widget.
    class WP_Widget_Text extends WP_Widget { 
    102239                        }
    103240                }
    104241
     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
    105248                echo $args['before_widget'];
    106249                if ( ! empty( $title ) ) {
    107250                        echo $args['before_title'] . $title . $args['after_title'];
    class WP_Widget_Text extends WP_Widget { 
    134277                }
    135278
    136279                /*
    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.
    138283                 * Prior to 4.8, this is a boolean value used to indicate whether or not wpautop should be
    139284                 * applied. By re-using this property, downgrading WordPress from 4.8 to 4.7 will ensure
    140285                 * that the content for Text widgets created with TinyMCE will continue to get wpautop.
    141286                 */
    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                }
    143294
    144295                return $instance;
    145296        }
    class WP_Widget_Text extends WP_Widget { 
    153304        public function enqueue_admin_scripts() {
    154305                wp_enqueue_editor();
    155306                wp_enqueue_script( 'text-widgets' );
     307                wp_enqueue_style( 'wp-pointer' );
    156308        }
    157309
    158310        /**
    class WP_Widget_Text extends WP_Widget { 
    160312         *
    161313         * @since 2.8.0
    162314         * @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.
    163316         * @access public
    164317         * @see WP_Widget_Visual_Text::render_control_template_scripts()
    165318         *
    class WP_Widget_Text extends WP_Widget { 
    175328                        )
    176329                );
    177330                ?>
    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 &#8220;Custom HTML&#8221; 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'] ) ); ?> />&nbsp;<label for="<?php echo $this->get_field_id( 'filter' ); ?>"><?php _e( 'Automatically add paragraphs' ); ?></label>
     349                        </p>
     350                <?php endif;
    181351        }
    182352
    183353        /**
    class WP_Widget_Text extends WP_Widget { 
    187357         * @access public
    188358         */
    189359        public function render_control_template_scripts() {
     360                $dismissed_pointers = explode( ',', (string) get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true ) );
    190361                ?>
    191362                <script type="text/html" id="tmpl-widget-text-control-fields">
    192363                        <# var elementIdPrefix = 'el' + String( Math.random() ).replace( /\D/g, '' ) + '_' #>
    class WP_Widget_Text extends WP_Widget { 
    194365                                <label for="{{ elementIdPrefix }}title"><?php esc_html_e( 'Title:' ); ?></label>
    195366                                <input id="{{ elementIdPrefix }}title" type="text" class="widefat title">
    196367                        </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 &#8220;Custom HTML&#8221; widget now? You can find it by pressing the &#8220;<a class="add-widget" href="#">Add a Widget</a>&#8221; button and searching for &#8220;HTML&#8221;. 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 &#8220;Custom HTML&#8221; 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 &#8220;Visual&#8221; tab of the Text widget. You may want to paste your code into the &#8220;Text&#8221; tab instead. Alternately, try out the new &#8220;Custom HTML&#8221; 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
    197403                        <p>
    198404                                <label for="{{ elementIdPrefix }}text" class="screen-reader-text"><?php esc_html_e( 'Content:' ); ?></label>
    199405                                <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 { 
    4040        }
    4141
    4242        /**
     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        /**
    4357         * Test enqueue_admin_scripts method.
    4458         *
    4559         * @covers WP_Widget_Text::_register
    class Test_WP_Widget_Text extends WP_UnitTestCase { 
    122136        }
    123137
    124138        /**
     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        /**
    125211         * Filters the content of the Text widget.
    126212         *
    127213         * @param string         $widget_text The widget content.
    class Test_WP_Widget_Text extends WP_UnitTestCase { 
    152238        }
    153239
    154240        /**
     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        /**
    155382         * Test update method.
    156383         *
    157384         * @covers WP_Widget_Text::update
    class Test_WP_Widget_Text extends WP_UnitTestCase { 
    161388                $instance = array(
    162389                        'title' => "The\nTitle",
    163390                        'text'  => "The\n\nText",
    164                         'filter' => false,
     391                        'filter' => 'content',
    165392                );
    166393
    167394                wp_set_current_user( $this->factory()->user->create( array(
    168395                        'role' => 'administrator',
    169396                ) ) );
    170397
    171                 // Should return valid instance.
     398                // Should return valid instance in legacy mode since filter=false and there are line breaks.
    172399                $expected = array(
    173400                        'title'  => sanitize_text_field( $instance['title'] ),
    174401                        'text'   => $instance['text'],
    175402                        'filter' => 'content',
    176403                );
    177404                $result = $widget->update( $instance, array() );
    178                 $this->assertEquals( $result, $expected );
     405                $this->assertEquals( $expected, $result );
    179406                $this->assertTrue( ! empty( $expected['filter'] ), 'Expected filter prop to be truthy, to handle case where 4.8 is downgraded to 4.7.' );
    180407
    181408                // Make sure KSES is applying as expected.
    class Test_WP_Widget_Text extends WP_UnitTestCase { 
    184411                $instance['text'] = '<script>alert( "Howdy!" );</script>';
    185412                $expected['text'] = $instance['text'];
    186413                $result = $widget->update( $instance, array() );
    187                 $this->assertEquals( $result, $expected );
     414                $this->assertEquals( $expected, $result );
    188415                remove_filter( 'map_meta_cap', array( $this, 'grant_unfiltered_html_cap' ) );
    189416
    190417                add_filter( 'map_meta_cap', array( $this, 'revoke_unfiltered_html_cap' ), 10, 2 );
    class Test_WP_Widget_Text extends WP_UnitTestCase { 
    192419                $instance['text'] = '<script>alert( "Howdy!" );</script>';
    193420                $expected['text'] = wp_kses_post( $instance['text'] );
    194421                $result = $widget->update( $instance, array() );
    195                 $this->assertEquals( $result, $expected );
     422                $this->assertEquals( $expected, $result );
    196423                remove_filter( 'map_meta_cap', array( $this, 'revoke_unfiltered_html_cap' ), 10 );
    197424        }
    198425
    199426        /**
     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        /**
    200479         * Grant unfiltered_html cap via map_meta_cap.
    201480         *
    202481         * @param array  $caps    Returns the user's actual capabilities.