Make WordPress Core

Ticket #40951: 40951.12.diff

File 40951.12.diff, 32.1 KB (added by westonruter, 8 years ago)

Improve accessibility of pointers: https://github.com/xwp/wordpress-develop/pull/235/commits/2b7d10b087994b20dc205c1710c89e9d412189f7

  • src/wp-admin/css/widgets.css

    diff --git src/wp-admin/css/widgets.css src/wp-admin/css/widgets.css
    index d06fc29f70..cf02c7626b 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 [hidden] {
     628        display: none;
     629}
     630.text-widget-fields .wp-pointer.wp-pointer-top {
     631        position: absolute;
     632        z-index: 3;
     633        top: 100px;
     634        right: 10px;
     635        left: 10px;
     636}
     637.text-widget-fields .wp-pointer .wp-pointer-arrow {
     638        left: auto;
     639        right: 15px;
     640}
     641.text-widget-fields .wp-pointer .wp-pointer-buttons {
     642        line-height: 1.4em;
     643}
     644
    622645/* =Media Queries
    623646-------------------------------------------------------------- */
    624647
  • 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..a241f25f84 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.hide();
     55                                        $( '#' + control.fields.text.attr( 'id' ) + '-html' ).focus();
     56                                        control.dismissPointers( [ 'text_widget_custom_html' ] );
     57                                });
     58                                control.customHtmlWidgetPointer.find( '.add-widget' ).on( 'click', function( event ) {
     59                                        event.preventDefault();
     60                                        control.customHtmlWidgetPointer.hide();
     61                                        control.openAvailableWidgetsPanel();
     62                                });
     63                        }
     64
     65                        control.pasteHtmlPointer = control.$el.find( '.wp-pointer.paste-html-pointer' );
     66                        if ( control.pasteHtmlPointer.length ) {
     67                                control.pasteHtmlPointer.find( '.close' ).on( 'click', function( event ) {
     68                                        event.preventDefault();
     69                                        control.pasteHtmlPointer.hide();
     70                                        control.editor.focus();
     71                                        control.dismissPointers( [ 'text_widget_custom_html', 'text_widget_paste_html' ] );
     72                                });
     73                        }
     74
    4875                        control.fields = {
    4976                                title: control.$el.find( '.title' ),
    5077                                text: control.$el.find( '.text' )
    wp.textWidgets = ( function( $ ) { 
    6693                },
    6794
    6895                /**
     96                 * Dismiss pointers for Custom HTML widget.
     97                 *
     98                 * @since 4.8.1
     99                 *
     100                 * @param {Array} pointers Pointer IDs to dismiss.
     101                 * @returns {void}
     102                 */
     103                dismissPointers: function dismissPointers( pointers ) {
     104                        _.each( pointers, function( pointer ) {
     105                                wp.ajax.post( 'dismiss-wp-pointer', {
     106                                        pointer: pointer
     107                                });
     108                                component.dismissedPointers.push( pointer );
     109                        });
     110                },
     111
     112                /**
     113                 * Open available widgets panel.
     114                 *
     115                 * @since 4.8.1
     116                 * @returns {void}
     117                 */
     118                openAvailableWidgetsPanel: function openAvailableWidgetsPanel() {
     119                        var sidebarControl;
     120                        wp.customize.section.each( function( section ) {
     121                                if ( section.extended( wp.customize.Widgets.SidebarSection ) && section.expanded() ) {
     122                                        sidebarControl = wp.customize.control( 'sidebars_widgets[' + section.params.sidebarId + ']' );
     123                                }
     124                        });
     125                        if ( ! sidebarControl ) {
     126                                return;
     127                        }
     128                        setTimeout( function() { // Timeout to prevent click event from causing panel to immediately collapse.
     129                                wp.customize.Widgets.availableWidgetsPanel.open( sidebarControl );
     130                                wp.customize.Widgets.availableWidgetsPanel.$search.val( 'HTML' ).trigger( 'keyup' );
     131                        });
     132                },
     133
     134                /**
    69135                 * Update input fields from the sync fields.
    70136                 *
    71137                 * This function is called at the widget-updated and widget-synced events.
    wp.textWidgets = ( function( $ ) { 
    108174                         * @returns {void}
    109175                         */
    110176                        function buildEditor() {
    111                                 var editor, triggerChangeIfDirty, onInit;
     177                                var editor, triggerChangeIfDirty, onInit, showPointerElement;
    112178
    113179                                // Abort building if the textarea is gone, likely due to the widget having been deleted entirely.
    114180                                if ( ! document.getElementById( id ) ) {
    wp.textWidgets = ( function( $ ) { 
    137203                                        quicktags: true
    138204                                });
    139205
     206                                /**
     207                                 * Show a pointer, focus on dismiss, and speak the contents for a11y.
     208                                 *
     209                                 * @param {jQuery} pointerElement Pointer element.
     210                                 * @returns {void}
     211                                 */
     212                                showPointerElement = function( pointerElement ) {
     213                                        pointerElement.show();
     214                                        pointerElement.find( '.close' ).focus();
     215                                        wp.a11y.speak( pointerElement.find( 'h3, p' ).map( function() {
     216                                                return $( this ).text();
     217                                        } ).get().join( '\n\n' ) );
     218                                };
     219
    140220                                editor = window.tinymce.get( id );
    141221                                if ( ! editor ) {
    142222                                        throw new Error( 'Failed to initialize editor' );
    wp.textWidgets = ( function( $ ) { 
    152232                                        if ( restoreTextMode ) {
    153233                                                switchEditors.go( id, 'toggle' );
    154234                                        }
     235
     236                                        // Show the pointer.
     237                                        $( '#' + id + '-html' ).on( 'click', function() {
     238                                                control.pasteHtmlPointer.hide(); // Hide the HTML pasting pointer.
     239
     240                                                if ( -1 !== component.dismissedPointers.indexOf( 'text_widget_custom_html' ) ) {
     241                                                        return;
     242                                                }
     243                                                showPointerElement( control.customHtmlWidgetPointer );
     244                                        });
     245
     246                                        // Hide the pointer when switching tabs.
     247                                        $( '#' + id + '-tmce' ).on( 'click', function() {
     248                                                control.customHtmlWidgetPointer.hide();
     249                                        });
     250
     251                                        // Show pointer when pasting HTML.
     252                                        editor.on( 'pastepreprocess', function( event ) {
     253                                                var content = event.content;
     254                                                if ( -1 !== component.dismissedPointers.indexOf( 'text_widget_paste_html' ) || ! content || ! /<\w+.*?>/.test( content ) ) {
     255                                                        return;
     256                                                }
     257
     258                                                // Show the pointer after a slight delay so the user sees what they pasted.
     259                                                _.delay( function() {
     260                                                        showPointerElement( control.pasteHtmlPointer );
     261                                                }, 250 );
     262                                        });
    155263                                };
    156264
    157265                                if ( editor.initialized ) {
    wp.textWidgets = ( function( $ ) { 
    233341                        return;
    234342                }
    235343
     344                // Bypass using TinyMCE when widget is in legacy mode.
     345                if ( widgetForm.find( '.legacy' ).length > 0 ) {
     346                        return;
     347                }
     348
    236349                /*
    237350                 * Create a container element for the widget control fields.
    238351                 * This is inserted into the DOM immediately before the the .widget-content
  • 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 faa79674f6..ad4667bb0f 100644
    class WP_Widget_Text extends WP_Widget { 
    6464        }
    6565
    6666        /**
     67         * Determines whether a given instance is legacy and should bypass using TinyMCE.
     68         *
     69         * @since 4.8.1
     70         *
     71         * @param array $instance {
     72         *     Instance data.
     73         *
     74         *     @type string      $text   Content.
     75         *     @type bool|string $filter Whether autop or content filters should apply.
     76         *     @type bool        $legacy Whether widget is in legacy mode.
     77         * }
     78         * @return bool Whether Text widget instance contains legacy data.
     79         */
     80        public function is_legacy_instance( $instance ) {
     81
     82                // If the widget has been updated while in legacy mode, it stays in legacy mode.
     83                if ( ! empty( $instance['legacy'] ) ) {
     84                        return true;
     85                }
     86
     87                // If the widget has been added/updated in 4.8 then filter prop is 'content' and it is no longer legacy.
     88                if ( isset( $instance['filter'] ) && 'content' === $instance['filter'] ) {
     89                        return false;
     90                }
     91
     92                // If the text is empty, then nothing is preventing migration to TinyMCE.
     93                if ( empty( $instance['text'] ) ) {
     94                        return false;
     95                }
     96
     97                $wpautop = ! empty( $instance['filter'] );
     98                $has_line_breaks = ( false !== strpos( $instance['text'], "\n" ) );
     99
     100                // If auto-paragraphs are not enabled and there are line breaks, then ensure legacy mode.
     101                if ( ! $wpautop && $has_line_breaks ) {
     102                        return true;
     103                }
     104
     105                // If an HTML comment is present, assume legacy mode.
     106                if ( false !== strpos( $instance['text'], '<!--' ) ) {
     107                        return true;
     108                }
     109
     110                /*
     111                 * If a shortcode is present (with support added by a plugin), assume legacy mode
     112                 * since shortcodes would apply at the widget_text filter and thus be applied
     113                 * before wpautop runs at the widget_text_content filter.
     114                 */
     115                if ( preg_match( '/' . get_shortcode_regex() . '/', $instance['text'] ) ) {
     116                        return true;
     117                }
     118
     119                // In the rare case that DOMDocument is not available we cannot reliably sniff content and so we assume legacy.
     120                if ( ! class_exists( 'DOMDocument' ) ) {
     121                        // @codeCoverageIgnoreStart
     122                        return true;
     123                        // @codeCoverageIgnoreEnd
     124                }
     125
     126                $doc = new DOMDocument();
     127                $doc->loadHTML( sprintf(
     128                        '<html><head><meta charset="%s"></head><body>%s</body></html>',
     129                        esc_attr( get_bloginfo( 'charset' ) ),
     130                        $instance['text']
     131                ) );
     132                $body = $doc->getElementsByTagName( 'body' )->item( 0 );
     133
     134                // See $allowedposttags.
     135                $safe_elements_attributes = array(
     136                        'strong' => array(),
     137                        'em' => array(),
     138                        'b' => array(),
     139                        'i' => array(),
     140                        'u' => array(),
     141                        's' => array(),
     142                        'ul' => array(),
     143                        'ol' => array(),
     144                        'li' => array(),
     145                        'hr' => array(),
     146                        'abbr' => array(),
     147                        'acronym' => array(),
     148                        'code' => array(),
     149                        'dfn' => array(),
     150                        'a' => array(
     151                                'href' => true,
     152                        ),
     153                        'img' => array(
     154                                'src' => true,
     155                                'alt' => true,
     156                        ),
     157                );
     158                $safe_empty_elements = array( 'img', 'hr', 'iframe' );
     159
     160                foreach ( $body->getElementsByTagName( '*' ) as $element ) {
     161                        /** @var DOMElement $element */
     162                        $tag_name = strtolower( $element->nodeName );
     163
     164                        // If the element is not safe, then the instance is legacy.
     165                        if ( ! isset( $safe_elements_attributes[ $tag_name ] ) ) {
     166                                return true;
     167                        }
     168
     169                        // If the element is not safely empty and it has empty contents, then legacy mode.
     170                        if ( ! in_array( $tag_name, $safe_empty_elements, true ) && '' === trim( $element->textContent ) ) {
     171                                return true;
     172                        }
     173
     174                        // If an attribute is not recognized as safe, then the instance is legacy.
     175                        foreach ( $element->attributes as $attribute ) {
     176                                /** @var DOMAttr $attribute */
     177                                $attribute_name = strtolower( $attribute->nodeName );
     178
     179                                if ( ! isset( $safe_elements_attributes[ $tag_name ][ $attribute_name ] ) ) {
     180                                        return true;
     181                                }
     182                        }
     183                }
     184
     185                // Otherwise, the text contains no elements/attributes that TinyMCE could drop, and therefore the widget does not need legacy mode.
     186                return false;
     187        }
     188
     189        /**
    67190         * Outputs the content for the current Text widget instance.
    68191         *
    69192         * @since 2.8.0
    class WP_Widget_Text extends WP_Widget { 
    79202                $title = apply_filters( 'widget_title', empty( $instance['title'] ) ? '' : $instance['title'], $instance, $this->id_base );
    80203
    81204                $text = ! empty( $instance['text'] ) ? $instance['text'] : '';
     205                $is_visual_text_widget = ( isset( $instance['filter'] ) && 'content' === $instance['filter'] );
     206
     207                /*
     208                 * Just-in-time temporarily upgrade Visual Text widget shortcode handling
     209                 * (with support added by plugin) from the widget_text filter to
     210                 * widget_text_content:11 to prevent wpautop from corrupting HTML output
     211                 * added by the shortcode.
     212                 */
     213                $widget_text_do_shortcode_priority = has_filter( 'widget_text', 'do_shortcode' );
     214                $should_upgrade_shortcode_handling = ( $is_visual_text_widget && false !== $widget_text_do_shortcode_priority );
     215                if ( $should_upgrade_shortcode_handling ) {
     216                        remove_filter( 'widget_text', 'do_shortcode', $widget_text_do_shortcode_priority );
     217                        add_filter( 'widget_text_content', 'do_shortcode', 11 );
     218                }
    82219
    83220                /**
    84221                 * Filters the content of the Text widget.
    class WP_Widget_Text extends WP_Widget { 
    113250                        }
    114251                }
    115252
     253                // Undo temporary upgrade of the plugin-supplied shortcode handling.
     254                if ( $should_upgrade_shortcode_handling ) {
     255                        remove_filter( 'widget_text_content', 'do_shortcode', 11 );
     256                        add_filter( 'widget_text', 'do_shortcode', $widget_text_do_shortcode_priority );
     257                }
     258
    116259                echo $args['before_widget'];
    117260                if ( ! empty( $title ) ) {
    118261                        echo $args['before_title'] . $title . $args['after_title'];
    class WP_Widget_Text extends WP_Widget { 
    145288                }
    146289
    147290                /*
    148                  * Re-use legacy 'filter' (wpautop) property to now indicate content filters will always apply.
     291                 * If the Text widget is in legacy mode, then a hidden input will indicate this
     292                 * and the new content value for the filter prop will by bypassed. Otherwise,
     293                 * re-use legacy 'filter' (wpautop) property to now indicate content filters will always apply.
    149294                 * Prior to 4.8, this is a boolean value used to indicate whether or not wpautop should be
    150295                 * applied. By re-using this property, downgrading WordPress from 4.8 to 4.7 will ensure
    151296                 * that the content for Text widgets created with TinyMCE will continue to get wpautop.
    152297                 */
    153                 $instance['filter'] = 'content';
     298                if ( isset( $new_instance['legacy'] ) || isset( $old_instance['legacy'] ) || ( isset( $new_instance['filter'] ) && 'content' !== $new_instance['filter'] ) ) {
     299                        $instance['filter'] = ! empty( $new_instance['filter'] );
     300                        $instance['legacy'] = true;
     301                } else {
     302                        $instance['filter'] = 'content';
     303                        unset( $instance['legacy'] );
     304                }
    154305
    155306                return $instance;
    156307        }
    class WP_Widget_Text extends WP_Widget { 
    171322         *
    172323         * @since 2.8.0
    173324         * @since 4.8.0 Form only contains hidden inputs which are synced with JS template.
     325         * @since 4.8.1 Restored original form to be displayed when in legacy mode.
    174326         * @access public
    175327         * @see WP_Widget_Visual_Text::render_control_template_scripts()
    176328         *
    class WP_Widget_Text extends WP_Widget { 
    186338                        )
    187339                );
    188340                ?>
    189                 <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'] ); ?>">
    190                 <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'] ); ?>">
     341                <?php if ( ! $this->is_legacy_instance( $instance ) ) : ?>
     342                        <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'] ); ?>">
     343                        <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'] ); ?>">
     344                <?php else : ?>
     345                        <input name="<?php echo $this->get_field_name( 'legacy' ); ?>" type="hidden" class="legacy" value="true">
     346                        <p>
     347                                <label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'Title:' ); ?></label>
     348                                <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'] ); ?>"/>
     349                        </p>
     350                        <div class="notice inline notice-info notice-alt">
     351                                <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>
     352                        </div>
     353                        <p>
     354                                <label for="<?php echo $this->get_field_id( 'text' ); ?>"><?php _e( 'Content:' ); ?></label>
     355                                <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>
     356                        </p>
     357                        <p>
     358                                <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>
     359                        </p>
    191360                <?php
     361                endif;
    192362        }
    193363
    194364        /**
    class WP_Widget_Text extends WP_Widget { 
    198368         * @access public
    199369         */
    200370        public function render_control_template_scripts() {
     371                $dismissed_pointers = explode( ',', (string) get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true ) );
    201372                ?>
    202373                <script type="text/html" id="tmpl-widget-text-control-fields">
    203374                        <# var elementIdPrefix = 'el' + String( Math.random() ).replace( /\D/g, '' ) + '_' #>
    class WP_Widget_Text extends WP_Widget { 
    205376                                <label for="{{ elementIdPrefix }}title"><?php esc_html_e( 'Title:' ); ?></label>
    206377                                <input id="{{ elementIdPrefix }}title" type="text" class="widefat title">
    207378                        </p>
     379
     380                        <?php if ( ! in_array( 'text_widget_custom_html', $dismissed_pointers, true ) ) : ?>
     381                                <div hidden class="wp-pointer custom-html-widget-pointer wp-pointer-top">
     382                                        <div class="wp-pointer-content">
     383                                                <h3><?php _e( 'New Custom HTML Widget' ); ?></h3>
     384                                                <?php if ( is_customize_preview() ) : ?>
     385                                                        <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>
     386                                                <?php else : ?>
     387                                                        <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>
     388                                                <?php endif; ?>
     389                                                <div class="wp-pointer-buttons">
     390                                                        <a class="close" href="#"><?php _e( 'Dismiss' ); ?></a>
     391                                                </div>
     392                                        </div>
     393                                        <div class="wp-pointer-arrow">
     394                                                <div class="wp-pointer-arrow-inner"></div>
     395                                        </div>
     396                                </div>
     397                        <?php endif; ?>
     398
     399                        <?php if ( ! in_array( 'text_widget_paste_html', $dismissed_pointers, true ) ) : ?>
     400                                <div hidden class="wp-pointer paste-html-pointer wp-pointer-top">
     401                                        <div class="wp-pointer-content">
     402                                                <h3><?php _e( 'Did you just paste HTML?' ); ?></h3>
     403                                                <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>
     404                                                <div class="wp-pointer-buttons">
     405                                                        <a class="close" href="#"><?php _e( 'Dismiss' ); ?></a>
     406                                                </div>
     407                                        </div>
     408                                        <div class="wp-pointer-arrow">
     409                                                <div class="wp-pointer-arrow-inner"></div>
     410                                        </div>
     411                                </div>
     412                        <?php endif; ?>
     413
    208414                        <p>
    209415                                <label for="{{ elementIdPrefix }}text" class="screen-reader-text"><?php esc_html_e( 'Content:' ); ?></label>
    210416                                <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.