Opened 4 days ago
Last modified 3 days ago
#66216 new defect (bug)
Ensure valid HTML when using the myblogs_options filter
| Reported by: | afercia | Owned by: | |
|---|---|---|---|
| Priority: | normal | Milestone: | Awaiting Review |
| Component: | Networks and Sites | Version: | |
| Severity: | normal | Keywords: | has-patch |
| Cc: | Focuses: | multisite |
Description
Discovered while reviewing #66205 / PR 13819
In the Network admin, in the my-sites.php page, there are two filters (technically it's the same filter with a different 'context'):
$settings_html = apply_filters( 'myblogs_options', '', 'global' );
and
echo apply_filters( 'myblogs_options', '', $user_blog );
They are meant to pass additional HTML to be printed on the page.
1
The first filter prints the additional HTML above the sites 'cards'. It also prints a hardcoded heading 'Global Settings'. However, the filter is placed right after a <ul> opening tag. As such, when the filter is in use, the heading and the passed HTMl are printed out as children of the <ul> element. That's invalid HTML. It may also make assistive technology confused when announcing the count of the items within the list. Example:
<ul class="my-sites striped">
<h2>Global Settings</h2> {any additional passed HTML here}
2
The second filter is meant to print additional HTML inside the sites 'cards'. It is placed inside a <li> tag. List items can contain almost any kind of elements so valid HTML isn't a big concern in this case. However, the filter documentation doesn't mention in any way that the passed HTML will be printed out inside a list item, it only references the first filter documentation. Given the actual usage of these filters renders HTML in different ways, the documentation should make clear what kind of HTML is expected and how to ensure it is valid.
Change History (3)
This ticket was mentioned in PR #13847 on WordPress/wordpress-develop by @therssoftware.
4 days ago
#1
- Keywords has-patch added
This ticket was mentioned in PR #13849 on WordPress/wordpress-develop by AbdiTolesa.
4 days ago
#2
Fixes: https://core.trac.wordpress.org/ticket/66216
Summary
In wp-admin/my-sites.php, the myblogs_options filter is used in two places with different contexts:
apply_filters( 'myblogs_options', '', 'global' )— meant to print additional HTML above the site list.apply_filters( 'myblogs_options', '', $user_blog )— meant to print additional HTML inside each site's list item.
Two issues:
- The 'global' context heading (
<h2>Global Settings</h2>) and filtered HTML were printed as the first children of<ul class="my-sites striped">instead of before it. When the filter is in use, this produces invalid HTML (block-level content as a direct child of<ul>) and can confuse assistive technology when announcing the list's item count. - The filter's documentation only describes the 'global' context. It doesn't mention that, for a site-specific context, the returned markup is printed inside that site's
<li>, so authors had no way to know their markup needed to be valid list-item content.
Changes
- Moved the Global Settings heading/HTML output above the
<ul>so the list only ever contains<li>children. - Expanded the
myblogs_optionsdoc block to explain where each context's markup is printed and what's safe to return.
Test plan
- [x]
php -l src/wp-admin/my-sites.php - [ ] Manually verify in a Multisite network admin: add a
myblogs_optionsfilter, confirm heading/HTML render above the<ul>and inside each site's<li>as expected, and that the page still renders correctly with no filter attached.
🤖 Generated with Claude Code
#3
@
3 days ago
Source review of wp-admin/my-sites.php confirms the reported markup. When the 'global' myblogs_options result is non-empty, Core prints <h2>Global Settings</h2> directly inside <ul class="my-sites striped">, before the first <li>. The heading alone makes the list invalid, regardless of what markup the callback returns. With the default empty result, Core does not print that heading.
The placement appears to date from [33072], when My Sites changed from a table to a list. Before that changeset, Global Settings occupied a valid <tr> with <td> cells. The conversion removed that row wrapper but left the global output after the new <ul> opening tag.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)
Trac ticket: https://core.trac.wordpress.org/ticket/66216
Description
In
wp-admin/my-sites.php, the Global Settings section ($settings_html = apply_filters( 'myblogs_options', '', 'global' );) and its hardcoded heading (<h2>Global Settings</h2>) were rendered immediately after<ul class="my-sites striped">opening tag, preceding the<li>elements.This caused two issues:
<h2>and settings fields directly inside a<ul>violates HTML specifications, as<ul>can only contain<li>elements (and script-supporting elements). This confusingly breaks assistive technology (e.g. screen readers) when announcing list item counts.<li>element, and documented$contextasstringeven though the second call passes the site object ($user_blog).Changes
<ul>: Moved the evaluation and output ofapply_filters( 'myblogs_options', '', 'global' )and<h2>Global Settings</h2>outside and before the<ul class="my-sites striped">list. The<ul>element now strictly contains only the<li>site cards.myblogs_options:$contextis'global', settings are rendered in the Global Settings section above the sites list.$contextis a site object, the markup is output inside the site's<li>element.@param string|object $context Context of the setting. Either 'global' or an object containing the site data. Default 'global'..Testing Instructions
wp-admin/my-sites.php.myblogs_optionsfilter.<ul class="my-sites striped">only contains<li>children.'global'context:<h2>Global Settings</h2>and the custom markup are rendered above<ul class="my-sites striped">.<ul class="my-sites striped">contains only<li>items and valid HTML.<li>element.Local checks:
php -l src/wp-admin/my-sites.phpvendor/bin/phpcs --standard=WordPress-Core src/wp-admin/my-sites.phpgit diff --checkProps therssoftware.
---
AI Disclosure: In accordance with the WordPress AI policy, I disclose that generative AI (Google Antigravity) was used for inspecting the markup, verifying HTML validation, and drafting this pull request and tests. All changes and test outputs were manually reviewed and verified.