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.
' . $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' ) ); } /**