Make WordPress Core

Opened 7 months ago

Closed 4 months ago

#60290 closed defect (bug) (fixed)

Changeset #56635 breaks template loading in multisite network using switch_to_blog()

Reported by: metropolis_john's profile metropolis_john Owned by: joemcgill's profile joemcgill
Milestone: 6.5 Priority: normal
Severity: normal Version: 6.4.2
Component: Themes Keywords: has-patch has-unit-tests changes-requested
Focuses: performance Cc:

Description

The changes to the locate_template() function in https://core.trac.wordpress.org/changeset/56635 that replaced STYLESHEETPATH and TEMPLATEPATH constants with the relevant functions have broken the get_template_part() functionality in a multisite network that uses switch_to_blog().

Expected behavior: get_template_part() should only look in the active theme of the current site even after switch_to_blog() is used.

In WP 6.4.2, if switch_to_blog(1) is used in site 2, then get_template_part() is called, only the active theme of site 1 is searched for the template part. switch_to_blog() should only affect database calls and should not allow theme developers to pull template parts from the theme of another site in a network.

Change History (33)

#1 @joemcgill
7 months ago

  • Keywords needs-patch added
  • Milestone changed from Awaiting Review to 6.5
  • Owner set to joemcgill
  • Severity changed from major to normal
  • Status changed from new to reviewing

Thanks for the report @metropolis_john.

I agree that this is an unexpected change in behavior. I'm curious what the use case is for calling get_template_part() after switch_to_blog()?

A possible fix for this is to move the memoization approach removed in [57129] to locate_template() rather than in the get_stylesheet_directory() and get_template_directory() functions where they previously were implemented.

Related issues:

#2 @metropolis_john
7 months ago

@joemcgill In this particular case we have a multisite network where the "main" site is a portal where users can login and maintain a portfolio of uploaded artwork. There are 2 other sites in the network that both query artists (users) and artwork from the main site using switch_to_blog(). The two sites have unique themes and each use unique template parts to display an artist or piece of artwork.

As we loop through the queried users we sometimes need to make an additional query to check if they have authored any posts (artwork) with specific parameters, so in those cases rather than bouncing back and forth between sites it makes sense to use switch_to_blog() only once and restore_current_blog() after the loop, while the loop contains a get_template_part() call.

That might look something like this:

switch_to_blog(1);

$artists = new WP_User_Query( array(
  'role'     => 'artist',
  'meta_key' => 'last_name',
  'orderby'  => 'meta_value',
) );

foreach( $artists->get_results() as $artist ) {
			
  $artwork = new WP_Query( array(
    'post_type'      => 'artwork',
    'posts_per_page' => 1,
    'author'         => $artist->ID,
    'meta_query'     => array(
      array(
	'key'        => 'price',
	'value'      => '',
	'compare'    => '!=',
      ),
    ),
  ) );
					
  if( $artwork->have_posts() ) {
    get_template_part( 'template-parts/artist' );	
  } 

}

restore_current_blog();

#3 @manfcarlo
7 months ago

At first glance, the change to switch_to_blog looks like a correction to me. You did say that switch_to_blog should only affect database calls, but the template and stylesheet options are themselves stored in the database, so I think it would be surprising if they don't get switched over.

If I read your code correctly, it looks like you could easily solve the problem by appending user ID's that have posts to an array and calling get_template_part on each of them after you switch back to blog 2.

This ticket was mentioned in PR #5926 on WordPress/wordpress-develop by @joemcgill.


7 months ago
#4

  • Keywords has-patch has-unit-tests added; needs-patch removed

This change updates locate_template() so that the stylesheet and template paths get stored to a global variable the first time this function is called and resets these globals any time the theme is switched. This memoizes these values similarly to how the previous STYLESHEETPATH and TEMPLATEPATH constants were stored prior to https://core.trac.wordpress.org/changeset/56635, but allows these values to be reset when the theme is switched. However, this avoids changing the template path whenever running in a switch_to_blog context.

Trac ticket: https://core.trac.wordpress.org/ticket/60290

