Make WordPress Core


Ignore:
Timestamp:
07/14/2017 05:27:33 PM (7 years 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:
2 edited

Legend:

Unmodified
Added
Removed
  • branches/4.8

  • 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>
Note: See TracChangeset for help on using the changeset viewer.