From 6327832abecb22c8d4d7e2ec3dbada1a994ac81b Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 10 Sep 2017 06:32:34 +0000 Subject: [PATCH] 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. git-svn-id: https://develop.svn.wordpress.org/trunk@41361 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/default-filters.php | 2 + .../widgets/class-wp-widget-text.php | 54 +++++++--- tests/phpunit/tests/widgets/text-widget.php | 101 +++++++++++++++--- 3 files changed, 131 insertions(+), 26 deletions(-) diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index a196f4ff19..0ed4be9056 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -169,6 +169,8 @@ add_filter( 'widget_text_content', 'capital_P_dangit', 11 ); add_filter( 'widget_text_content', 'wptexturize' ); add_filter( 'widget_text_content', 'convert_smilies', 20 ); add_filter( 'widget_text_content', 'wpautop' ); +add_filter( 'widget_text_content', 'shortcode_unautop' ); +add_filter( 'widget_text_content', 'do_shortcode', 11 ); // Runs after wpautop(); note that $post global will be null when shortcodes run. add_filter( 'date_i18n', 'wp_maybe_decline_date' ); diff --git a/src/wp-includes/widgets/class-wp-widget-text.php b/src/wp-includes/widgets/class-wp-widget-text.php index a79e0daa6c..4c8fa9b088 100644 --- a/src/wp-includes/widgets/class-wp-widget-text.php +++ b/src/wp-includes/widgets/class-wp-widget-text.php @@ -183,11 +183,14 @@ class WP_Widget_Text extends WP_Widget { * * @since 2.8.0 * + * @global WP_Post $post + * * @param array $args Display arguments including 'before_title', 'after_title', * 'before_widget', and 'after_widget'. * @param array $instance Settings for the current Text widget instance. */ public function widget( $args, $instance ) { + global $post; /** This filter is documented in wp-includes/widgets/class-wp-widget-pages.php */ $title = apply_filters( 'widget_title', empty( $instance['title'] ) ? '' : $instance['title'], $instance, $this->id_base ); @@ -205,16 +208,22 @@ class WP_Widget_Text extends WP_Widget { } /* - * Just-in-time temporarily upgrade Visual Text widget shortcode handling - * (with support added by plugin) from the widget_text filter to - * widget_text_content:11 to prevent wpautop from corrupting HTML output - * added by the shortcode. + * Suspend legacy plugin-supplied do_shortcode() for 'widget_text' filter for the visual Text widget to prevent + * shortcodes being processed twice. Now do_shortcode() is added to the 'widget_text_content' filter in core itself + * and it applies after wpautop() to prevent corrupting HTML output added by the shortcode. When do_shortcode() is + * added to 'widget_text_content' then do_shortcode() will be manually called when in legacy mode as well. */ $widget_text_do_shortcode_priority = has_filter( 'widget_text', 'do_shortcode' ); - $should_upgrade_shortcode_handling = ( $is_visual_text_widget && false !== $widget_text_do_shortcode_priority ); - if ( $should_upgrade_shortcode_handling ) { + $should_suspend_legacy_shortcode_support = ( $is_visual_text_widget && false !== $widget_text_do_shortcode_priority ); + if ( $should_suspend_legacy_shortcode_support ) { remove_filter( 'widget_text', 'do_shortcode', $widget_text_do_shortcode_priority ); - add_filter( 'widget_text_content', 'do_shortcode', 11 ); + } + + // Nullify the $post global during widget rendering to prevent shortcodes from running with the unexpected context. + $suspended_post = null; + if ( isset( $post ) ) { + $suspended_post = $post; + $post = null; } /** @@ -244,14 +253,35 @@ class WP_Widget_Text extends WP_Widget { * @param WP_Widget_Text $this Current Text widget instance. */ $text = apply_filters( 'widget_text_content', $text, $instance, $this ); + } else { + // Now in legacy mode, add paragraphs and line breaks when checkbox is checked. + if ( ! empty( $instance['filter'] ) ) { + $text = wpautop( $text ); + } - } elseif ( ! empty( $instance['filter'] ) ) { - $text = wpautop( $text ); // Back-compat for instances prior to 4.8. + /* + * Manually do shortcodes on the content when the core-added filter is present. It is added by default + * in core by adding do_shortcode() to the 'widget_text_content' filter to apply after wpautop(). + * Since the legacy Text widget runs wpautop() after 'widget_text' filters are applied, the widget in + * legacy mode here manually applies do_shortcode() on the content unless the default + * core filter for 'widget_text_content' has been removed, or if do_shortcode() has already + * been applied via a plugin adding do_shortcode() to 'widget_text' filters. + */ + if ( has_filter( 'widget_text_content', 'do_shortcode' ) && ! $widget_text_do_shortcode_priority ) { + if ( ! empty( $instance['filter'] ) ) { + $text = shortcode_unautop( $text ); + } + $text = do_shortcode( $text ); + } } - // Undo temporary upgrade of the plugin-supplied shortcode handling. - if ( $should_upgrade_shortcode_handling ) { - remove_filter( 'widget_text_content', 'do_shortcode', 11 ); + // Restore post global. + if ( isset( $suspended_post ) ) { + $post = $suspended_post; + } + + // Undo suspension of legacy plugin-supplied shortcode handling. + if ( $should_suspend_legacy_shortcode_support ) { add_filter( 'widget_text', 'do_shortcode', $widget_text_do_shortcode_priority ); } diff --git a/tests/phpunit/tests/widgets/text-widget.php b/tests/phpunit/tests/widgets/text-widget.php index f7e810fd04..a3e348f8a6 100644 --- a/tests/phpunit/tests/widgets/text-widget.php +++ b/tests/phpunit/tests/widgets/text-widget.php @@ -222,7 +222,21 @@ class Test_WP_Widget_Text extends WP_UnitTestCase { * * @var string */ - protected $example_shortcode_content = "

