In this blog, we’ll discuss an issue that happened while performing monthly maintenance on a client’s website that involved updating WordPress Core and Plugins. We’ll deep dive into the discovery of the problem, will discuss how we identified the root cause, and how we went about implementing a fix.
The Goal: Update WordPress to 5.3
Install monthly WordPress updates including Core, Plugin, and Theme files. This is a must for any website running WordPress. In this case, I was upgrading from WP Core 5.2.4 to WP Core 5.3 along with a bunch of plugin updates. In the years that I’ve been doing updates on this system, I haven’t encountered an issue of this magnitude.
The WordPress Hosting Environment
These websites are running on WordPress multisite with two websites powered by the install. Both websites use a custom theme that relies on custom plugins for some functionality. One website uses the parent theme because it is the main site and the other has a child theme to separate out site-specific changes. Typically we use Cloudways as our preferred cloud hosting environment, but this install is running on a high power Virtual Private Server (VPS) and each website has independent CDNs and firewalls.
The Issue After Installing WordPress 5.3
After installing the WordPress 5.3 updates, some backend (admin) pages were not displaying correctly and some core WordPress functionality was broken. The most important being Advanced Custom Fields Pro, or ACF Pro, which powered the flexible layout system these websites are built on. This issue was only present in the administrative interface (wp-admin), and there was no disruption to the user-facing pages.
One of the truly odd aspects of this issue is that there were no additional console errors to identify what wasn’t loading properly. All I could see was the admin interface not looking like what WordPress 5.3 was supposed to look like. The page list was displayed in a completely unusable way and ACF Pro custom fields were not appearing on pages that they were supposed to be on. Everything from the WYSIWYG editor to the Yoast SEO meta boxes was unusable on posts and pages. However, when you switch to the front end of the site, everything appeared normal and the familiar face of the website was staring right back at me.
Findings / Root Cause
The issue turned out to be directly related to the WordPress Core 5.3 update and a custom plugin integration that the websites required. Some of the core functionality of this plugin modifies the administrative interface which was updated for accessibility in WordPress 5.3. During this update, WordPress developers removed the type attribute from linked CSS files on the administrative side only, which caused a custom matching algorithm to break because it was expecting the attribute to be there. This, in turn, caused CSS files to fail to load in some cases on the WP admin panel.
XDebug and VSCode to the Rescue
Typically I use XDebug and VScode when developing custom themes or plugins to verify that my code is functioning properly and not causing issues by placing breakpoints at specific points. This was the first time that I was going to use it to track down an unknown issue and I didn’t have the faintest idea where to even start placing the breakpoints. Luckily, VSCode has the option to debug everything and follow the call stack through various functions that are throwing warnings and errors (WordPress has a lot of these, which is why I like strategic debugging over catchall). A few calls deep I notice an offset error when iterating through an array in a custom must-use plugin in a function called “style_tags_tidy”.
The style_loader_tag WordPress Hook and preg_match_all Function
The style_tags_tidy function seemed like a good place to drop a few breakpoints to narrow down what I was looking at—and by a few I mean I added a breakpoint to every line of the function to follow along with what it was doing. Here’s the function which was filtered on the style_loader_tag hook in WordPress:
public function style_tags_tidy($input) {
preg_match_all(“!<link rel=’stylesheet’\s?(id='[^’]+’)?\s+href='(.*)’ type=’text/css’ media='(.*)’ />!”, $input, $matches);
$media = $matches[3][0] !== ” && $matches[3][0] !== ‘all’ ? ‘ media=”‘ . $matches[3][0] . ‘”‘ : ”;
return ‘<link rel=”stylesheet” href=”‘ . $matches[2][0] . ‘”‘ . $media . ‘>’ . “\n”;
}
It’s a pretty simple function that is taking every CSS stylesheet that is loaded and matching it against a capturing regular expression, or regex, to get specific information. Mainly the:
- ID
- Source location
- Media attribute value
Then, they return a custom link with only the rel, href, and media attributes (if the media attribute existed in the first place).
The problem with filtering the style_loader_tag Hook
It all comes down to one seemingly insignificant issue(?) in WordPress 5.3 functionality—linked CSS files no longer contain type=’text/css’ on the backend of WordPress. This in itself is a pretty small change since HTML5 dictates that the type attribute is only advisory and isn’t really valid or invalid. But since we’re using a custom regular expression to grab specific data from every CSS link, it had major ramifications in that the regex was no longer matching. This function was now erroring out and not returning anything, which means the CSS files weren’t even being called. This was the big aha moment—it completely explains the broken appearance of the wp-admin and the fact that the console wasn’t showing any errors.
I also debugged on the front-end to see why everything was acting normally because I would expect to see some signs of a problem if CSS files weren’t loading properly. This is the second big aha moment (maybe even more so than the first). The CSS links still contained type=’text/css’ on the frontend of a WordPress 5.3 site. So the custom function was working properly on the front-end and only broken on the backed.
WordPress 5.3 Backend CSS link:
<link rel='stylesheet' id='thickbox-css' href='https://example.test/wp-includes/js/thickbox/thickbox.css?ver=5.3' media='all' />
WordPress 5.3 Frontend CSS link:
<link rel='stylesheet' id='thickbox-css' href='https://example.test/wp-includes/js/thickbox/thickbox.css?ver=5.3' type='text/css' media='all' />
Time for a fix
It’s actually a very simple fix. We need to account for two types of tags now:
- Tags that have type=’text/css’
- Tags that don’t have type=’text/css’
The non-capturing regex groups save the day. A simple change to the existing regular expression to wrap the type attribute in a non-capturing group of 0 or more will fix the problem completely. The function now looks like this:
public function style_tags_tidy($input) {
preg_match_all("!<link rel='stylesheet'\s?(id='[^']+')?\s+href='(.*)' (?:type='text\/css')*media='(.*)' \/>!", $input, $matches);
if($matches[3][0] && $matches[2][0]){
$media = $matches[3][0] !== '' && $matches[3][0] !== 'all' ? ' media="' . $matches[3][0] . '"' : '';
return '<link rel="stylesheet" href="' . $matches[2][0] . '"' . $media . '>' . "\n";
}else
return $input;
}
I also threw in a conditional check to make sure that our second and third arrays actually exist, and if not, then to return the unfiltered input. Mainly because this function should always return something, otherwise you’ll run into major problems…kind of like we did.
Lessons Learned
- Backup Information Before Making Changes – This issue certainly reinforced the need for backups before making ANY changes. No matter how much testing and QA you do on development, staging, and production environments, unforeseen unique problems can happen at any time. Being able to quickly and easily restore a WordPress website, including files and databases, is an absolute necessity.
- Ensure You Have a Backout Procedure – A clearcut and efficient backout procedure is a must in any development organization. Just because you have the backup doesn’t mean your website is safe. You need a set of executable procedures in case something goes wrong. This will quickly help minimize any downtime associated with mistakes or unexpected errors.
- XDebug and VSCode are Invaluable in Tracking Down PHP Errors (especially on WordPress.) – Had I not been using this, finding the issue with someone else’s code would have taken hours to figure out. In all I spent maybe an hour and a half debugging, researching, and applying a fix. This, of course, requires a local testing environment such as Laravel Valet (or in my case Linux Valet).
- Standardize a QA Process – A repeatable, standardized, and preferably automated QA process should catch 95% of the issues that could happen. Believe me, I’ll be setting it up for this website after Thanksgiving. Check out Ghost Inspector if you’re curious about that.
- Always QA Test – Never become overly confident because you’ve worked on a site for so long. Like I said in the beginning, I’ve been working on and updating this site for years and not once have I come across a problem like this. As a matter of fact, most WordPress updates go quite smoothly (except maybe Gutenburg). Always QA and test as much as possible – frontend, backend, mobile, desktop – and better yet, get a second pair of eyes to check. A fresh pair of eyes never hurts.