Media Upload Request Flow in WordPress 5.3

After a couple of questions from hosting companies about how to detect how well uploads are working at the server/request level, I thought further documentation would help.

Here is a walkthrough of the request flow for media uploads, both before and after WordPress 5.3. Hopefully this will help with troubleshooting and debugging uploads on your infrastructure or sites!

Before WordPress 5.3

Before WordPress 5.3, uploads had to complete during a single HTTP POST request to .../wp-admin/async-upload.php or to the REST API with the media endpoint (.../wp-json/wp/v2/media/).

The block editor uses the REST API to upload, while any uploads outside the block editor (or using the popup Media Modal) use async-upload.php.

When uploads failed, they would return whatever HTTP error the server is set up to give (usually in the 5xx range). This is usually due to timeout or resource exhaustion (like running out of memory) reasons.

WordPress 5.3+

In WordPress 5.3+, uploads have been decoupled from a single request to allow for resuming. Now, when an upload returns a 5xx error, there can be up to 5 followup requests.

This is accomplished differently depending on whether admin-ajax.php or the REST API are used.

Legacy Uploads

For admin-ajax.php, this is done with form-data containing action: media-create-image-subsizes and attachment_id: #### to attempt to complete the action.

If all attempts fail, the last request has _wp_upload_failed_cleanup: true in form-data and attempts a cleanup. This deletes the the attachment post, the uploaded file, and any intermediate sizes that were created. Then, an error message is shown to users advising them to scale down the image and try uploading again.

REST API Uploads

When using the REST API, the initial request is to .../wp-json/wp/v2/media?_locale=user, and retry requests are to .../wp-json/wp/v2/media/####/post-process?_locale=user (where #### is the attachment post ID).

If all attempts to resume post-processing fail with HTTP 5xx errors, the last is a standard “delete attachment” request, sent to .../wp-json/wp/v2/media/####?force=true&_locale=user with X-HTTP-Method-Override: DELETE header. It performs the same cleanup as described for the admin-ajax.php upload method.

Hopefully this helps in understanding how uploads work! If you have any questions, please ask in the comments for clarification.

Thanks to @azaozz for helping write this post!

+make.wordpress.org/hosting

#5-3, #dev-notes, #media

Admin form controls height and alignment standardization in WordPress 5.3.1

WordPress 5.3 introduced some notable Admin CSS changes to improve administration accessibility and consistency with the block editor.

These changes made more obvious that form controls heights and alignments were not consistent across WP Admin.

Before WordPress 5.3, there were already some inconsistencies between form controls. For reference, WordPress 5.2 form controls various alignments and heights are grouped in the screenshot below:

Form controls alignment and height inconsistencies in WordPress 5.2

WordPress 5.3 Admin CSS changes brought more attention to those general inconsistencies on form controls. And several users and contributors reported these inconsistencies as regressions from 5.3.

Example of form controls inconsistencies in WordPress 5.3

Although these inconsistencies cannot be considered as regressions from 5.3 since they existed before 5.3 Admin CSS changes, WordPress 5.3.1 introduces some fixes on heights and alignments.

5.3
5.3.1
5.3
5.3.1
5.3
5.3.1

WordPress 5.3.1 changes include:

  • Standardized height basis of 30px for all form controls
  • Consistent line height basis of 30px across the interface
  • 14px font size basis for select controls option text
  • Remove various top/bottom margin and padding

These changes are part of a continuous effort to improve styling and consistency of all form controls in the WordPress admin pages. In general, plugin authors and WordPress developers are encouraged to:

  • remove any fixed height: flexible heights are now the WordPress recommended standard to allow form controls to better scale with text zoom
  • remove any custom top and bottom padding values
  • remove any custom line-height value
  • remove any custom min-height value
  • update custom focus/hover styles on custom UI components to match the new WordPress focus/hover styles

For full details about these changes, see the related changeset on WordPress Trac. WordPress developers and plugin authors may also want to visit the related Trac ticket: #48420

#5-3-1, #core-css, #dev-notes

Alternate color schemes changes in WordPress 5.3.1

WordPress 5.3 added some noteworthy CSS changes to WordPress Admin. These changes also impacted alternate color schemes, especially concerning secondary buttons.

Indeed, secondary button borders unexpectedly inherited their scheme’s primary color, which resulted in arguable visual aspect and poor readability for most color schemes.

“Blue” alternate color scheme in WordPress 5.3
“Coffee” alternate color scheme in WordPress 5.3
“Midnight” alternate color scheme in WordPress 5.3

A bug also occurred on :active state, where the background color of the button was quite the same than the text color.

On the right: secondary button with :active state on “Blue” color scheme in WordPress 5.3
On the right: secondary button with :active state on “Midnight” color scheme in WordPress 5.3

WordPress 5.3.1 will fix these issues by unifying WP-Admin secondary buttons for all alternate color schemes.

Secondary button styles will be the same for all bundled alternate color schemes. This change also fixes the :active state CSS issue.

Please note this change also introduces some hardcoded colors for both gray borders and texts. Plugin authors who provide specific support for alternate color schemes can use these new hardcoded colors.

New secondary button CSS/Sass styles in WP 5.3.1:

.button {
	border-color: #7e8993;
	color: #32373c;
}