One\nTwo\n\nThree

\n"; + protected $example_shortcode_content = "

One\nTwo\n\nThree\n\nThis is testing the [example note='This will not get processed since it is part of shortcode output itself.'] shortcode.

\n"; + + /** + * The captured global post during shortcode rendering. + * + * @var WP_Post|null + */ + protected $post_during_shortcode = null; + + /** + * Number of times the shortcode was rendered. + * + * @var int + */ + protected $shortcode_render_count = 0; /** * Do example shortcode. @@ -230,15 +244,21 @@ class Test_WP_Widget_Text extends WP_UnitTestCase { * @return string Shortcode content. */ function do_example_shortcode() { + $this->post_during_shortcode = get_post(); + $this->shortcode_render_count++; return $this->example_shortcode_content; } /** - * Test widget method when a plugin has added shortcode support. + * Test widget method with shortcodes. * * @covers WP_Widget_Text::widget */ function test_widget_shortcodes() { + global $post; + $post_id = $this->factory()->post->create(); + $post = get_post( $post_id ); + $args = array( 'before_title' => '

', 'after_title' => "

\n", @@ -246,47 +266,100 @@ class Test_WP_Widget_Text extends WP_UnitTestCase { 'after_widget' => "\n", ); $widget = new WP_Widget_Text(); - add_filter( 'widget_text', 'do_shortcode' ); add_shortcode( 'example', array( $this, 'do_example_shortcode' ) ); $base_instance = array( 'title' => 'Example', - 'text' => "This is an example:\n\n[example]", + 'text' => "This is an example:\n\n[example]\n\nHello.", 'filter' => false, ); - // Legacy Text Widget. + // Legacy Text Widget without wpautop. $instance = array_merge( $base_instance, array( 'filter' => false, ) ); + $this->shortcode_render_count = 0; ob_start(); $widget->widget( $args, $instance ); $output = ob_get_clean(); + $this->assertEquals( 1, $this->shortcode_render_count ); + $this->assertNotContains( '[example]', $output, 'Expected shortcode to be processed in legacy widget with plugin adding filter' ); $this->assertContains( $this->example_shortcode_content, $output, 'Shortcode was applied without wpautop corrupting it.' ); - $this->assertEquals( 10, has_filter( 'widget_text', 'do_shortcode' ), 'Filter was restored.' ); + $this->assertNotContains( '

' . $this->example_shortcode_content . '

', $output, 'Expected shortcode_unautop() to have run.' ); + $this->assertNull( $this->post_during_shortcode ); - // Visual Text Widget. + // Legacy Text Widget with wpautop. $instance = array_merge( $base_instance, array( - 'filter' => 'content', + 'filter' => true, + 'visual' => false, ) ); + $this->shortcode_render_count = 0; ob_start(); $widget->widget( $args, $instance ); $output = ob_get_clean(); + $this->assertEquals( 1, $this->shortcode_render_count ); + $this->assertNotContains( '[example]', $output, 'Expected shortcode to be processed in legacy widget with plugin adding filter' ); $this->assertContains( $this->example_shortcode_content, $output, 'Shortcode was applied without wpautop corrupting it.' ); - $this->assertEquals( 10, has_filter( 'widget_text', 'do_shortcode' ), 'Filter was restored.' ); - $this->assertFalse( has_filter( 'widget_text_content', 'do_shortcode' ), 'Filter was removed.' ); + $this->assertNotContains( '