#5 @joemcgill
7 months ago

  • Focuses performance added
  • Status changed from reviewing to accepted

I've come up with a possible fix for this issue that ensures locate_template() uses the original site's theme whenever in a switch_to_blog context. @metropolis_john could you see if something like this would work in your context?

#6 follow-up: @manfcarlo
7 months ago

The following was written on the other ticket #60025:

This is sounding more and more like a wontfix situation since the previous behavior was both undocumented and unintentional.

@joemcgill can you explain how this ticket isn't the same with regard to "undocumented and unintentional" behaviour? One would expect that switch_to_blog switches the blog. If it was intentional to keep locate_template on the previous blog, it would have been documented in the code reference for switch_to_blog, but the only exception listed there is plugins. It would be disingenuous to say after so many years that the omission was intentional.

#7 in reply to: ↑ 6 ; follow-up: @joemcgill
7 months ago

Replying to manfcarlo:

@joemcgill can you explain how this ticket isn't the same with regard to "undocumented and unintentional" behaviour?

Good question, @manfcarlo. I think that this case is slightly different in that the previous behavior that changed was the expected behavior. Prior to [56635], locate_template() relied on the value of constants that were set early in a request lifecycle because the expectation is that you would only have a single theme active during one request. switch_to_blog() is a function that is meant to let you access data from another site on the network (e.g., users, posts, options, etc.), but was not meant to allow you to change to different parts of a filesystem. This similar to why switch_to_blog() doesn't change which plugins are active (see #14941).

#60025 was relying on behavior that was a side-effect of the previous implementation, in which you could trick WP into using the current theme's root directory, but then loading templates from a different sub-directory, based on a race condition between when the constants were set and when filters were registered. Even so, the approach in this PR might restore the previous (undocumented) behavior for #60025 as well.

#8 @metropolis_john
7 months ago

@joemcgill Thanks for your effort on this - the approach in your PR does resolve the template loading issue in our multisite setup

This ticket was mentioned in Slack in #core-performance by joemcgill. View the logs.


7 months ago

@joemcgill commented on PR #5926:


7 months ago
#10

Thanks for the quick feedback, @felixarntz. After giving it more thought, I had also come to the same conclusion that moving the memoization of the template paths to load_template() directly is not ideal, since we cannot be sure when in the request lifecycle that function is being called.

Originally, the template constants were set using the wp_templating_constants() function in wp-settings.php (ref), right after the setup_theme action is fired as a first step to loading the active theme. Once those constants were set, several templating functions, including load_template() relied on those stored values for the rest of the lifecycle.

After [56635], those values were instead stored as static variables inside of get_stylesheet_directory() and get_template_directory(), which were initially called before the theme's functions.php file is loaded and any filters provided by the theme were added, which is what caused the initial bugs. By removing that memoization from those functions, we've introduced a new set of issues for the template functions that assumed that relied on the previous constants, as those were immutable (unlike the values returned from the related functions).

As a compromise, I've updated the approach here so that a new function is added, wp_set_template_globals() which sets these global values at the same place where the previous constants were being set. This function is also now used in locate_template() and switch_theme() to ensure globals are properly reset to the currently active theme when needed. We may also need to update other places where the old constants were replaced with the helper functions in the original commit to ensure backwards compatibility.

#11 in reply to: ↑ 7 @manfcarlo
7 months ago

Replying to joemcgill:

switch_to_blog() is a function that is meant to let you access data from another site on the network (e.g., users, posts, options, etc.), but was not meant to allow you to change to different parts of a filesystem. This similar to why switch_to_blog() doesn't change which plugins are active (see #14941).

I think this is very different from #14941. The request there was to un-include files that were already included, which is not possible with PHP, so there was no choice but to leave the ticket unpatched.

What we are talking about here is adding new code to explicitly disable a feature of switch_to_blog when it was fully operational previously.

If I actually want to load a template part from the other site, and I know the template part isn't going to cause any PHP errors, then I would use this code:

switch_to_blog( 2 );
get_template_part( 'template-part-for-site-2' );
restore_current_blog();

