WordPress.org

Make WordPress Core

Opened 4 years ago

Closed 4 years ago

#36097 closed defect (bug) (duplicate)

wp_filter reset after sub-queries

Reported by: cartpauj Owned by:
Milestone: Priority: normal
Severity: normal Version: 4.4.2
Component: Query Keywords: reporter-feedback
Focuses: Cc:
PR Number:

Description

I have a shortcode which implements a sub-query and loops through calling the_content() in the process. At the end I call wp_reset_query() and wp_reset_postdata().

That's all fine and good, but the problem I'm experiencing is that the content from the main query (where the shortcode was placed to begin with) no longer executes any filters on the_content.

Here's a real world example of what I'm experiencing. Let's say I have a shortcode [some-custom-loop] which outputs a custom loop.

function my_shortcode_callback($atts, $content = '') {
    global $post;
    global $wp_query;

    $args          = array(//SOME ARGS HERE//);
    $original_post = $post;
    $wp_query      = new WP_Query($args);

    ob_start();

    while($wp_query->have_posts()) {
      $wp_query->the_post();

      //Avoid infinite loop
      if($original_post->ID === $post->ID) {
        continue;
      }

      the_content();
    }

    // Reset the global query.
    wp_reset_query();
    wp_reset_postdata();

    return ob_get_clean();
}
add_shortcode('some-custom-loop', 'my_shortcode_callback');

Now, this filter below does not get applied to the_content of posts from the main query which contain this shortcode, but it is applied to the_content on the post(s) from the sub-query

function append_to_content($content) {
  return $content . "<p>Hello WP!</p>";
}
add_filter('the_content', 'append_to_content');

I'm honestly not even sure what to recommend as a fix at this point, but doing the following with $wp_filter global is working as a work-around for now.

function my_shortcode_callback($atts, $content = '') {
    global $post;
    global $wp_query;
    global $wp_filter; //Changed

    $original_filter = $wp_filter; //Changed
    $args            = array(//SOME ARGS HERE//);
    $original_post   = $post;
    $wp_query        = new WP_Query($args);

    ob_start();

    while($wp_query->have_posts()) {
      $wp_query->the_post();

      //Avoid infinite loop
      if($original_post->ID === $post->ID) {
        continue;
      }

      the_content();
    }

    // Reset the global query.
    wp_reset_query();
    wp_reset_postdata();

    $wp_filter = $original_filter; //Changed

    return ob_get_clean();
}
add_shortcode('some-custom-loop', 'my_shortcode_callback');

So to sum up what is happening (I think):

  1. apply_filters is called on the_content
  2. do_shortcode runs creating a sub-query
  3. apply_filters is called on the_content again since the shortcode is calling the_content();
  4. All of the_content's filters are run in the sub-query
  5. When the do_shortcode call is finished, all of the filters for the_content ($wp_filter) have already run, so any after that don't get called on the main content.

Change History (6)

#1 @justinbusa
4 years ago

I can confirm that sub-queries are causing subsequent filters to not be applied in the main query. This seems like a tough one to fix, but I'd be happy to have a look if someone with knowledge of apply_filters can provide some direction. I had a look at the logic for that one but am not sure where to start.

#2 @boonebgorges
4 years ago

  • Keywords reporter-feedback added

I'm not able to reproduce the issue. The filter is being applied to each instance of the_content() within the shortcode query, as well as the content of the post that contains the shortcode. I've used the exact code from above (but filled in some args for the WP_Query instance), in the context of an mu-plugin. Have I done something wrong?

It may not be related, but it's odd and unnecessary to assign your shortcode's query to the $wp_query global. global $wp_query is supposed to contain the main query. The following should be sufficient:

$my_query = new WP_Query( $args );
while ( $my_query->have_posts() ) {
    $my_query->the_post();

    // ...
}

// wp_reset_query() is not necessary if you're not touching $wp_query
wp_reset_postdata();

When the do_shortcode call is finished, all of the filters for the_content ($wp_filter) have already run, so any after that don't get called on the main content.

Not exactly. Shortcodes are parsed like this:

add_filter( 'the_content', 'do_shortcode', 11 );

Consider a post with the content "Main Post Content [some-custom-loop]". This will get run through append_to_content() first, at priority 10: "Main Post Content [some-custom-loop]<p>Hello WP!</p>". Then, at priority 11, do_shortcode() will run my_shortcode_callback() and replace "[some-custom-loop]" with the content of your output buffer: "Main Post Content $output_buffer <p>Hello WP!</p>". This is, in fact, what I'm seeing in my tests.

To put the point a different way: 'the_content' callbacks, once registered via apply_filters(), are not removed from the $wp_filter array unless it's done manually via remove_filter(). The callbacks will be invoked each time the_content() is called. While it's true that you're running a WP_Query inside of another WP_Query, the calls to the_content() are not themselves nested in any way that's relevant here. By the time the_content() is called in your shortcode handler, append_to_content() has already been applied to the content of the container post.

Again, it could be that I've missed a critical part of the setup, or it could be that you've got a rogue plugin doing something funky with remove_filters(). But WP shouldn't be demonstrating this behavior out of the box, and it's not for me.

#3 @justinbusa
4 years ago

Thanks for having a look at this! For context, I have run into this before but have always found a workaround until cartpauj ran into a conflict between our two plugins.

I need some time to put together some examples, but what I believe is happening is that the array pointer for $wp_filtersthe_content? gets moved to the end for sub-queries, so subsequent filters never get called on the main query. Give me a few days and I'll come back with some examples, or my tail between my legs ;) Thanks!

#4 @cartpauj
4 years ago

I will set up a vanilla WP site and see if I can replicate this again as well. I'll post my findings.

#5 follow-up: @cartpauj
4 years ago

@boonebgorges - If you set the priority of the_content filter to 12 or greater, you'll see this happen. My fault for forgetting that part in the initial post.

There are some instances where it doesn't work when priority < 12 - but I couldn't duplicate that on the vanilla WP site, so I don't think we need to worry about that case here.

#6 in reply to: ↑ 5 @boonebgorges
4 years ago

  • Milestone Awaiting Review deleted
  • Resolution set to duplicate
  • Status changed from new to closed

Replying to cartpauj:

@boonebgorges - If you set the priority of the_content filter to 12 or greater, you'll see this happen. My fault for forgetting that part in the initial post.

Ah, thanks. With this, I'm able to reproduce.

To spell it out a bit more: apply_filters( 'the_content' ), when run on the post in the main loop, triggers do_shortcode() at priority 11, inside the foreach loop here https://core.trac.wordpress.org/browser/tags/4.4.2/src/wp-includes/plugin.php?marks=235#L226. Thus, at this point, the array pointer of $wp_filter['the_content'] is at 11. When apply_filters( 'the_content' ) is run inside the shortcode handler, the pointer is reset, and runs all the way through the end of the $wp_filter['the_content'] array - at least until 12, where append_to_content() has been hooked. When do_shortcodes() is finished, and we return to the foreach loop, the pointer is now at the end of $wp_filter['the_content']. Thus, next( $wp_filter[ $tag ] ) === false, and we never end up processing anything at priority 12+.

The obvious way around this is to make a separate copy of $wp_filter[ $tag ] each time apply_filters( $tag ) is run, so that array pointers are not shared in cases like this. But I think there are performance concerns to this.

This is all getting a bit above my pay grade, so on a whim, I checked against #17817. The WP_Hook implementation fixes the currently reported issue. Because of this, and because I think that what's being reported here is a close cousin of what's being reported in #17817, I'm going to mark this one as a duplicate.

Note: See TracTickets for help on using tickets.