' . $this->example_shortcode_content . '

', $output, 'Expected shortcode_unautop() to have run.' ); + $this->assertNull( $this->post_during_shortcode ); - // Visual Text Widget with properly-used widget_text_content filter. + // Legacy text widget with plugin adding shortcode support as well. + add_filter( 'widget_text', 'do_shortcode' ); + $this->shortcode_render_count = 0; + ob_start(); + $widget->widget( $args, $instance ); + $output = ob_get_clean(); + $this->assertEquals( 1, $this->shortcode_render_count ); + $this->assertNotContains( '[example]', $output, 'Expected shortcode to be processed in legacy widget with plugin adding filter' ); + $this->assertContains( wpautop( $this->example_shortcode_content ), $output, 'Shortcode was applied *with* wpautop() applying to shortcode output since plugin used legacy filter.' ); + $this->assertNull( $this->post_during_shortcode ); remove_filter( 'widget_text', 'do_shortcode' ); - add_filter( 'widget_text_content', 'do_shortcode', 11 ); + $instance = array_merge( $base_instance, array( - 'filter' => 'content', + 'filter' => true, + 'visual' => true, ) ); + + // Visual Text Widget with only core-added widget_text_content filter for do_shortcode. + $this->assertFalse( has_filter( 'widget_text', 'do_shortcode' ) ); + $this->assertEquals( 11, has_filter( 'widget_text_content', 'do_shortcode' ), 'Expected core to have set do_shortcode as widget_text_content filter.' ); + $this->shortcode_render_count = 0; ob_start(); $widget->widget( $args, $instance ); $output = ob_get_clean(); + $this->assertEquals( 1, $this->shortcode_render_count ); $this->assertContains( $this->example_shortcode_content, $output, 'Shortcode was applied without wpautop corrupting it.' ); - $this->assertFalse( has_filter( 'widget_text', 'do_shortcode' ), 'Filter was not erroneously restored.' ); + $this->assertNotContains( '

' . $this->example_shortcode_content . '

', $output, 'Expected shortcode_unautop() to have run.' ); + $this->assertFalse( has_filter( 'widget_text', 'do_shortcode' ), 'The widget_text filter still lacks do_shortcode handler.' ); + $this->assertEquals( 11, has_filter( 'widget_text_content', 'do_shortcode' ), 'The widget_text_content filter still has do_shortcode handler.' ); + $this->assertNull( $this->post_during_shortcode ); + + // Visual Text Widget with both filters applied added, one from core and another via plugin. + add_filter( 'widget_text', 'do_shortcode' ); + $this->shortcode_render_count = 0; + ob_start(); + $widget->widget( $args, $instance ); + $output = ob_get_clean(); + $this->assertEquals( 1, $this->shortcode_render_count ); + $this->assertContains( $this->example_shortcode_content, $output, 'Shortcode was applied without wpautop corrupting it.' ); + $this->assertNotContains( '

' . $this->example_shortcode_content . '

', $output, 'Expected shortcode_unautop() to have run.' ); + $this->assertEquals( 10, has_filter( 'widget_text', 'do_shortcode' ), 'Expected do_shortcode to be restored to widget_text.' ); + $this->assertNull( $this->post_during_shortcode ); + $this->assertNull( $this->post_during_shortcode ); + remove_filter( 'widget_text', 'do_shortcode' ); + + // Visual Text Widget with shortcode handling disabled via plugin removing filter. + remove_filter( 'widget_text_content', 'do_shortcode', 11 ); + remove_filter( 'widget_text', 'do_shortcode' ); + $this->shortcode_render_count = 0; + ob_start(); + $widget->widget( $args, $instance ); + $output = ob_get_clean(); + $this->assertEquals( 0, $this->shortcode_render_count ); + $this->assertContains( '[example]', $output ); + $this->assertNotContains( $this->example_shortcode_content, $output ); + $this->assertFalse( has_filter( 'widget_text', 'do_shortcode' ) ); + $this->assertFalse( has_filter( 'widget_text_content', 'do_shortcode' ) ); } /**