Skip to content

Commit 6327832

Browse files
committed
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 WordPress#10457. git-svn-id: https://develop.svn.wordpress.org/trunk@41361 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 249c416 commit 6327832

3 files changed

Lines changed: 131 additions & 26 deletions

File tree

src/wp-includes/default-filters.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@
169169
add_filter( 'widget_text_content', 'wptexturize' );
170170
add_filter( 'widget_text_content', 'convert_smilies', 20 );
171171
add_filter( 'widget_text_content', 'wpautop' );
172+
add_filter( 'widget_text_content', 'shortcode_unautop' );
173+
add_filter( 'widget_text_content', 'do_shortcode', 11 ); // Runs after wpautop(); note that $post global will be null when shortcodes run.
172174

173175
add_filter( 'date_i18n', 'wp_maybe_decline_date' );
174176

src/wp-includes/widgets/class-wp-widget-text.php

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,14 @@ public function is_legacy_instance( $instance ) {
183183
*
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'.
188190
* @param array $instance Settings for the current Text widget instance.
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 */
193196
$title = apply_filters( 'widget_title', empty( $instance['title'] ) ? '' : $instance['title'], $instance, $this->id_base );
@@ -205,16 +208,22 @@ public function widget( $args, $instance ) {
205208
}
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

220229
/**
@@ -244,14 +253,35 @@ public function widget( $args, $instance ) {
244253
* @param WP_Widget_Text $this Current Text widget instance.
245254
*/
246255
$text = apply_filters( 'widget_text_content', $text, $instance, $this );
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+
}
247277

248-
} elseif ( ! empty( $instance['filter'] ) ) {
249-
$text = wpautop( $text ); // Back-compat for instances prior to 4.8.
278+
// Restore post global.
279+
if ( isset( $suspended_post ) ) {
280+
$post = $suspended_post;
250281
}
251282

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 );
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
}
257287

tests/phpunit/tests/widgets/text-widget.php

Lines changed: 87 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -222,71 +222,144 @@ function test_widget() {
222222
*
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
/**
228242
* Do example shortcode.
229243
*
230244
* @return string Shortcode content.
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>',
244264
'after_title' => "</h2>\n",
245265
'before_widget' => '<section>',
246266
'after_widget' => "</section>\n",
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]",
273+
'text' => "This is an example:\n\n[example]\n\nHello.",
255274
'filter' => false,
256275
);
257276

258-
// Legacy Text Widget.
277+
// Legacy Text Widget without wpautop.
259278
$instance = array_merge( $base_instance, array(
260279
'filter' => false,
261280
) );
281+
$this->shortcode_render_count = 0;
262282
ob_start();
263283
$widget->widget( $args, $instance );
264284
$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.' );
288+
$this->assertNotContains( '<p>' . $this->example_shortcode_content . '</p>', $output, 'Expected shortcode_unautop() to have run.' );
289+
$this->assertNull( $this->post_during_shortcode );
267290

268-
// Visual Text Widget.
291+
// Legacy Text Widget with wpautop.
269292
$instance = array_merge( $base_instance, array(
270-
'filter' => 'content',
293+
'filter' => true,
294+
'visual' => false,
271295
) );
296+
$this->shortcode_render_count = 0;
272297
ob_start();
273298
$widget->widget( $args, $instance );
274299
$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.' );
303+
$this->assertNotContains( '<p>' . $this->example_shortcode_content . '</p>', $output, 'Expected shortcode_unautop() to have run.' );
304+
$this->assertNull( $this->post_during_shortcode );
278305

279-
// Visual Text Widget with properly-used widget_text_content filter.
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 );
317+
282318
$instance = array_merge( $base_instance, array(
283-
'filter' => 'content',
319+
'filter' => true,
320+
'visual' => true,
284321
) );
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;
285327
ob_start();
286328
$widget->widget( $args, $instance );
287329
$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

292365
/**

0 commit comments

Comments
 (0)