Make WordPress Core

Ticket #40951: 40951.10.diff

File 40951.10.diff, 31.4 KB (added by westonruter, 8 years ago)

https://github.com/xwp/wordpress-develop/pull/235/files/6e6c550..d31f7ba

  • 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..19327b5e79 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                /*
     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        /**
    56174         * Outputs the content for the current Text widget instance.
    57175         *
    58176         * @since 2.8.0
    class WP_Widget_Text extends WP_Widget { 
    68186                $title = apply_filters( 'widget_title', empty( $instance['title'] ) ? '' : $instance['title'], $instance, $this->id_base );
    69187
    70188                $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                }
    71203
    72204                /**
    73205                 * Filters the content of the Text widget.
    class WP_Widget_Text extends WP_Widget { 
    102234                        }
    103235                }
    104236
     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
    105243                echo $args['before_widget'];
    106244                if ( ! empty( $title ) ) {
    107245                        echo $args['before_title'] . $title . $args['after_title'];
    class WP_Widget_Text extends WP_Widget { 
    134272                }
    135273
    136274                /*
    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.
    138278                 * Prior to 4.8, this is a boolean value used to indicate whether or not wpautop should be
    139279                 * applied. By re-using this property, downgrading WordPress from 4.8 to 4.7 will ensure
    140280                 * that the content for Text widgets created with TinyMCE will continue to get wpautop.
    141281                 */
    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                }
    143289
    144290                return $instance;
    145291        }
    class WP_Widget_Text extends WP_Widget { 
    153299        public function enqueue_admin_scripts() {
    154300                wp_enqueue_editor();
    155301                wp_enqueue_script( 'text-widgets' );
     302                wp_enqueue_style( 'wp-pointer' );
    156303        }
    157304
    158305        /**
    class WP_Widget_Text extends WP_Widget { 
    160307         *
    161308         * @since 2.8.0
    162309         * @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.
    163311         * @access public
    164312         * @see WP_Widget_Visual_Text::render_control_template_scripts()
    165313         *
    class WP_Widget_Text extends WP_Widget { 
    175323                        )
    176324                );
    177325                ?>
    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 &#8220;Custom HTML&#8221; 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'] ) ); ?> />&nbsp;<label for="<?php echo $this->get_field_id( 'filter' ); ?>"><?php _e( 'Automatically add paragraphs' ); ?></label>
     344                        </p>
     345                <?php endif;
    181346        }
    182347
    183348        /**
    class WP_Widget_Text extends WP_Widget { 
    187352         * @access public
    188353         */
    189354        public function render_control_template_scripts() {
     355                $dismissed_pointers = explode( ',', (string) get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true ) );
    190356                ?>
    191357                <script type="text/html" id="tmpl-widget-text-control-fields">
    192358                        <# var elementIdPrefix = 'el' + String( Math.random() ).replace( /\D/g, '' ) + '_' #>
    class WP_Widget_Text extends WP_Widget { 
    194360                                <label for="{{ elementIdPrefix }}title"><?php esc_html_e( 'Title:' ); ?></label>
    195361                                <input id="{{ elementIdPrefix }}title" type="text" class="widefat title">
    196362                        </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 &#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>
     370                                                <?php else : ?>
     371                                                        <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>
     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 &#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>
     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
    197398                        <p>
    198399                                <label for="{{ elementIdPrefix }}text" class="screen-reader-text"><?php esc_html_e( 'Content:' ); ?></label>
    199400                                <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 { 
    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' => '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        /**
    155376         * Test update method.
    156377         *
    157378         * @covers WP_Widget_Text::update
    class Test_WP_Widget_Text extends WP_UnitTestCase { 
    161382                $instance = array(
    162383                        'title' => "The\nTitle",
    163384                        'text'  => "The\n\nText",
    164                         'filter' => false,
     385                        'filter' => 'content',
    165386                );
    166387
    167388                wp_set_current_user( $this->factory()->user->create( array(
    168389                        'role' => 'administrator',
    169390                ) ) );
    170391
    171                 // Should return valid instance.
     392                // Should return valid instance in legacy mode since filter=false and there are line breaks.
    172393                $expected = array(
    173394                        'title'  => sanitize_text_field( $instance['title'] ),
    174395                        'text'   => $instance['text'],
    175396                        'filter' => 'content',
    176397                );
    177398                $result = $widget->update( $instance, array() );
    178                 $this->assertEquals( $result, $expected );
     399                $this->assertEquals( $expected, $result );
    179400                $this->assertTrue( ! empty( $expected['filter'] ), 'Expected filter prop to be truthy, to handle case where 4.8 is downgraded to 4.7.' );
    180401
    181402                // Make sure KSES is applying as expected.
    class Test_WP_Widget_Text extends WP_UnitTestCase { 
    184405                $instance['text'] = '<script>alert( "Howdy!" );</script>';
    185406                $expected['text'] = $instance['text'];
    186407                $result = $widget->update( $instance, array() );
    187                 $this->assertEquals( $result, $expected );
     408                $this->assertEquals( $expected, $result );
    188409                remove_filter( 'map_meta_cap', array( $this, 'grant_unfiltered_html_cap' ) );
    189410
    190411                add_filter( 'map_meta_cap', array( $this, 'revoke_unfiltered_html_cap' ), 10, 2 );
    class Test_WP_Widget_Text extends WP_UnitTestCase { 
    192413                $instance['text'] = '<script>alert( "Howdy!" );</script>';
    193414                $expected['text'] = wp_kses_post( $instance['text'] );
    194415                $result = $widget->update( $instance, array() );
    195                 $this->assertEquals( $result, $expected );
     416                $this->assertEquals( $expected, $result );
    196417                remove_filter( 'map_meta_cap', array( $this, 'revoke_unfiltered_html_cap' ), 10 );
    197418        }
    198419
    199420        /**
     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        /**
    200473         * Grant unfiltered_html cap via map_meta_cap.
    201474         *
    202475         * @param array  $caps    Returns the user's actual capabilities.