If the above code is to not work as I expected, who does it benefit? It adds extra maintenance to core and causes inconvenience to me.

Hypothetically, your patch could cause even greater issues. If the template part being included fetches theme mods or fetches the theme URI to load assets etc, it will try to fetch from the switched blog, which is obviously of no use to anyone.

moshiajonta commented on PR #5926:


6 months ago
#13

i was thinking same few days ago this issue is going upper level

This ticket was mentioned in Slack in #core by jorbin. View the logs.


6 months ago

This ticket was mentioned in Slack in #core by joemcgill. View the logs.


6 months ago

@joemcgill commented on PR #5926:


6 months ago
#16

After reviewing all of the changes that were originally made in r56635, I believe the only safe way to make this change is to also use of the same globals in all the places where the previous constants were in use. I've applied those changes in the following functions:

@joemcgill commented on PR #5926:


6 months ago
#17

@swissspidy

Feels a bit ironic that we end up replacing constants with globals :)

Agreed, and I feel the same. Essentially, we were using the old constants as a way to store the application state for template paths while the functions that are responsible for getting these values can change after the initial state is set. By keeping them as constants, any functions that relied on the old constants were not testable. However, trying to deprecate the constants in favor of using the functions directly caused several bugs due to the fact that these functions are used throughout the application in different ways than the state stored in the old constants and expect different values.

Perhaps there is a different pattern that would be preferable to using globals in this case, but I didn't want to over-engineer something in order to fix the bugs that have been reported.

@manfcarlo commented on PR #5926:


6 months ago
#18

I didn't want to over-engineer something in order to fix the bugs that have been reported.

It looks over-engineered to me already, and I'd use stronger language than "ironic" to describe the replacement of constants with globals. I just don't see enough evidence that these "bugs" are actually bugs at all. What it looks like to me is the real bugs got coincidentally fixed and a small number of vocal theme/plugin authors complained because they were relying on the bugs and were forced to update their theme/plugin.

By the time this patch gets committed and deployed, which I hope it doesn't, anyone affected will have found other solutions already and it will serve no purpose other than to frustrate many other plugin/theme authors who want functions that behave in simple and consistent ways and are easy to learn. This kind of thing makes people want to stop authoring themes/plugins altogether, and most of them just leave quietly without making their frustrations known.

Some core decisions that I've seen recently have been so perplexing that it made me honestly question whether "shooing" plugin/theme authors is the intention. If so, then go right ahead and commit this, but I'm still perplexed. If not, maybe reflect on why it so often looks that way. It seems there is often too much attention on designing a solution and not enough effort to define the problem.

metropoliscreative commented on PR #5926:


6 months ago
#19

@joemcgill Thanks for your time on this PR - I think it works to solve the multisite issues we discussed in ticket #60290 and restores previously expected functionality.

Will this make it into 6.5?

This ticket was mentioned in Slack in #core-performance by mukeshpanchal27. View the logs.


6 months ago

@manfcarlo commented on PR #5926:


6 months ago
#21

@metropoliscreative I previously suggested an alternative for you in ticket 60290 that you could use immediately without a core patch. I would be interested to know whether it achieves what you're looking for.

@metropolis_john commented on PR #5926:


6 months ago
#22

@metropoliscreative I previously suggested an alternative for you in ticket 60290 that you could use immediately without a core patch. I would be interested to know whether it achieves what you're looking for.

@carlomanf Yes, thanks for your feedback. I understand you have strong opinions about this ticket and PR, but the goal here is to address unintended functionality changes from [56635] related to switch_to_blog(). If you feel this long-standing functionality is in fact a bug, then please open another ticket to address it.

@manfcarlo commented on PR #5926:


6 months ago
#23

@metropoliscreative Let me quote the part I am referring to, in case you missed it:

If I read your code correctly, it looks like you could easily solve the problem by appending user ID's that have posts to an array and calling get_template_part on each of them after you switch back to blog 2.

