WordPress.org

Make WordPress Core

Changeset 41053


Ignore:
Timestamp:
07/14/17 17:27:33 (4 months ago)
Author:
westonruter
Message:

Widgets: Add legacy mode for Text widget and add usage pointers to default visual mode.

The Text widget in legacy mode omits TinyMCE and retains old behavior for matching pre-existing Text widgets. Usage pointers added to default visual mode appear when attempting to paste HTML code into the Visual tab and when clicking on the Text tab, informing users of the new Custom HTML widget.

Merges [41050] to 4.8 branch.
Props westonruter, melchoyce, gitlost for testing, obenland for testing, dougal for testing, afercia for testing.
See #35243.
Fixes #40951 for 4.8.1.

Location:
branches/4.8
Files:
6 edited

Legend:

Unmodified
Added
Removed
  • branches/4.8

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

    r40653 r41053  
    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-------------------------------------------------------------- */ 
  • branches/4.8/src/wp-admin/js/widgets/text-widgets.js

    r41051 r41053  
    44    'use strict'; 
    55 
    6     var component = {}; 
     6    var component = { 
     7        dismissedPointers: [] 
     8    }; 
    79 
    810    /** 
     
    4547            control.$el.addClass( 'text-widget-fields' ); 
    4648            control.$el.html( wp.template( 'widget-text-control-fields' ) ); 
     49 
     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            } 
    4774 
    4875            control.fields = { 
     
    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         * 
     
    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. 
     
    129195                }); 
    130196 
     197                /** 
     198                 * Show a pointer, focus on dismiss, and speak the contents for a11y. 
     199                 * 
     200                 * @param {jQuery} pointerElement Pointer element. 
     201                 * @returns {void} 
     202                 */ 
     203                showPointerElement = function( pointerElement ) { 
     204                    pointerElement.show(); 
     205                    pointerElement.find( '.close' ).focus(); 
     206                    wp.a11y.speak( pointerElement.find( 'h3, p' ).map( function() { 
     207                        return $( this ).text(); 
     208                    } ).get().join( '\n\n' ) ); 
     209                }; 
     210 
    131211                editor = window.tinymce.get( id ); 
    132212                if ( ! editor ) { 
     
    144224                        switchEditors.go( id, 'toggle' ); 
    145225                    } 
     226 
     227                    // Show the pointer. 
     228                    $( '#' + id + '-html' ).on( 'click', function() { 
     229                        control.pasteHtmlPointer.hide(); // Hide the HTML pasting pointer. 
     230 
     231                        if ( -1 !== component.dismissedPointers.indexOf( 'text_widget_custom_html' ) ) { 
     232                            return; 
     233                        } 
     234                        showPointerElement( control.customHtmlWidgetPointer ); 
     235                    }); 
     236 
     237                    // Hide the pointer when switching tabs. 
     238                    $( '#' + id + '-tmce' ).on( 'click', function() { 
     239                        control.customHtmlWidgetPointer.hide(); 
     240                    }); 
     241 
     242                    // Show pointer when pasting HTML. 
     243                    editor.on( 'pastepreprocess', function( event ) { 
     244                        var content = event.content; 
     245                        if ( -1 !== component.dismissedPointers.indexOf( 'text_widget_paste_html' ) || ! content || ! /<\w+.*?>/.test( content ) ) { 
     246                            return; 
     247                        } 
     248 
     249                        // Show the pointer after a slight delay so the user sees what they pasted. 
     250                        _.delay( function() { 
     251                            showPointerElement( control.pasteHtmlPointer ); 
     252                        }, 250 ); 
     253                    }); 
    146254                }; 
    147255 
     
    225333        } 
    226334 
     335        // Bypass using TinyMCE when widget is in legacy mode. 
     336        if ( widgetForm.find( '.legacy' ).length > 0 ) { 
     337            return; 
     338        } 
     339 
    227340        /* 
    228341         * Create a container element for the widget control fields. 
     
    281394        } 
    282395 
     396        // Bypass using TinyMCE when widget is in legacy mode. 
     397        if ( widgetForm.find( '.legacy' ).length > 0 ) { 
     398            return; 
     399        } 
     400 
    283401        fieldContainer = $( '<div></div>' ); 
    284402        syncContainer = widgetForm.find( '> .widget-inside' ); 
  • branches/4.8/src/wp-includes/script-loader.php

    r40866 r41053  
    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' ) ); 
     611        $scripts->add( 'text-widgets', "/wp-admin/js/widgets/text-widgets$suffix.js", array( 'jquery', 'backbone', 'editor', 'wp-util', 'wp-a11y' ) ); 
    612612        $scripts->add_inline_script( 'text-widgets', 'wp.textWidgets.init();', 'after' ); 
    613613 
     
    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" ); 
  • branches/4.8/src/wp-includes/widgets/class-wp-widget-text.php

    r41052 r41053  
    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     * 
     
    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        /** 
     
    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 ) ) { 
     
    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; 
     
    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() 
     
    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 
     
    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"> 
     
    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> 
  • branches/4.8/tests/phpunit/tests/widgets/text-widget.php

    r40673 r41053  
    3838        $wp_scripts = null; 
    3939        $wp_styles = null; 
     40    } 
     41 
     42    /** 
     43     * Test constructor method. 
     44     * 
     45     * @covers WP_Widget_Text::__construct 
     46     */ 
     47    function test_construct() { 
     48        $widget = new WP_Widget_Text(); 
     49        $this->assertEquals( 'text', $widget->id_base ); 
     50        $this->assertEquals( 'widget_text', $widget->widget_options['classname'] ); 
     51        $this->assertTrue( $widget->widget_options['customize_selective_refresh'] ); 
     52        $this->assertEquals( 400, $widget->control_options['width'] ); 
     53        $this->assertEquals( 350, $widget->control_options['height'] ); 
    4054    } 
    4155 
     
    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     * 
     
    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     * 
     
    162389            'title' => "The\nTitle", 
    163390            'text'  => "The\n\nText", 
    164             'filter' => false, 
     391            'filter' => 'content', 
    165392        ); 
    166393 
     
    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'] ), 
     
    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 
     
    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 
     
    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 ); 
     424    } 
     425 
     426    /** 
     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 ); 
    197476    } 
    198477 
Note: See TracChangeset for help on using the changeset viewer.