Make WordPress Core

Changeset 41361


Ignore:
Timestamp:
09/10/2017 06:32:34 AM (6 years ago)
Author:
westonruter
Message:

Widgets: Add shortcode support inside Text widgets.

  • Used now in core to facilitate displaying inserted media. See #40854.
  • The [embed] shortcode is not supported because there is no post context for caching oEmbed responses. This depends on #34115.
  • Add do_shortcode() to the widget_text_content filter in the same way it is added for the_content at priority 11, with shortcode_unautop() called at priority 10 after wpautop().
  • For Text widget in legacy mode, manually apply do_shortcode() (and shortcode_unautop() if auto-paragraph checked) if the core-added widget_text_content filter remains, unless a plugin added do_shortcode() to widget_text to prevent applying shortcodes twice.
  • Ensure that global $post is null while filters apply in the Text widget so shortcode handlers won't run with unexpected contexts.

Props westonruter, nacin, aaroncampbell.
See #40854, #34115.
Fixes #10457.

Location:
trunk
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/default-filters.php

    r41268 r41361  
    170170add_filter( 'widget_text_content', 'convert_smilies',  20 );
    171171add_filter( 'widget_text_content', 'wpautop'              );
     172add_filter( 'widget_text_content', 'shortcode_unautop'    );
     173add_filter( 'widget_text_content', 'do_shortcode',     11 ); // Runs after wpautop(); note that $post global will be null when shortcodes run.
    172174
    173175add_filter( 'date_i18n', 'wp_maybe_decline_date' );
  • trunk/src/wp-includes/widgets/class-wp-widget-text.php

    r41260 r41361  
    184184     * @since 2.8.0
    185185     *
     186     * @global WP_Post $post
     187     *
    186188     * @param array $args     Display arguments including 'before_title', 'after_title',
    187189     *                        'before_widget', and 'after_widget'.
     
    189191     */
    190192    public function widget( $args, $instance ) {
     193        global $post;
    191194
    192195        /** This filter is documented in wp-includes/widgets/class-wp-widget-pages.php */
     
    206209
    207210        /*
    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.
     211         * Suspend legacy plugin-supplied do_shortcode() for 'widget_text' filter for the visual Text widget to prevent
     212         * shortcodes being processed twice. Now do_shortcode() is added to the 'widget_text_content' filter in core itself
     213         * and it applies after wpautop() to prevent corrupting HTML output added by the shortcode. When do_shortcode() is
     214         * added to 'widget_text_content' then do_shortcode() will be manually called when in legacy mode as well.
    212215         */
    213216        $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 ) {
     217        $should_suspend_legacy_shortcode_support = ( $is_visual_text_widget && false !== $widget_text_do_shortcode_priority );
     218        if ( $should_suspend_legacy_shortcode_support ) {
    216219            remove_filter( 'widget_text', 'do_shortcode', $widget_text_do_shortcode_priority );
    217             add_filter( 'widget_text_content', 'do_shortcode', 11 );
     220        }
     221
     222        // Nullify the $post global during widget rendering to prevent shortcodes from running with the unexpected context.
     223        $suspended_post = null;
     224        if ( isset( $post ) ) {
     225            $suspended_post = $post;
     226            $post = null;
    218227        }
    219228
     
    245254             */
    246255            $text = apply_filters( 'widget_text_content', $text, $instance, $this );
    247 
    248         } elseif ( ! empty( $instance['filter'] ) ) {
    249             $text = wpautop( $text ); // Back-compat for instances prior to 4.8.
    250         }
    251 
    252         // Undo temporary upgrade of the plugin-supplied shortcode handling.
    253         if ( $should_upgrade_shortcode_handling ) {
    254             remove_filter( 'widget_text_content', 'do_shortcode', 11 );
     256        } else {
     257            // Now in legacy mode, add paragraphs and line breaks when checkbox is checked.
     258            if ( ! empty( $instance['filter'] ) ) {
     259                $text = wpautop( $text );
     260            }
     261
     262            /*
     263             * Manually do shortcodes on the content when the core-added filter is present. It is added by default
     264             * in core by adding do_shortcode() to the 'widget_text_content' filter to apply after wpautop().
     265             * Since the legacy Text widget runs wpautop() after 'widget_text' filters are applied, the widget in
     266             * legacy mode here manually applies do_shortcode() on the content unless the default
     267             * core filter for 'widget_text_content' has been removed, or if do_shortcode() has already
     268             * been applied via a plugin adding do_shortcode() to 'widget_text' filters.
     269             */
     270            if ( has_filter( 'widget_text_content', 'do_shortcode' ) && ! $widget_text_do_shortcode_priority ) {
     271                if ( ! empty( $instance['filter'] ) ) {
     272                    $text = shortcode_unautop( $text );
     273                }
     274                $text = do_shortcode( $text );
     275            }
     276        }
     277
     278        // Restore post global.
     279        if ( isset( $suspended_post ) ) {
     280            $post = $suspended_post;
     281        }
     282
     283        // Undo suspension of legacy plugin-supplied shortcode handling.
     284        if ( $should_suspend_legacy_shortcode_support ) {
    255285            add_filter( 'widget_text', 'do_shortcode', $widget_text_do_shortcode_priority );
    256286        }
  • trunk/tests/phpunit/tests/widgets/text-widget.php

    r41260 r41361  
    223223     * @var string
    224224     */
    225     protected $example_shortcode_content = "<p>One\nTwo\n\nThree</p>\n<script>\ndocument.write('Test1');\n\ndocument.write('Test2');\n</script>";
     225    protected $example_shortcode_content = "<p class='sortcodep'>One\nTwo\n\nThree\n\nThis is testing the <code>[example note='This will not get processed since it is part of shortcode output itself.']</code> shortcode.</p>\n<script>\ndocument.write('Test1');\n\ndocument.write('Test2');\n</script>";
     226
     227    /**
     228     * The captured global post during shortcode rendering.
     229     *
     230     * @var WP_Post|null
     231     */
     232    protected $post_during_shortcode = null;
     233
     234    /**
     235     * Number of times the shortcode was rendered.
     236     *
     237     * @var int
     238     */
     239    protected $shortcode_render_count = 0;
    226240
    227241    /**
     
    231245     */
    232246    function do_example_shortcode() {
     247        $this->post_during_shortcode = get_post();
     248        $this->shortcode_render_count++;
    233249        return $this->example_shortcode_content;
    234250    }
    235251
    236252    /**
    237      * Test widget method when a plugin has added shortcode support.
     253     * Test widget method with shortcodes.
    238254     *
    239255     * @covers WP_Widget_Text::widget
    240256     */
    241257    function test_widget_shortcodes() {
     258        global $post;
     259        $post_id = $this->factory()->post->create();
     260        $post = get_post( $post_id );
     261
    242262        $args = array(
    243263            'before_title'  => '<h2>',
     
    247267        );
    248268        $widget = new WP_Widget_Text();
    249         add_filter( 'widget_text', 'do_shortcode' );
    250269        add_shortcode( 'example', array( $this, 'do_example_shortcode' ) );
    251270
    252271        $base_instance = array(
    253272            'title' => 'Example',
    254             'text' => "This is an example:\n\n[example]",
    255             'filter' => false,
    256         );
    257 
    258         // Legacy Text Widget.
    259         $instance = array_merge( $base_instance, array(
    260             'filter' => false,
    261         ) );
    262         ob_start();
    263         $widget->widget( $args, $instance );
    264         $output = ob_get_clean();
     273            'text' => "This is an example:\n\n[example]\n\nHello.",
     274            'filter' => false,
     275        );
     276
     277        // Legacy Text Widget without wpautop.
     278        $instance = array_merge( $base_instance, array(
     279            'filter' => false,
     280        ) );
     281        $this->shortcode_render_count = 0;
     282        ob_start();
     283        $widget->widget( $args, $instance );
     284        $output = ob_get_clean();
     285        $this->assertEquals( 1, $this->shortcode_render_count );
     286        $this->assertNotContains( '[example]', $output, 'Expected shortcode to be processed in legacy widget with plugin adding filter' );
    265287        $this->assertContains( $this->example_shortcode_content, $output, 'Shortcode was applied without wpautop corrupting it.' );
    266         $this->assertEquals( 10, has_filter( 'widget_text', 'do_shortcode' ), 'Filter was restored.' );
    267 
    268         // Visual Text Widget.
    269         $instance = array_merge( $base_instance, array(
    270             'filter' => 'content',
    271         ) );
    272         ob_start();
    273         $widget->widget( $args, $instance );
    274         $output = ob_get_clean();
     288        $this->assertNotContains( '<p>' . $this->example_shortcode_content . '</p>', $output, 'Expected shortcode_unautop() to have run.' );
     289        $this->assertNull( $this->post_during_shortcode );
     290
     291        // Legacy Text Widget with wpautop.
     292        $instance = array_merge( $base_instance, array(
     293            'filter' => true,
     294            'visual' => false,
     295        ) );
     296        $this->shortcode_render_count = 0;
     297        ob_start();
     298        $widget->widget( $args, $instance );
     299        $output = ob_get_clean();
     300        $this->assertEquals( 1, $this->shortcode_render_count );
     301        $this->assertNotContains( '[example]', $output, 'Expected shortcode to be processed in legacy widget with plugin adding filter' );
    275302        $this->assertContains( $this->example_shortcode_content, $output, 'Shortcode was applied without wpautop corrupting it.' );
    276         $this->assertEquals( 10, has_filter( 'widget_text', 'do_shortcode' ), 'Filter was restored.' );
    277         $this->assertFalse( has_filter( 'widget_text_content', 'do_shortcode' ), 'Filter was removed.' );
    278 
    279         // Visual Text Widget with properly-used widget_text_content filter.
     303        $this->assertNotContains( '<p>' . $this->example_shortcode_content . '</p>', $output, 'Expected shortcode_unautop() to have run.' );
     304        $this->assertNull( $this->post_during_shortcode );
     305
     306        // Legacy text widget with plugin adding shortcode support as well.
     307        add_filter( 'widget_text', 'do_shortcode' );
     308        $this->shortcode_render_count = 0;
     309        ob_start();
     310        $widget->widget( $args, $instance );
     311        $output = ob_get_clean();
     312        $this->assertEquals( 1, $this->shortcode_render_count );
     313        $this->assertNotContains( '[example]', $output, 'Expected shortcode to be processed in legacy widget with plugin adding filter' );
     314        $this->assertContains( wpautop( $this->example_shortcode_content ), $output, 'Shortcode was applied *with* wpautop() applying to shortcode output since plugin used legacy filter.' );
     315        $this->assertNull( $this->post_during_shortcode );
    280316        remove_filter( 'widget_text', 'do_shortcode' );
    281         add_filter( 'widget_text_content', 'do_shortcode', 11 );
    282         $instance = array_merge( $base_instance, array(
    283             'filter' => 'content',
    284         ) );
    285         ob_start();
    286         $widget->widget( $args, $instance );
    287         $output = ob_get_clean();
     317
     318        $instance = array_merge( $base_instance, array(
     319            'filter' => true,
     320            'visual' => true,
     321        ) );
     322
     323        // Visual Text Widget with only core-added widget_text_content filter for do_shortcode.
     324        $this->assertFalse( has_filter( 'widget_text', 'do_shortcode' ) );
     325        $this->assertEquals( 11, has_filter( 'widget_text_content', 'do_shortcode' ), 'Expected core to have set do_shortcode as widget_text_content filter.' );
     326        $this->shortcode_render_count = 0;
     327        ob_start();
     328        $widget->widget( $args, $instance );
     329        $output = ob_get_clean();
     330        $this->assertEquals( 1, $this->shortcode_render_count );
    288331        $this->assertContains( $this->example_shortcode_content, $output, 'Shortcode was applied without wpautop corrupting it.' );
    289         $this->assertFalse( has_filter( 'widget_text', 'do_shortcode' ), 'Filter was not erroneously restored.' );
     332        $this->assertNotContains( '<p>' . $this->example_shortcode_content . '</p>', $output, 'Expected shortcode_unautop() to have run.' );
     333        $this->assertFalse( has_filter( 'widget_text', 'do_shortcode' ), 'The widget_text filter still lacks do_shortcode handler.' );
     334        $this->assertEquals( 11, has_filter( 'widget_text_content', 'do_shortcode' ), 'The widget_text_content filter still has do_shortcode handler.' );
     335        $this->assertNull( $this->post_during_shortcode );
     336
     337        // Visual Text Widget with both filters applied added, one from core and another via plugin.
     338        add_filter( 'widget_text', 'do_shortcode' );
     339        $this->shortcode_render_count = 0;
     340        ob_start();
     341        $widget->widget( $args, $instance );
     342        $output = ob_get_clean();
     343        $this->assertEquals( 1, $this->shortcode_render_count );
     344        $this->assertContains( $this->example_shortcode_content, $output, 'Shortcode was applied without wpautop corrupting it.' );
     345        $this->assertNotContains( '<p>' . $this->example_shortcode_content . '</p>', $output, 'Expected shortcode_unautop() to have run.' );
     346        $this->assertEquals( 10, has_filter( 'widget_text', 'do_shortcode' ), 'Expected do_shortcode to be restored to widget_text.' );
     347        $this->assertNull( $this->post_during_shortcode );
     348        $this->assertNull( $this->post_during_shortcode );
     349        remove_filter( 'widget_text', 'do_shortcode' );
     350
     351        // Visual Text Widget with shortcode handling disabled via plugin removing filter.
     352        remove_filter( 'widget_text_content', 'do_shortcode', 11 );
     353        remove_filter( 'widget_text', 'do_shortcode' );
     354        $this->shortcode_render_count = 0;
     355        ob_start();
     356        $widget->widget( $args, $instance );
     357        $output = ob_get_clean();
     358        $this->assertEquals( 0, $this->shortcode_render_count );
     359        $this->assertContains( '[example]', $output );
     360        $this->assertNotContains( $this->example_shortcode_content, $output );
     361        $this->assertFalse( has_filter( 'widget_text', 'do_shortcode' ) );
     362        $this->assertFalse( has_filter( 'widget_text_content', 'do_shortcode' ) );
    290363    }
    291364
Note: See TracChangeset for help on using the changeset viewer.