The point I am making is that 6.5 is more than a month away, a number of core committers above have expressed concern about the design of this patch, and there is potentially a way for you to independently solve the problem you encountered. Maybe you are already doing something like this, but it would assist everyone involved to find a way forward if you could confirm.

This ticket was mentioned in Slack in #core-performance by joemcgill. View the logs.


6 months ago

This ticket was mentioned in Slack in #core-performance by joemcgill. View the logs.


6 months ago

#26 @joemcgill
6 months ago

  • Resolution set to fixed
  • Status changed from accepted to closed

In 57685:

Themes: Use original template paths when switching blogs.

This fixes a bug introduced by [57129] and [56635] in which deprecating the previous TEMPLATEPATH and STYLESHEETPATH constants in favor of get_template_directory() and get_stylesheet_directory() functions caused the active theme template path to change when using switch_to_blog().

This introduces a new function, wp_set_template_globals(), which is called during the bootstrap process to store the template paths to new globals values $wp_template_path and $wp_stylesheet_path. This restores behavior to how things worked prior to [56635] but retains the ability for template values to be reset for better testability.

Related #18298, #60025.

Props joemcgill, flixos90, mukesh27, swissspidy, manfcarlo, metropolis_john, jeremyfelt.
Fixes #60290.

@joemcgill commented on PR #5926:


6 months ago
#27

Merged in https://core.trac.wordpress.org/changeset/57685. Thanks all for the feedback and testing.

#28 @manfcarlo
5 months ago

  • Keywords changes-requested 2nd-opinion added
  • Resolution fixed deleted
  • Status changed from closed to reopened

As flagged in comment:11, the solution committed here is not tested with template parts that use get_theme_mod or get_template_directory_uri. Many template parts use these. For example, the header.php in Twenty Eleven, Twenty Twelve and Twenty Fourteen use get_template_directory_uri for the HTML5 polyfill. The Twenty Twenty-One site-branding.php and site-header.php use get_theme_mod to conditionally output markup based on the presence of a theme mod.

With the solution committed, if blog A has Twenty Twenty-One and blog B has another theme, such as a block theme, switching from blog A to blog B will fetch the template part from Twenty Twenty-One but fetch the theme mods from the block theme, which likely doesn't use theme mods, so Twenty Twenty-One's conditional markup will never output, even if the theme mod is set and it would have output without the blog switched.

If blog A has Twenty Eleven, Twenty Twelve or Twenty Fourteen, switching from blog A to blog B will fetch the header from the old theme but generate the HTML5 polyfill URI for the block theme, which likely doesn't have an HTML5 polyfill and is likely to generate a 404 error.

The scenarios above are clearly bugs. For a long time, these bugs were difficult to fix because of the use of the TEMPLATEPATH and STYLESHEETPATH constants. 6.4 prevented these bugs if not fixed them. The patch committed reinstates these bugs.

Understandably, this ticket was opened on the basis of reinstating long-standing behaviour. However, by the same token, a whole release cycle (4 to 5 months) has passed since 6.4, and there is every chance that theme/plugin authors have since deployed code that relies on the 6.4 behaviour, perhaps not being aware of the previous behaviour. They will not understand why 6.5 causes this breakage, especially since the patch has no documentation and the code is not very easy to interpret without locating the commit message. The last thing anyone wants is to give people a reason to downgrade from 6.5 to 6.4.