.button:hover {
	border-color: darken( #7e8993, 5% );
	color: darken( #32373c, 5% );
}

.button:focus {
	border-color: #7e8993;
	color: darken( #32373c, 5% );
	box-shadow: 0 0 0 1px #32373c;
}

See /wp-admin/css/colors/_admin.scss.

New CSS styles for secondary buttons in alternate color schemes:

Secondary buttons in WordPress 5.3.1
“Midnight” alternate color scheme in WP 5.3.1
“Coffee” alternate color scheme in WP 5.3.1

For reference, see the related Trac tickets: #48585 and #48598.

#5-3-1, #accessibility, #core-css, #dev-notes

Twenty Twenty: animated scroll changes in WordPress 5.3.1

In WordPress 5.3, Twenty Twenty new bundled theme added smooth scroll animations to anchor links. These animations were handled by native JavaScript and it caused several issues, mentioned in the following Trac tickets and GitHub issues:

  • #48763 – Twenty Twenty: SmoothScroll is broken
  • #48551 – Twenty Twenty: Replace JS-based smooth scroll with CSS
  • #48866 – TwentyTwenty: Paginated comments don’t work
  • GitHub issue 476 – Consider removing JS-based smooth scroll

Additionally to the multiple issues listed in the above tickets, JavaScript-based scroll animations add a bunch of relatively complex JS code, override natural anchor behavior and may interfere with how specific user agents handle in-page scrolling.

In WordPress 5.3.1, the current smooth scroll JavaScript implementation will be replaced with “scroll-behavior” CSS property.

This change fixes the issues encountered with the current JavaScript implementation, and also includes an accessibility enhancement by using prefers-reduced-motion: reduce media query property for users that have opted in to reduced motion in their browser settings.

For further explanation on this media query, see Mozilla Developer Network documentation.

Browsers that don’t support scroll-behavior CSS property will fallback to default HTML anchor behavior. For reference, see full browsers support for this CSS property on MDN.

New CSS scroll animation implementation in Twenty Twenty:

html {
    scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
    html {
        scroll-behavior: auto;
    }
}

Developers are able to remove scroll-behavior effect on specific elements by using a class CSS selector, as follows:

.disable-smooth-scrolling {
    scroll-behavior: auto;
}

For reference, see the related changeset in 5.3.1 branch.

#5-3-1, #accessibility, #bundled-theme, #core-css, #dev-notes, #twentytwenty

Noteworthy Admin CSS changes in WordPress 5.3

WordPress 5.3 will introduce a number of CSS changes in WordPress admin. While the necessity to improve wp-admin accessibility was previously raised in several Trac tickets, Gutenberg’s recent interface improvements made it necessary to improve the whole interface as well.

Background: in April 2019, WP-Campus conducted an accessibility audit of the new editor interface, made by an independent contractor, Tenon LLC. This audit raised issues in the editor but also in the media modal, which uses wp-admin styles. Fixing these issues on Gutenberg and on the media modal but not in the whole wp-admin interface would have been very inconsistent.

Some tickets were milestoned to the 5.3 release cycle to start backporting Gutenberg accessibility improvements to the whole admin interface. These first tickets aim to improve:

  • Color contrasts on form fields and buttons
  • Focus styles on form fields and buttons
  • Content behavior on text zoom

Backporting some of Gutenberg’s styles to fix these issues introduced some visual issues with the interface elements hierarchy. Therefore, Design and Accessibility teams worked on the overall visual hierarchy:

  • darker tables and metaboxes borders were introduced for a better hierarchy between interface elements

Note for plugin authors and WordPress developers

These changes are only CSS changes, and not structural changes, so the HTML markup is exactly the same as before, with the same class attributes on each element.

In short, your styles should align with these changes if interface elements are not overridden by custom CSS. If you are overriding WordPress Admin CSS on form elements, you should test your plugins or your custom developments against WordPress 5.3 RC 1.

If you are a plugin author, there are different use cases:

  • Plugins that are using default Admin CSS styles should work just like before.
  • Plugins that are using custom Admin CSS styles by overriding default Admin CSS should be checked against 5.3.
  • Plugins that are using fully customized Admin CSS styles should not be concerned by those changes.

In general, plugin authors and WordPress developers are encouraged to:

  • remove any fixed heights: flexible heights are the WordPress recommended standard (and one of the main goals of the Admin CSS changes).
  • remove any custom top and bottom padding values.
  • remove any custom line-height values.
  • update their CSS code to override new focus/hover buttons colors if they use custom colors on this type of element.

In the next section of this dev note, you’ll find some noteworthy CSS changes coming in WordPress 5.3.

Main things that are changing in 5.3:

  • Forms fields: 
    • text inputs
    • textareas
    • selects
    • checkboxes
    • radiobuttons
    • both primary and secondary buttons
    • colorpickers
  • Tables, notifications and metaboxes

Known issues

Available for testing in WordPress 5.3 RC 1, these changes have been tested in various use cases and no breakage situation was identified during the tests. Please check the report for full information about the testing panel.

This is a work in progress, just like anything in WordPress Core. These usability improvements were implemented during summer 2019 then tested and iterated on September and October. After 5.3 is released, the idea is to iterate on wp-admin design to make it fully consistent with the editor interface, and to provide a great and accessible editorial experience for websites administrators. The next minor releases will fix small issues with 5.3 changes and the next majors will improve the consistency of user experiences between Gutenberg and WordPress administration.

Changes related to form fields

Text inputs

Legacy CSS code:

/* Legacy styles */
input[type=text] {
    border: 1px solid #ddd;
    box-shadow: inset 0 1px 2px rgba(0,0,0,.07);
    background-color: #fff;
    color: #32373c;
    outline: 0;
    transition: 50ms border-color ease-in-out;
}
input[type=text]:focus {
    border-color: #5b9dd9;
    box-shadow: 0 0 2px rgba(30,140,190,.8);
    outline: 2px solid transparent;
}
Legacy input style
Legacy focused input style

New CSS code:

/* New styles */
input[type=text] {
    padding: 0 8px;
    line-height: 2;
    min-height: 30px;
    box-shadow: 0 0 0 transparent;
    border-radius: 4px;
    border: 1px solid #7e8993;
    background-color: #fff;
    color: #32373c;
}
input[type=text]:focus {
    border-color: #007cba;
    box-shadow: 0 0 0 1px #007cba;
    outline: 2px solid transparent;
}
New input style
New focused input style

Textareas

Legacy CSS code:

/* Legacy styles */
textarea {
    border: 1px solid #ddd;
    box-shadow: inset 0 1px 2px rgba(0,0,0,.07);
    background-color: #fff;
    color: #32373c;
    outline: 0;
    transition: 50ms border-color ease-in-out;
}
textarea:focus {
    border-color: #5b9dd9;
    box-shadow: 0 0 2px rgba(30,140,190,.8);
    outline: 2px solid transparent;
}
Legacy textarea style
Legacy focused textarea style

New CSS code:

/* New styles */
textarea {
    box-shadow: 0 0 0 transparent;
    border-radius: 4px;
    border: 1px solid #7e8993;
    background-color: #fff;
    color: #32373c;
}
textarea:focus {
    border-color: #007cba;
    box-shadow: 0 0 0 1px #007cba;
    outline: 2px solid transparent;
}
New textarea style
New focused textarea style

Selects

Legacy CSS code:

/* Legacy styles */
select {
    padding: 2px;
    line-height: 28px;
    height: 28px;
    vertical-align: middle;
    border: 1px solid #ddd;
    box-shadow: inset 0 1px 2px rgba(0,0,0,.07);
    background-color: #fff;
    color: #32373c;
    outline: 0;
    transition: 50ms border-color ease-in-out;
}
select:focus {
    border-color: #5b9dd9;
    box-shadow: 0 0 2px rgba(30,140,190,.8);
    outline: 2px solid transparent;    
}
Legacy select style
Legacy focused select style

New CSS code:

select {
    font-size: 14px;
    line-height: 1.28571428;
    color: #32373c;
    border: 1px solid #7e8993;
    border-radius: 3px;
    box-shadow: none;
    padding: 4px 24px 4px 8px;
    min-height: 30px;
    max-width: 25rem;
    vertical-align: middle;
    -webkit-appearance: none;
    background: #fff url(data:image/svg+xml;charset=US-ASCII,<svg image>) no-repeat right 5px top 55%;
    background-size: 16px 16px;
    cursor: pointer;
}
select:focus {
    border-color: #007cba;
    color: #016087;
    box-shadow: 0 0 0 1px #007cba;
}
New select style
New focused select style

Radiobuttons

Legacy CSS code:

/* Legacy styles */
input[type=radio] {
    border: 1px solid #b4b9be;
    border-radius: 50%;
    background: #fff;
    color: #555;
    clear: none;
    cursor: pointer;
    display: inline-block;
    line-height: 0;
    height: 16px;
    margin: -4px 4px 0 0;
    outline: 0;
    padding: 0!important;
    text-align: center;
    vertical-align: middle;
    width: 16px;
    min-width: 16px;
    -webkit-appearance: none;
    box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
    transition: .05s border-color ease-in-out;
}
input[type=radio]:checked:before {
    content: "\2022";
    text-indent: -9999px;
    border-radius: 50px;
    font-size: 24px;
    width: 6px;
    height: 6px;
    margin: 4px;
    line-height: 16px;
    background-color: #1e8cbe;
}
Legacy radio buttons styles

New CSS code:

/* New styles */
input[type=radio] {
	border: 1px solid #7e8993;
	border-radius: 50%;
	line-height: .71428571;
	background: #fff;
	color: #555;
	clear: none;
	cursor: pointer;
	display: inline-block;
	height: 1rem;
	margin: -.25rem .25rem 0 0;
	outline: 0;
	padding: 0 !important;
	text-align: center;
	vertical-align: middle;
	width: 1rem;
	min-width: 1rem;
	-webkit-appearance: none;
	box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
	transition: .05s border-color ease-in-out;
}
input[type=radio]:checked::before {
	content: "";
	border-radius: 50%;
	width: .5rem;
	height: .5rem;
	margin: .1875rem;
	background-color: #1e8cbe;
	line-height: 1.14285714;
}
New radio buttons styles

Checkboxes

Legacy CSS code:

/* Legacy styles */
input[type=checkbox] {
	border: 1px solid #b4b9be;
	background: #fff;
	color: #555;
	clear: none;
	cursor: pointer;
	display: inline-block;
	line-height: 0;
	height: 16px;
	margin: -4px 4px 0 0;
	outline: 0;
	padding: 0!important;
	text-align: center;
	vertical-align: middle;
	width: 16px;
	min-width: 16px;
	-webkit-appearance: none;
	box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
	transition: .05s border-color ease-in-out;
}
input[type=checkbox]:checked:before {
	content: "\f147";
	margin: -3px 0 0 -4px;
	color: #1e8cbe;
}
input[type=checkbox]:focus {
	border-color: #5b9dd9;
	box-shadow: 0 0 2px rgba(30,140,190,.8);
	outline: 2px solid transparent;
}
Legacy checkbox styles
Legacy focused checkbox style

New CSS code:

/* New styles */
input[type=checkbox], input[type=radio] {
	border: 1px solid #7e8993;
	border-radius: 4px;
	background: #fff;
	color: #555;
	clear: none;
	cursor: pointer;
	display: inline-block;
	line-height: 0;
	height: 1rem;
	margin: -.25rem .25rem 0 0;
	outline: 0;
	padding: 0!important;
	text-align: center;
	vertical-align: middle;
	width: 1rem;
	min-width: 1rem;
	-webkit-appearance: none;
	box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
	transition: .05s border-color ease-in-out;
}
input[type=checkbox]:checked::before {
	content: url(data:image/svg+xml;utf8,<svg image>);
	margin: -.1875rem 0 0 -.25rem;
	height: 1.3125rem;
	width: 1.3125rem;
}
input[type=checkbox]:focus {
	border-color: #007cba;
	box-shadow: 0 0 0 1px #007cba;
	outline: 2px solid transparent;
}
New checkbox style
New focused checkbox style

Buttons

Legacy CSS code:

/* 
 * Legacy styles for buttons
 */

/* General */
.wp-core-ui .button, .wp-core-ui .button-primary, .wp-core-ui .button-secondary {
	display: inline-block;
	text-decoration: none;
	font-size: 13px;
	line-height: 26px;
	height: 28px;
	margin: 0;
	padding: 0 10px 1px;
	cursor: pointer;
	border-width: 1px;
	border-style: solid;
	-webkit-appearance: none;
	border-radius: 3px;
	white-space: nowrap;
	box-sizing: border-box;
}

/* Primary buttons styles */
.wp-core-ui .button-primary {
	background: #0085ba;
	border-color: #0073aa #006799 #006799;
	box-shadow: 0 1px 0 #006799;
	color: #fff;
	text-decoration: none;
	text-shadow: 0 -1px 1px #006799, 1px 0 1px #006799, 0 1px 1px #006799, -1px 0 1px #006799;
}
/* Primary buttons :hover styles */
.wp-core-ui .button-primary.focus, .wp-core-ui .button-primary.hover, .wp-core-ui .button-primary:focus, .wp-core-ui .button-primary:hover {
	background: #008ec2;
	border-color: #006799;
	color: #fff;
}
/* Primary buttons :focus styles */
.wp-core-ui .button-primary.focus, .wp-core-ui .button-primary:focus {
	box-shadow: 0 1px 0 #0073aa, 0 0 2px 1px #33b3db;
}

/* Secondary buttons styles */
.wp-core-ui .button, .wp-core-ui .button-secondary {
	color: #555;
	border-color: #ccc;
	background: #f7f7f7;
	box-shadow: 0 1px 0 #ccc;
	vertical-align: top;
}
/* Secondary buttons :hover styles */
.wp-core-ui .button-secondary:focus, .wp-core-ui .button-secondary:hover, .wp-core-ui .button.focus, .wp-core-ui .button.hover, .wp-core-ui .button:focus, .wp-core-ui .button:hover {
	background: #fafafa;
	border-color: #999;
	color: #23282d;
}

/* Secondary buttons :focus styles */
.wp-core-ui .button-secondary:focus, .wp-core-ui .button.focus, .wp-core-ui .button:focus {
	border-color: #5b9dd9;
	box-shadow: 0 0 3px rgba(0,115,170,.8);
}
Legacy style for primary button
Legacy style for focused primary button
Legacy style for secondary button
Legacy style for focused secondary button

New CSS code:

/* 
 * New styles for buttons
 */

/* General */
.wp-core-ui .button, .wp-core-ui .button-primary, .wp-core-ui .button-secondary {
	display: inline-block;
	text-decoration: none;
	font-size: 13px;
	line-height: 2.15384615;
	min-height: 30px;
	margin: 0;
	padding: 0 10px;
	cursor: pointer;
	border-width: 1px;
	border-style: solid;
	-webkit-appearance: none;
	border-radius: 3px;
	white-space: nowrap;
	box-sizing: border-box;
}

/* Primary buttons styles */
.wp-core-ui .button-primary {
	background: #007cba;
	border-color: #007cba;
	color: #fff;
	text-decoration: none;
	text-shadow: none;
}
/* Primary buttons :hover styles */
.wp-core-ui .button-primary.focus, .wp-core-ui .button-primary.hover, .wp-core-ui .button-primary:focus, .wp-core-ui .button-primary:hover {
	background: #0071a1;
	border-color: #0071a1;
	color: #fff;
}
/* Primary buttons :focus styles */
.wp-core-ui .button-primary.focus, .wp-core-ui .button-primary.hover, .wp-core-ui .button-primary:focus, .wp-core-ui .button-primary:hover {
	background: #0071a1;
	border-color: #0071a1;
	color: #fff;
}

/* Secondary buttons styles */
.wp-core-ui .button, .wp-core-ui .button-secondary {
	color: #0071a1;
	border-color: #0071a1;
	background: #f3f5f6;
	vertical-align: top;
}
/* Secondary buttons :hover styles */
.wp-core-ui .button-secondary:hover, .wp-core-ui .button.hover, .wp-core-ui .button:hover {
	background: #f1f1f1;
	border-color: #016087;
	color: #016087;
}
/* Secondary buttons :focus styles */
.wp-core-ui .button-secondary:focus, .wp-core-ui .button.focus, .wp-core-ui .button:focus {
	background: #f3f5f6;
	border-color: #007cba;
	color: #016087;
	box-shadow: 0 0 0 1px #007cba;
	outline: 2px solid transparent;
	outline-offset: 0;
}
New style for primary button
New style for focused primary button
New style for secondary button
Legacy style for focused secondary button

Darker borders on tables, notices, metaboxes and other similar elements

These changes introduce better contrast for borders for the following user interface elements:

  • Tables
  • Screen Options and Help
  • Admin notices
  • Welcome panel
  • Meta boxes (post boxes on classic editor or in edit attachment screens)
  • Cards
  • Health Check accordions and headings
  • Theme and Plugin upload forms

Legacy CSS code:

Depending on the related element, several CSS declarations were used.

border: 1px solid #e5e5e5;
border: 1px solid #e1e1e1;
border: 1px solid #eee;
border: 1px solid #ddd;
border: 1px solid #e2e4e7;
Legacy card with light grey border
Legacy table with light grey border
Legacy Screen options panel with light grey border
Legacy notice with light grey border

New CSS code:

All those CSS declaration are replaced with a unique color:

border: 1px solid #ccd0d4;
New card with darker border
New table with darker border
Legacy Screen options panel with darker border
New notice with darker border

Related Trac tickets

  • #47153: Field boundaries have insufficient color contrast
  • #34904: The design of the focus outline on buttons/elements could be improved
  • #47477: Content overflows and is cut off at 200% text enlarge
  • #47498: Revise checkbox/radio button css for better compatibility with text zoom
  • #48101: Use darker table + card borders across WP-Admin

#5-3, #accessibility, #dev-notes

WordPress 5.3: Site Admin Email Verification Screen

In WordPress 5.3, a new screen has been introduced to help ensure the site’s administration email remains accurate and up to date. The site’s admin email (as defined when installing WordPress, and found on the Settings > General page) is a critical part of every WordPress site. This new screen will help site owners remain in full control of their site, even as years go by.

How does this work?

By default, administrators will see a screen after logging in that lists the site’s admin email address once every 6 months.

They are presented with 4 actions:

  1. The site’s email is verified as correct: After clicking “The email is correct” button, the user is taken to the Dashboard with an admin notice saying “Thank you for verifying”. The screen will be hidden for 6 months from all administrators.
  2. The site’s email needs to be changed: After clicking the “Update” button, the user is taken to the Settings > General page where they can update the site’s email address. Administrators will be presented with the verification screen the next time they log in.
  3. The user clicks “Remind me later”: the user is taken to the Dashboard. Administators will see the screen again after 3 days have passed.
  4. Back to “Site Name”: When this link is clicked, the user will be taken to the site’s home page. Administrators will be presented with the verification screen the next time they log in.

Available Actions and Filters

Actions

There are a few action hooks on the verification page that developers can use to customize the screen.

  • admin_email_confirm – Fires before the admin email confirm form.
  • admin_email_confirm_form – Fires inside the admin-email-confirm-form form tag, but before any other output.

A new action hook after the form was not introduced. Instead, use the login_footer action, which is called just after the closing </form> tag. The if ( 'confirm_admin_email' === $_GET['action'] ) conditional can be used to check that the email verification screen is being shown.

Filters

One new filter, admin_email_check_interval, has also been introduced. This filter can be used to change the frequency that administrators should see the verification screen.

The following example changes the interval from the default of 6 months to 2 months:

<?php
function myplugin_admin_email_check_interval( $interval ) {
	return 2 * MONTH_IN_SECONDS;
}
add_filter( 'admin_email_check_interval', 'myplugin_admin_email_check_interval' );

Note: The returned value should always be in seconds.

This filter can also be used to disable the feature by returning a “falsey” value, such as 0, or false.

The following example will disable the admin email verification check:

<?php
add_filter( 'admin_email_check_interval', '__return_false' );

For more information about these changes, check out #46349 in Trac.

Props to @desrosj for peer review.

#5-3, #admin, #dev-notes

The REST API in WordPress 5.3

WordPress 5.3 contains a number of REST API improvements designed to make it easier and faster to work with API data from the block editor or other client applications.

Register Array & Object Metadata

As covered previously in this developer note on array & object metadata, it is now possible to use register_post_meta & register_term_meta (as well as the underlying method register_meta) to interact with complex meta values as schema-validated JSON arrays or objects using the REST API. See the linked post for more details.

Nested response filtering with _fields query parameter

This developer note on the changes to the REST API’s _fields= query parameter shows how you may now filter your REST API response objects to include only specific nested properties within the response body.

Ticket #42094

Set drafts back to “floating date” status

Once a date has been set for a draft, it was previously impossible to set the post back to showing “publish immediately” (also referred to as a “floating date,” where the post will be dated whenever it is published). As of 5.3, passing null for a date value will unset the draft date and restore this floating state.

Ticket #39953

Faster Responses

We have introduced a caching wrapper around the generation of REST resource schema objects, which initial testing has shown to yield up to a 30-40% performance increase in large API responses. If you work with expensive or large REST API queries, things should be quite a bit faster now. (Ticket #47871)

The REST API has also been improved to avoid unnecessary controller object instantiation (#45677) and to skip generation of sample permalinks when that data is not requested (#45605).

Please Note: if your team has existing performance benchmarking tooling for the REST API, please contact the component maintainers in the #core-restapi Slack channel; we very much desire to expand our metrics in this area.

Additional Changes of Note

In addition to these key enhancements, there are a number of smaller improvements to the REST API which may be of interest to developers.

  • It is no longer possible to DELETE a Revision resource using the REST API, as this behavior could break a post’s audit trail. Ticket #43709
  • The /search endpoint will now correctly embed the full original body of each matched resource when passing the _embed query parameter. Ticket #47684
  • rest_do_request and rest_ensure_request now accept a string API path, so it is possible to instantiate a request in PHP using nothing more than the desired endpoint string, e.g.
    rest_do_request( '/wp/v2/posts' );
    Ticket #40614
  • Creating or updating a Terms resource via the REST API now returns the updated object using the “edit” context. Ticket #41411
  • It is now possible to edit a posted comment through the REST API when authenticating the request as a user with the moderate_comments capability. Previously a full editor- or admin-level role was needed. Ticket #47024
  • rest_get_avatar_urls now receives the entire User or Comment object, not just the object’s email address. Ticket #40030

Welcoming Timothy Jacobs as a REST API component maintainer

Last but not least, many of you have no doubt seen @timothyblynjacobs active in trac, slack, and community events. Timothy has driven much of the momentum that resulted in the above improvements, and I’m excited to (belatedly) announce that he has joined the REST API team as an official component maintainer. Thank you very much for your energy and dedication!

Thank you also to every other person who contributed to API changes this cycle; it’s the best version of the REST API yet, and we couldn’t have done it without the dozens of contributors who helped create, review and land these patches.

We’ve got some ambitious ideas about how we can make the REST API even better in 5.4. Interested in helping out, with code, docs, or triage? Join us for weekly office hours, every week on Thursdays at 1800 UTC!

#5-3, #dev-notes, #rest-api

PHP Native JSON Extension Now Required

The PHP native JSON extension has been bundled and compiled with PHP by default since 5.2.0 (2006). However, a significant number of PHP installs did not include it. In order to ensure a consistent experience for JSON related functionality in all supported versions of PHP, WordPress Core has historically included a large number of workarounds, functions, and polyfills.

In 2011 (WordPress 3.2), an attempt was made to remove JSON related compatibility code. However, it was discovered that a fair number of distributions were still missing the PHP JSON extension by default, and the removed code was restored to ensure compatibility.

In WordPress 5.2, the minimum version of PHP supported was raised from 5.2.6 to 5.6.20. In the 8 year period since the last attempt was made to encourage use of the PHP native JSON extension, the number of distributions with this extension disabled has significantly decreased.

Because of this, the PHP native JSON extension is now required to run WordPress 5.3 and higher.

To prevent compatibility issues, a site that does not have the PHP native JSON extension enabled will see an error message when attempting to upgrade to WordPress 5.3. The update will be cancelled and the site will remain on the current version (see [46455]). This is to prevent potential compatibility issues on servers running custom PHP configurations.

Here’s a summary of what has changed.

Deprecated

The following functions and classes will remain in the code base, but will trigger a deprecated warning when used (see [46205]):

  • The Services_JSON and Services_JSON_Error classes and all methods
  • The wp-includes/class-json.php file
  • The (private) _wp_json_prepare_data() function

Removed

The following functions, and classes have been removed entirely from the code base (see the [46208] changeset):

  • json_encode() function polyfill
  • json_decode() function polyfill
  • _json_decode_object_helper() function polyfill
  • json_last_error_msg() polyfill
  • JsonSerializable interface polyfill
  • $wp_json global variable
  • JSON_PRETTY_PRINT constant polyfill
  • JSON_ERROR_NONE constant polyfill

Unchanged

The wp_json_encode() function will remain with no intention to deprecate it at this time. This function includes an extra sanity check for JSON encoding data and should still be used as the preferred way to encode data into JSON.

For more information about these changes, check out #47699 on Trac and the relevant changesets ([46205], [46206], [46208], [46377], and [46455]).

Props @jrf & @jorbin for peer review.

#5-3, #dev-notes, #php

Enhancements to the Network Sites Screen in WordPress 5.3

Changes to the database

The introduction of Site metadata in WordPress 5.1 has opened up a lot of new possibilities for multisite.

Save database version and date updated in multisite site meta

In [46193], the database version and the updated dates are now stored in the blogmeta table.

If your setup of multisite requires the database version to be accessed from a global context, instead of looping around every site with an expensive switch_to_blog call to get_option( 'db_version' ), you maybe want to try a function like the following.

function get_site_versions() {
	global $wpdb;
	$query = $wpdb->prepare( "SELECT blog_id, meta_value FROM $wpdb->blogmeta WHERE meta_key = 'db_version' ORDER BY blog_id DESC");
	return $wpdb->get_results( $query );
}

Remove blog_versions table

Currently, there is a table in multisite called blog_versions. This table stores the database version as a number and the updated date. It was introduced in #11644 and has never been used in Core since then.

With the database version and updated date now stored in theblogmeta table, blog_versionstable becomes redundant. In [46194], this table is removed from Core.

Changes to WP_MS_Sites_List_Table

WordPress 5.3 adds several enhancements to the WP_MS_Sites_List_Table class that allows plugin authors to take advantage of Site metadata to provide a richer experience for multisite administrators on the Network Admin Sites screen.

These enhancements will be very familiar to those who have used and/or customized the All Posts screen.

Site Status Views

The Network Sites screen now displays a list of links with the counts of Sites by status (e.g., Public, Spam, etc.), similar to the post status links on the All Posts screen.

Network Sites screen showing site status views.

The status links can also be filtered with the new views_sites-network filter, introduced in Trac ticket #37392.

For example, imagine there is a multisite where the main site acts as a directory of local restaurants and each separate site is for an individual restaurant, and restaurant owners can purchase a “subscription” that would allow them to display more information about their restaurant listing: a basic subscription would allow them to add photographs of their restaurant and an advanced subscription would additionally allow them to include their menu.

The subscription level could then be stored in the blogmeta table and “status” links can be added for the different subscription levels as follows:

add_filter( 'views_sites-network', 'myplugin_add_site_status_views' );
function myplugin_add_site_status_views( $view_links ) {
	$statuses = array(
		'free'      => _n_noop(
			'Free <span class="count">(%s)</span>',
			'Free <span class="count">(%s)</span>',
			'myplugin'
		),
		'basic'   => _n_noop(
			'Basic <span class="count">(%s)</span>',
			'Basic <span class="count">(%s)</span>',
			'myplugin'
		),
		'advanced'   => _n_noop(
			'Advanced <span class="count">(%s)</span>',
			'Advanced <span class="count">(%s)</span>',
			'myplugin'
		),
	);

	// get the count of sites with each of our custom statuses.
	$args = array(
		'meta_query' => array(
			array(
				'key'     => 'myplugin-status',
				'compare' => '=',
			),
		),
		'count' => true,
	);
	$counts = array();
	foreach ( array_keys( $statuses ) as $status ) {
		$args['meta_query'][0]['value'] = $status;
		$counts[ $status ] = get_sites( $args );
	}

	$requested_status = isset( $_GET['status'] ) ? wp_unslash( trim( $_GET['status'] ) ) : '';

	foreach ( $statuses as $status => $label_count ) {
		$current_link_attributes = $requested_status === $status ?
			' class="current" aria-current="page"' :
			'';
		if ( (int) $counts[ $status ] > 0 ) {
			$label = sprintf( translate_nooped_plural( $label_count, $counts[ $status ] ), number_format_i18n( $counts[ $status ] ) );

			$view_links[ $status ] = sprintf(
				'<a href="%1$s"%2$s>%3$s</a>',
				esc_url( add_query_arg( 'status', $status, 'sites.php' ) ),
				$current_link_attributes,
				$label
			);
		}
	}

	return $view_links;
}

When a user clicks on one of the custom status links, the rows in the list table can be limited to those sites with that specific custom Status using the existing ms_sites_list_table_query_args as follows:

add_filter( 'ms_sites_list_table_query_args', 'myplugin_sites_with_custom_status' );
function myplugin_sites_with_custom_status( $args ) {
	$status = ! empty( $_GET['status' ] ) ? wp_unslash( $_GET['status' ] ) : '';

	if ( empty( $status ) || ! in_array( $_GET['status'], array( 'free', 'basic', 'advanced' ) ) ) {
		return $args;
	}

	$meta_query = array(
		'key'   => 'myplugin-status',
		'value' => $status,
	);

	if ( isset( $args['meta_query'] ) ) {
		// add our meta query to the existing one(s).
		$args['meta_query'] = array(
			'relation' => 'AND',
			$meta_query,
			array( $args['meta_query'] ),
		);
	}
	else {
		// add our meta query.
		$args['meta_query'] = array(
			$meta_query,
		);
	}

	return $args;
}

Extra Tablenav

The posts displayed on the All Posts screen can be filtered by date and category. Plugins can also add custom filter criteria with the restrict_manage_posts filter.

To continue with the above restaurant guide example, imagine the food served by each restaurant is also stored in the blogmeta table. We could then allow Network administrators to filter the Sites by the type of food by adding a dropdown of the various cuisines:

Network Sites screen that includes a dropdown added with the "restrict_manage_sites" action.

Dropdowns like this can now be added on the Network Sites screen with the new restrict_manage_sites action ( introduced in Trac ticket #45954), as follows:

add_action( 'restrict_manage_sites', 'myplugin_add_cuisines_dropdown' );
function myplugin_add_cuisines_dropdown( $which ) {
	if ( 'top' !== $which ) {
		return;
	}

	echo '<select name="cuisine">';
	printf( '<option value="">%s</option>', __( 'All cuisines', 'myplugin' ) );

	$cuisines = array(
		'French'  => __( 'French', 'myplugin' ),
		'Indian'  => __( 'Indian', 'myplugin' ),
		'Mexican' => __( 'Mexican', 'myplugin' ),
	);
	
	$requested_cuisine = isset( $_GET['cuisine'] ) ? wp_unslash( $_GET['cuisine'] ) : '';
	
	foreach ( $cuisines as $cuisine => $label ) {
		$selected = selected( $cuisine, $requested_cuisine, false );
		printf( '<option%s>%s</option>', $selected, $label );
	}

	echo '</select>';

	return;
}

When a user selects a food type and clicks the Filter button, the rows in the list table can be limited to just those Sites that serve that cuisine using the existing ms_sites_list_table_query_args filter, as follows:

add_filter( 'ms_sites_list_table_query_args', 'myplugin_sites_with_cuisine' );
function myplugin_sites_with_cuisine( $args ) {
	if ( empty( $_GET['cuisine' ] ) ) {
		return $args;
	}

	$meta_query = array(
		'key'   => 'myplugin-cuisine',
		'value' => wp_unslash( $_GET['cuisine' ] ),
	);

	if ( isset( $args['meta_query'] ) ) {
		// add our meta query to the existing one(s).
		$args['meta_query'] = array(
			'relation' => 'AND',
			$meta_query,
			array( $args['meta_query'] ),
		);
	}
	else {
		// add our meta query.
		$args['meta_query'] = array(
			$meta_query,
		);
	}

	return $args;
}

Site Display States

As with other list tables, each row in the Sites list table can now have display states. By default, all Site statuses (other than Public) of each Site are included as display states. Additionally, the main Site for the Network also has the “Main” display state.

Network Sites screen showing display states added with the "display_site_states" filter.

When a specific Site status view has been selected by the user, that status will not be among the display states (this is just like the All Posts screen).

Plugins can also modify the display states with the new display_site_states filter, introduced in Trac ticket #37684.

To further continue our restaurant guide example, we can add our custom statuses and the cuisine served at each restaurant as display states. This can be achieved as follows:

add_filter( 'display_site_states', 'site_display_states', 10, 2 );
function site_display_states( $display_states, $site ) {
	$status = get_site_meta( $site->blog_id, 'myplugin-status', true );
	$requested_status = isset( $_GET['status'] ) ? wp_unslash( trim( $_GET['status'] ) ) : '';

	if ( $status !== $requested_status ) {
		switch ( $status ) {
			case 'free':
				$display_states['free']     = __( 'Free', 'myplugin' );
				break;
			case 'basic':
				$display_states['basic']    = __( 'Basic', 'myplugin' );
				break;
			case 'advanced':
				$display_states['advanced'] = __( 'Advanced', 'myplugin' );
				break;
		}
	}

	$cuisine = get_site_meta( $site->blog_id, 'myplugin-cuisine', true );
	$requested_cuisine = isset( $_GET['cuisine'] ) ? wp_unslash( trim( $_GET['cuisine'] ) ) : '';

	if ( $cuisine !== $requested_cuisine ) {
		$display_states[ $cuisine ] = $cuisine;
	}

	return $display_states;
}

Misc changes

Return for short circuits for multisite classes.

Fixing a bug created in the [44983] original patch, introduced pre query filters in multisite classes. This bug made short circuit act differently from other short circuits and as it still continues to execute. Now after the filter networks_pre_query and sites_pre_query run, the code will exit straight away. This allows developers to completely hot-wire the network and site query, to load from another source, such as a different cache or Elastic search.

Improved performance for site and network lookups by ID

In earlier versions of WordPress when running the code get_site( 12345 ) was run and no ID with that site exists, the result is not being cached. That means every subsequent lookup will still cause a DB query to be fired, which is unnecessary. In [45910] non-existent sites data is stored as -1 instead of false to save further database lookups.

#5-3, #dev-notes, #multisite, #networks-sites

Expanded meta key comparison operators in 5.3

WordPress 5.1 introduced the compare_key parameter for WP_Meta_Query, allowing developers to perform LIKE queries against postmeta keys. (See #42409 and the related dev note.) WordPress 5.3 expands the options available to compare_key, so that developers have access to meta-key comparison operators similar to those available for meta values. See #43446 and [46188].

After this change, compare_key accepts the following operators: =, LIKE, !=, IN,NOT IN,NOT LIKE,RLIKE,REGEXP,NOT REGEXP,EXISTS, andNOT EXISTS. Usage notes:

  • For parity with value operators, we’ve added support for EXISTS and NOT EXISTS. In the case of compare_key, these map to = and !=, respectively.
  • MySQL regular expression comparison operators (RLIKE, REGEXP, NOT REGEXP) are case-insensitive by default. To perform case-sensitive regular expression matches, it’s necessary to cast to BINARY. To support this, WP_Meta_Query now accepts a type_key parameter; pass 'BINARY' to force case-sensitive comparisons. (This directly parallels the use of type when using regular expressions to match meta value.)
  • As is the case with other WP_Meta_Query-related parameters, the parameters discussed here are available to WP_Query using the meta_ prefix: meta_compare_key and meta_type_key.

#5-3, #dev-notes, #query