WordPress.org

Make WordPress Core

Ticket #40951: 40951.3.diff

File 40951.3.diff, 21.2 KB (added by westonruter, 3 years ago)

Refresh patch

  • src/wp-admin/js/widgets/text-widgets.js

    diff --git src/wp-admin/js/widgets/text-widgets.js src/wp-admin/js/widgets/text-widgets.js
    index 6f057044cf..3796c3b29d 100644
     
    33wp.textWidgets = ( function( $ ) {
    44        'use strict';
    55
    6         var component = {};
     6        var component = {
     7                data: {
     8                        custom_html_pointer_dismissed: false,
     9                        l10n: {
     10                                pointer_heading: '',
     11                                pointer_text: ''
     12                        }
     13                }
     14        };
    715
    816        /**
    917         * Text widget control.
    wp.textWidgets = ( function( $ ) { 
    143151                                        if ( restoreTextMode ) {
    144152                                                switchEditors.go( id, 'toggle' );
    145153                                        }
     154
     155                                        $( '#' + id + '-html' ).one( 'click', function() {
     156                                                var tabContainer;
     157                                                if ( component.data.custom_html_pointer_dismissed ) {
     158                                                        return;
     159                                                }
     160                                                tabContainer = $( this ).parent();
     161                                                tabContainer.pointer({
     162                                                        position: 'bottom',
     163                                                        align: 'right',
     164                                                        edge: 'right',
     165                                                        content: '<h3>' + component.data.l10n.pointer_heading + '</h3><p>' + component.data.l10n.pointer_text + '</p>',
     166                                                        close: function() {
     167                                                                wp.ajax.post( 'dismiss-wp-pointer', {
     168                                                                        pointer: 'text_widget_custom_html'
     169                                                                });
     170                                                                component.data.custom_html_pointer_dismissed = true;
     171                                                        }
     172                                                });
     173                                                tabContainer.pointer( 'open' );
     174                                                tabContainer.pointer( 'widget' ).css( 'z-index', 999999 ); // Default z-index of 9999 is not enough in Customizer.
     175                                        });
    146176                                };
    147177
    148178                                if ( editor.initialized ) {
    wp.textWidgets = ( function( $ ) { 
    224254                        return;
    225255                }
    226256
     257                // Bypass using TinyMCE when widget is in legacy mode.
     258                if ( widgetForm.find( '.legacy' ).length > 0 ) {
     259                        return;
     260                }
     261
    227262                /*
    228263                 * Create a container element for the widget control fields.
    229264                 * This is inserted into the DOM immediately before the the .widget-content
    wp.textWidgets = ( function( $ ) { 
    328363         * When WordPress enqueues this script, it should have an inline script
    329364         * attached which calls wp.textWidgets.init().
    330365         *
     366         * @param {object} exports      Server exports.
     367         * @param {object} exports.l10n Translations.
    331368         * @returns {void}
    332369         */
    333         component.init = function init() {
     370        component.init = function init( exports ) {
    334371                var $document = $( document );
     372                component.data = exports;
    335373                $document.on( 'widget-added', component.handleWidgetAdded );
    336374                $document.on( 'widget-synced widget-updated', component.handleWidgetUpdated );
    337375
  • src/wp-includes/script-loader.php

    diff --git src/wp-includes/script-loader.php src/wp-includes/script-loader.php
    index ba52778d2f..d48df598b9 100644
    function wp_default_scripts( &$scripts ) { 
    608608                $scripts->add( 'media-audio-widget', "/wp-admin/js/widgets/media-audio-widget$suffix.js", array( 'media-widgets', 'media-audiovideo' ) );
    609609                $scripts->add( 'media-image-widget', "/wp-admin/js/widgets/media-image-widget$suffix.js", array( 'media-widgets' ) );
    610610                $scripts->add( 'media-video-widget', "/wp-admin/js/widgets/media-video-widget$suffix.js", array( 'media-widgets', 'media-audiovideo' ) );
    611                 $scripts->add( 'text-widgets', "/wp-admin/js/widgets/text-widgets$suffix.js", array( 'jquery', 'backbone', 'editor', 'wp-util' ) );
    612                 $scripts->add_inline_script( 'text-widgets', 'wp.textWidgets.init();', 'after' );
     611                $scripts->add( 'text-widgets', "/wp-admin/js/widgets/text-widgets$suffix.js", array( 'jquery', 'backbone', 'editor', 'wp-util', 'wp-pointer' ) );
     612                $exports = array(
     613                        'custom_html_pointer_dismissed' => is_user_logged_in() && in_array( 'text_widget_custom_html', explode( ',', (string) get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true ) ) ),
     614                        'l10n' => array(
     615                                'pointer_heading' => __( 'New Custom HTML Widget' ),
     616                                'pointer_text' => __( 'Hey, did you hear we have a "Custom HTML" widget now? Check it out to add some custom code to your site!' ),
     617                        ),
     618                );
     619                $scripts->add_inline_script( 'text-widgets', sprintf( 'wp.textWidgets.init( %s );', wp_json_encode( $exports ) ), 'after' );
    613620
    614621                $scripts->add( 'theme', "/wp-admin/js/theme$suffix.js", array( 'wp-backbone', 'wp-a11y' ), false, 1 );
    615622
  • src/wp-includes/widgets/class-wp-widget-text.php

    diff --git src/wp-includes/widgets/class-wp-widget-text.php src/wp-includes/widgets/class-wp-widget-text.php
    index 7d149c0a0f..c9f5badee2 100644
    class WP_Widget_Text extends WP_Widget { 
    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
     88                // If auto-paragraphs are not enabled and there are line breaks, then ensure legacy mode.
     89                if ( ! $wpautop && false === strpos( "\n", $instance['text'] ) ) {
     90                        return true;
     91                }
     92
     93                // In the rare case that DOMDocument is not available we cannot reliably sniff content and so we assume legacy.
     94                if ( ! class_exists( 'DOMDocument' ) ) {
     95                        // @codeCoverageIgnoreStart
     96                        return true;
     97                        // @codeCoverageIgnoreEnd
     98                }
     99
     100                $doc = new DOMDocument();
     101                $doc->loadHTML( sprintf(
     102                        '<html><head><meta charset="%s"></head><body>%s</body></html>',
     103                        esc_attr( get_bloginfo( 'charset' ) ),
     104                        $instance['text']
     105                ) );
     106                $body = $doc->getElementsByTagName( 'body' )->item( 0 );
     107
     108                // See $allowedposttags.
     109                $safe_elements_attributes = array(
     110                        'strong' => array(),
     111                        'em' => array(),
     112                        'b' => array(),
     113                        'i' => array(),
     114                        'u' => array(),
     115                        's' => array(),
     116                        'ul' => array(),
     117                        'ol' => array(),
     118                        'li' => array(),
     119                        'hr' => array(),
     120                        'abbr' => array(),
     121                        'acronym' => array(),
     122                        'code' => array(),
     123                        'dfn' => array(),
     124                        'a' => array(
     125                                'href' => true,
     126                        ),
     127                        'img' => array(
     128                                'src' => true,
     129                                'alt' => true,
     130                        ),
     131                );
     132                $safe_empty_elements = array( 'img', 'hr', 'iframe' );
     133
     134                foreach ( $body->getElementsByTagName( '*' ) as $element ) {
     135                        /** @var DOMElement $element */
     136                        $tag_name = strtolower( $element->nodeName );
     137
     138                        // If the element is not safe, then the instance is legacy.
     139                        if ( ! isset( $safe_elements_attributes[ $tag_name ] ) ) {
     140                                return true;
     141                        }
     142
     143                        // If the element is not safely empty and it has empty contents, then legacy mode.
     144                        if ( ! in_array( $tag_name, $safe_empty_elements, true ) && '' === trim( $element->textContent ) ) {
     145                                return true;
     146                        }
     147
     148                        // If an attribute is not recognized as safe, then the instance is legacy.
     149                        foreach ( $element->attributes as $attribute ) {
     150                                /** @var DOMAttr $attribute */
     151                                $attribute_name = strtolower( $attribute->nodeName );
     152
     153                                if ( ! isset( $safe_elements_attributes[ $tag_name ][ $attribute_name ] ) ) {
     154                                        return true;
     155                                }
     156                        }
     157                }
     158
     159                // Otherwise, the text contains no elements/attributes that TinyMCE could drop, and therefore the widget does not need legacy mode.
     160                return false;
     161        }
     162
     163        /**
    56164         * Outputs the content for the current Text widget instance.
    57165         *
    58166         * @since 2.8.0
    class WP_Widget_Text extends WP_Widget { 
    134242                }
    135243
    136244                /*
    137                  * Re-use legacy 'filter' (wpautop) property to now indicate content filters will always apply.
     245                 * If the Text widget is in legacy mode, then a hidden input will indicate this
     246                 * and the new content value for the filter prop will by bypassed. Otherwise,
     247                 * re-use legacy 'filter' (wpautop) property to now indicate content filters will always apply.
    138248                 * Prior to 4.8, this is a boolean value used to indicate whether or not wpautop should be
    139249                 * applied. By re-using this property, downgrading WordPress from 4.8 to 4.7 will ensure
    140250                 * that the content for Text widgets created with TinyMCE will continue to get wpautop.
    141251                 */
    142                 $instance['filter'] = 'content';
     252                if ( isset( $new_instance['legacy'] ) || isset( $old_instance['legacy'] ) || ( isset( $new_instance['filter'] ) && 'content' !== $new_instance['filter'] ) ) {
     253                        $instance['filter'] = ! empty( $new_instance['filter'] );
     254                        $instance['legacy'] = true;
     255                } else {
     256                        $instance['filter'] = 'content';
     257                        unset( $instance['legacy'] );
     258                }
    143259
    144260                return $instance;
    145261        }
    class WP_Widget_Text extends WP_Widget { 
    153269        public function enqueue_admin_scripts() {
    154270                wp_enqueue_editor();
    155271                wp_enqueue_script( 'text-widgets' );
     272                wp_enqueue_style( 'wp-pointer' );
    156273        }
    157274
    158275        /**
    class WP_Widget_Text extends WP_Widget { 
    160277         *
    161278         * @since 2.8.0
    162279         * @since 4.8.0 Form only contains hidden inputs which are synced with JS template.
     280         * @since 4.8.1 Restored original form to be displayed when in legacy mode.
    163281         * @access public
    164282         * @see WP_Widget_Visual_Text::render_control_template_scripts()
    165283         *
    class WP_Widget_Text extends WP_Widget { 
    175293                        )
    176294                );
    177295                ?>
    178                 <input id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" class="title" type="hidden" value="<?php echo esc_attr( $instance['title'] ); ?>">
    179                 <input id="<?php echo $this->get_field_id( 'text' ); ?>" name="<?php echo $this->get_field_name( 'text' ); ?>" class="text" type="hidden" value="<?php echo esc_attr( $instance['text'] ); ?>">
    180                 <?php
     296                <?php if ( ! $this->is_legacy_instance( $instance ) ) : ?>
     297                        <input id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" class="title" type="hidden" value="<?php echo esc_attr( $instance['title'] ); ?>">
     298                        <input id="<?php echo $this->get_field_id( 'text' ); ?>" name="<?php echo $this->get_field_name( 'text' ); ?>" class="text" type="hidden" value="<?php echo esc_attr( $instance['text'] ); ?>">
     299                <?php else : ?>
     300                        <input name="<?php echo $this->get_field_name( 'legacy' ); ?>" type="hidden" class="legacy" value="true">
     301                        <p>
     302                                <label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'Title:' ); ?></label>
     303                                <input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $instance['title'] ); ?>"/>
     304                        </p>
     305                        <div class="notice inline notice-info notice-alt">
     306                                <p><?php _e( 'This widget contains code that may work better in the new &#8220;Custom HTML&#8221; widget. How about trying that widget instead?' ); ?></p>
     307                        </div>
     308                        <p>
     309                                <label for="<?php echo $this->get_field_id( 'text' ); ?>"><?php _e( 'Content:' ); ?></label>
     310                                <textarea class="widefat" rows="16" cols="20" id="<?php echo $this->get_field_id( 'text' ); ?>" name="<?php echo $this->get_field_name( 'text' ); ?>"><?php echo esc_textarea( $instance['text'] ); ?></textarea>
     311                        </p>
     312                        <p>
     313                                <input id="<?php echo $this->get_field_id( 'filter' ); ?>" name="<?php echo $this->get_field_name( 'filter' ); ?>" type="checkbox"<?php checked( ! empty( $instance['filter'] ) ); ?> />&nbsp;<label for="<?php echo $this->get_field_id( 'filter' ); ?>"><?php _e( 'Automatically add paragraphs' ); ?></label>
     314                        </p>
     315                <?php endif;
    181316        }
    182317
    183318        /**
  • tests/phpunit/tests/widgets/text-widget.php

    diff --git tests/phpunit/tests/widgets/text-widget.php tests/phpunit/tests/widgets/text-widget.php
    index a28e6a668a..074b42b201 100644
    class Test_WP_Widget_Text extends WP_UnitTestCase { 
    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 { 
    152166        }
    153167
    154168        /**
     169         * Test is_legacy_instance method.
     170         *
     171         * @covers WP_Widget_Text::is_legacy_instance
     172         */
     173        function test_is_legacy_instance() {
     174                $widget = new WP_Widget_Text();
     175                $base_instance = array(
     176                        'title' => 'Title',
     177                        'text' => "Hello\n\nWorld",
     178                );
     179
     180                $instance = array_merge( $base_instance, array(
     181                        'legacy' => true,
     182                ) );
     183                $this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy when legacy prop is present.' );
     184
     185                $instance = array_merge( $base_instance, array(
     186                        'filter' => 'content',
     187                ) );
     188                $this->assertFalse( $widget->is_legacy_instance( $instance ), 'Not legacy when filter is explicitly content.' );
     189
     190                $instance = array_merge( $base_instance, array(
     191                        'text' => '',
     192                        'filter' => true,
     193                ) );
     194                $this->assertFalse( $widget->is_legacy_instance( $instance ), 'Not legacy when text is empty.' );
     195
     196                $instance = array_merge( $base_instance, array(
     197                        'text' => "One\nTwo",
     198                        'filter' => false,
     199                ) );
     200                $this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy when not-wpautop and there are line breaks.' );
     201
     202                $instance = array_merge( $base_instance, array(
     203                        'text' => "One\n\nTwo",
     204                        'filter' => false,
     205                ) );
     206                $this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy when not-wpautop and there are paragraph breaks.' );
     207
     208                $instance = array_merge( $base_instance, array(
     209                        'text' => "One\nTwo",
     210                        'filter' => true,
     211                ) );
     212                $this->assertFalse( $widget->is_legacy_instance( $instance ), 'Not automatically legacy when wpautop and there are line breaks.' );
     213
     214                $instance = array_merge( $base_instance, array(
     215                        'text' => "One\n\nTwo",
     216                        'filter' => true,
     217                ) );
     218                $this->assertFalse( $widget->is_legacy_instance( $instance ), 'Not automatically legacy when wpautop and there are paragraph breaks.' );
     219
     220                // Check text examples that will not migrate to TinyMCE.
     221                $legacy_text_examples = array(
     222                        '<span class="hello"></span>',
     223                        '<span></span>',
     224                        "<ul>\n<li><a href=\"#\" class=\"location\"></a>List Item 1</li>\n<li><a href=\"#\" class=\"location\"></a>List Item 2</li>\n</ul>",
     225                        '<a href="#" class="map"></a>',
     226                        "<script>\n\\Line one\n\n\\Line two</script>",
     227                        "<style>body {\ncolor:red;\n}</style>",
     228                        '<span class="fa fa-cc-discover fa-2x" aria-hidden="true"></span>',
     229                        "<p>\nStay updated with our latest news and specials. We never sell your information and you can unsubscribe at any time.\n</p>\n\n<div class=\"custom-form-class\">\n\t<form action=\"#\" method=\"post\" name=\"mc-embedded-subscribe-form\">\n\n\t\t<label class=\"screen-reader-text\" for=\"mce-EMAIL-b\">Email </label>\n\t\t<input id=\"mce-EMAIL-b\" class=\"required email\" name=\"EMAIL\" required=\"\" type=\"email\" value=\"\" placeholder=\"Email Address*\" />\n\n\t\t<input class=\"button\" name=\"subscribe\" type=\"submit\" value=\"Go!\" />\n\n\t</form>\n</div>",
     230                        '<span class="sectiondown"><a href="#front-page-3"><i class="fa fa-chevron-circle-down"></i></a></span>',
     231                );
     232                foreach ( $legacy_text_examples as $legacy_text_example ) {
     233                        $instance = array_merge( $base_instance, array(
     234                                'text' => $legacy_text_example,
     235                                'filter' => true,
     236                        ) );
     237                        $this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy when wpautop and there is HTML that is not liable to be mutated.' );
     238
     239                        $instance = array_merge( $base_instance, array(
     240                                'text' => $legacy_text_example,
     241                                'filter' => false,
     242                        ) );
     243                        $this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy when not-wpautop and there is HTML that is not liable to be mutated.' );
     244                }
     245
     246                // Check text examples that will migrate to TinyMCE, where elements and attributes are not in whitelist.
     247                $migratable_text_examples = array(
     248                        'Check out <a href="http://example.com">Example</a>',
     249                        '<img src="http://example.com/img.jpg" alt="Img">',
     250                        '<strong><em>Hello</em></strong>',
     251                        '<b><i><u><s>Hello</s></u></i></b>',
     252                        "<ul>\n<li>One</li>\n<li>One</li>\n<li>One</li>\n</ul>",
     253                        "<ol>\n<li>One</li>\n<li>One</li>\n<li>One</li>\n</ol>",
     254                        "Text\n<hr>\nAddendum",
     255                        "Look at this code:\n\n<code>echo 'Hello World!';</code>",
     256                );
     257                foreach ( $migratable_text_examples as $migratable_text_example ) {
     258                        $instance = array_merge( $base_instance, array(
     259                                'text' => $migratable_text_example,
     260                                'filter' => true,
     261                        ) );
     262                        $this->assertFalse( $widget->is_legacy_instance( $instance ), 'Legacy when wpautop and there is HTML that is not liable to be mutated.' );
     263                }
     264        }
     265
     266        /**
     267         * Test update method.
     268         *
     269         * @covers WP_Widget_Text::form
     270         */
     271        function test_form() {
     272                $widget = new WP_Widget_Text();
     273                $instance = array(
     274                        'title' => 'Title',
     275                        'text' => 'Text',
     276                        'filter' => false,
     277                        'legacy' => true,
     278                );
     279                $this->assertTrue( $widget->is_legacy_instance( $instance ) );
     280                ob_start();
     281                $widget->form( $instance );
     282                $form = ob_get_clean();
     283                $this->assertContains( 'class="legacy"', $form );
     284
     285                $instance = array(
     286                        'title' => 'Title',
     287                        'text' => 'Text',
     288                        'filter' => 'content',
     289                );
     290                $this->assertFalse( $widget->is_legacy_instance( $instance ) );
     291                ob_start();
     292                $widget->form( $instance );
     293                $form = ob_get_clean();
     294                $this->assertNotContains( 'class="legacy"', $form );
     295        }
     296
     297        /**
    155298         * Test update method.
    156299         *
    157300         * @covers WP_Widget_Text::update
    class Test_WP_Widget_Text extends WP_UnitTestCase { 
    161304                $instance = array(
    162305                        'title' => "The\nTitle",
    163306                        'text'  => "The\n\nText",
    164                         'filter' => false,
     307                        'filter' => 'content',
    165308                );
    166309
    167310                wp_set_current_user( $this->factory()->user->create( array(
    168311                        'role' => 'administrator',
    169312                ) ) );
    170313
    171                 // Should return valid instance.
     314                // Should return valid instance in legacy mode since filter=false and there are line breaks.
    172315                $expected = array(
    173316                        'title'  => sanitize_text_field( $instance['title'] ),
    174317                        'text'   => $instance['text'],
    175318                        'filter' => 'content',
    176319                );
    177320                $result = $widget->update( $instance, array() );
    178                 $this->assertEquals( $result, $expected );
     321                $this->assertEquals( $expected, $result );
    179322                $this->assertTrue( ! empty( $expected['filter'] ), 'Expected filter prop to be truthy, to handle case where 4.8 is downgraded to 4.7.' );
    180323
    181324                // Make sure KSES is applying as expected.
    class Test_WP_Widget_Text extends WP_UnitTestCase { 
    184327                $instance['text'] = '<script>alert( "Howdy!" );</script>';
    185328                $expected['text'] = $instance['text'];
    186329                $result = $widget->update( $instance, array() );
    187                 $this->assertEquals( $result, $expected );
     330                $this->assertEquals( $expected, $result );
    188331                remove_filter( 'map_meta_cap', array( $this, 'grant_unfiltered_html_cap' ) );
    189332
    190333                add_filter( 'map_meta_cap', array( $this, 'revoke_unfiltered_html_cap' ), 10, 2 );
    class Test_WP_Widget_Text extends WP_UnitTestCase { 
    192335                $instance['text'] = '<script>alert( "Howdy!" );</script>';
    193336                $expected['text'] = wp_kses_post( $instance['text'] );
    194337                $result = $widget->update( $instance, array() );
    195                 $this->assertEquals( $result, $expected );
     338                $this->assertEquals( $expected, $result );
    196339                remove_filter( 'map_meta_cap', array( $this, 'revoke_unfiltered_html_cap' ), 10 );
    197340        }
    198341
    199342        /**
     343         * Test update for legacy widgets.
     344         *
     345         * @covers WP_Widget_Text::update
     346         */
     347        function test_update_legacy() {
     348                $widget = new WP_Widget_Text();
     349
     350                // Updating a widget with explicit filter=true persists with legacy mode.
     351                $instance = array(
     352                        'title' => 'Legacy',
     353                        'text' => 'Text',
     354                        'filter' => true,
     355                );
     356                $result = $widget->update( $instance, array() );
     357                $expected = array_merge( $instance, array(
     358                        'legacy' => true,
     359                        'filter' => true,
     360                ) );
     361                $this->assertEquals( $expected, $result );
     362
     363                // Updating a widget with explicit filter=false persists with legacy mode.
     364                $instance['filter'] = false;
     365                $result = $widget->update( $instance, array() );
     366                $expected = array_merge( $instance, array(
     367                        'legacy' => true,
     368                        'filter' => false,
     369                ) );
     370                $this->assertEquals( $expected, $result );
     371
     372                // Updating a widget in legacy form results in filter=false when checkbox not checked.
     373                $instance['filter'] = true;
     374                $result = $widget->update( $instance, array() );
     375                $expected = array_merge( $instance, array(
     376                        'legacy' => true,
     377                        'filter' => true,
     378                ) );
     379                $this->assertEquals( $expected, $result );
     380
     381                // Updating a widget that previously had legacy form results in filter persisting.
     382                unset( $instance['legacy'] );
     383                $instance['filter'] = true;
     384                $result = $widget->update( $instance, array(
     385                        'legacy' => true,
     386                ) );
     387                $expected = array_merge( $instance, array(
     388                        'legacy' => true,
     389                        'filter' => true,
     390                ) );
     391                $this->assertEquals( $expected, $result );
     392        }
     393
     394        /**
    200395         * Grant unfiltered_html cap via map_meta_cap.
    201396         *
    202397         * @param array  $caps    Returns the user's actual capabilities.