If this is to be treated as a fix of a regression, the further things should be done:

  • committed to 6.4, as 6.4 introduced the regression
  • extra documentation added to explain why it works this way and warn against errors caused by get_theme_mod and get_template_directory_uri (see also #60332)
  • addition of a filter or similar to reinstate the previous 6.4 behaviour for those who need it

It's clearly a matter of opinion whether 6.4 was a regression or not, so the extra work is better avoided and I would rather just revert. I don't see the evidence that the 6.4 behaviour is causing any major problems for anyone, other than encouraging them to improve their code.

This ticket was mentioned in Slack in #core-performance by mukeshpanchal27. View the logs.


5 months ago

#30 follow-up: @joemcgill
5 months ago

  • Keywords fixed-major added; 2nd-opinion removed
  • Milestone changed from 6.5 to 6.4.4

@manfcarlo Thank you for the attention that you've given to this issue and for sharing your concerns. I can tell you that marking this issue as a wontfix for the reasons you have raised throughout this ticket were seriously considered. After feedback from many longstanding contributors and component maintainers, including @swissspidy and @jeremyfelt, I decided to fix this bug.

The get_theme_mod issues that you have raised here seem valid, but have been existing issues for a long time, except for WP 6.4.2+ when this issue was first introduced due to [57129]. Based on your response, it sounds like your concern is theoretical rather than an issue that is affecting your project, whereas this issue was opened to address a change in behavior that was adversely effecting real sites. Now that this issue is solved, I'd encourage you to open a new issue to address the issues you've described here.

I agree with you that we should consider backporting this fix to 6.4 as well, so I've moved this to the 6.4.4 milestone for consideration, but at this time no other maintenance releases are planned for 6.4. If another maintenance release does not happen before 6.5, this should be closed as fixed.

#31 in reply to: ↑ 30 @manfcarlo
5 months ago

Replying to joemcgill:

After feedback from many longstanding contributors and component maintainers, including @swissspidy and @jeremyfelt, I decided to fix this bug.

Replying to joemcgill:

Based on your response, it sounds like your concern is theoretical rather than an issue that is affecting your project, whereas this issue was opened to address a change in behavior that was adversely effecting real sites. Now that this issue is solved, I'd encourage you to open a new issue to address the issues you've described here.

My concern is that I am a heavy user of switch_to_blog and until this ticket was not aware of this behaviour with templates and template parts. I would have to think about it some more to determine if it affects my project.

Prior to [57596], the use of a bulleted list in the documentation strongly suggested that plugins were the only thing not switched. Even the change to PHP code loaded with the originally requested site, such as code from a plugin or theme does not cover this because templates and template parts are not pre-loaded like functions.php and there is no structural reason that they can't be switched.

Honestly, it's unlikely that I would open a new ticket at this stage, as I don't see how a new ticket would be able to achieve anything that this ticket can't, and more dead-end tickets is not what the project needs. However, seeing as you consulted with @jeremyfelt, who drafted the change to the documentation, it looks like #60332 will need to be re-opened to cover this.

Replying to joemcgill:

I agree with you that we should consider backporting this fix to 6.4 as well, so I've moved this to the 6.4.4 milestone for consideration, but at this time no other maintenance releases are planned for 6.4. If another maintenance release does not happen before 6.5, this should be closed as fixed.

I see that there is one other ticket in the 6.4.4 milestone, there is a lot of demand for a new maintenance release, and a lot of the same arguments made over there also apply here:

We're going to get hundredes of calls asking us to rollback the change and this is going to cause a lot of pain both to our customers and to us.

This isn't a "new feature" to be added in a major version. This is a major bug to be patched in the major version it was introduced in.

Anyway, 835 million sites use WordPress. As of now 41,750,000 of those run WordPress 6.2. So millions of sites will have this bug for quite awhile if as many people continue to run 6.4 for as long as they have been running 6.2.

The idea that this will go away in a few weeks in 6.5 is a fantasy. 6.5 does not make this problem go away.

Edit for clarity: #60398 is the ticket I'm referring to.

Last edited 5 months ago by manfcarlo (previous) (diff)

#32 @SergeyBiryukov
4 months ago

  • Milestone changed from 6.4.4 to 6.5.1

Looks like this needs some more work after [57685].

With WordPress 6.5 released, moving this to the 6.5.1 milestone, as there are no plans for 6.4.4 at this time.

This ticket was mentioned in Slack in #core by jorbin. View the logs.


4 months ago

#34 @jorbin
4 months ago

  • Keywords fixed-major removed
  • Milestone changed from 6.5.1 to 6.5
  • Resolution set to fixed
  • Status changed from reopened to closed

This was fixed in 6.5 with [57685] so closing this as fixed during that milestone. It will not be backported to 6.4

Note: See TracTickets for help on using tickets.