Skip to content

Admin & Customizer: Fix type casting for version string and aria-pressed attribute#11206

Open
huzaifaalmesbah wants to merge 6 commits intoWordPress:trunkfrom
huzaifaalmesbah:fix/type-casting-admin-customize
Open

Admin & Customizer: Fix type casting for version string and aria-pressed attribute#11206
huzaifaalmesbah wants to merge 6 commits intoWordPress:trunkfrom
huzaifaalmesbah:fix/type-casting-admin-customize

Conversation

@huzaifaalmesbah
Copy link
Member

@huzaifaalmesbah huzaifaalmesbah commented Mar 8, 2026

Description

Fixes type casting issues found by PHPStan:

  • admin-header.php: Change (float) to (string) cast for version to preserve full format (e.g., "7.0-beta3")
  • customize.php: Add (string) cast for $active in aria-pressed attribute

Trac Ticket

https://core.trac.wordpress.org/ticket/64238

Testing

  1. Admin body class shows full version (e.g., branch-7-0-beta3)
  2. Customizer device buttons have proper aria-pressed string values

@github-actions
Copy link

github-actions bot commented Mar 8, 2026

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props huzaifaalmesbah, apermo, westonruter, siliconforks.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions
Copy link

github-actions bot commented Mar 8, 2026

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@apermo
Copy link

apermo commented Mar 8, 2026

One issue here is that admin-header.php is 15+ year old spaghetti code. Maybe it should be considered to modernize the file. Due to the nature of the file, this can't be unit tested. A function wp_get_admin_body_class() that wrapps all the logic would allow proper unit testing here.

cc @westonruter

}

$admin_body_class .= ' branch-' . str_replace( array( '.', ',' ), '-', (float) get_bloginfo( 'version' ) );
$admin_body_class .= ' branch-' . str_replace( array( '.', ',' ), '-', (string) ( (float) get_bloginfo( 'version' ) ) );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a curious way to have extracted the major version from the $wp_version. When I do (float) '6.9.1-beta1' the result is a 6.9 (with a class branch-6-9) and for (float) '7.0-beta3' the result is 7 (with a class branch-7). This seems like an abuse of a float. But, it works.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about get_bloginfo( 'branch' ) or get_bloginfo( 'branch_version' ) that would be cleaner than this. I agree that this is abuse, or at least would fall under "clever coding".

Appearance number 2

$checksums = get_core_checksums( $wp_version, 'en_US' );
$dev = ( str_contains( $wp_version, '-' ) );
// Get the last stable version's files and test against that.
if ( ! $checksums && $dev ) {
$checksums = get_core_checksums( (float) $wp_version - 0.1, 'en_US' );
}

And number 3

$current_version = substr( $GLOBALS['wp_version'], 0, 3 );
$latest_stable = number_format( (float) $current_version - 0.1, 1 ) . '.x';

I think at least a helper function to get the x.y style version without abusing float looks seems a logical choice.

Copy link

@apermo apermo Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a total of 4 ways in core to get a x.y style version.

  1. substr($version, 0, 3) — assumes single-digit major
  • src/wp-admin/includes/theme.php:507 — API request for theme compatibility
  • src/wp-admin/includes/plugin-install.php:116 — API request for plugin compatibility
  • src/wp-includes/block-template-utils.php:1556 — theme.json version path
  • tests/phpunit/tests/basic.php:39 — test assertion

Note: This approach will break once we reach 10.0, still a long way, but not a sustainable approach.

  1. implode('.', array_slice(preg_split('/[.-]/', $ver), 0, 2)) — robust split
  • src/wp-admin/includes/class-core-upgrader.php:282-283 — comparing current vs offered branch during core updates
    $current_branch = implode( '.', array_slice( preg_split( '/[.-]/', $wp_version ), 0, 2 ) ); // x.y
    $new_branch = implode( '.', array_slice( preg_split( '/[.-]/', $offered_ver ), 0, 2 ) ); // x.y
  1. explode('.') with index access — $parts[0] . '.' . $parts[1]
  • src/wp-admin/includes/class-wp-site-health.php:292-296 — comparing major versions for update status
  1. (float) cast
  • src/wp-admin/admin-header.php:194
  • src/wp-admin/includes/class-wp-site-health-auto-updates.php:355
  • tests/phpunit/tests/basic.php:40

My proposal is a helper function that can be used throughout core, using the implode/split approach from 2. that accepts a version, and returns it in x.y format.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a curious way to have extracted the major version from the $wp_version. When I do (float) '6.9.1-beta1' the result is a 6.9 (with a class branch-6-9) and for (float) '7.0-beta3' the result is 7 (with a class branch-7). This seems like an abuse of a float. But, it works.

Does it really work in all cases though? Converting from float to string is not really well-defined behavior.

For example, if I set precision to 50 in my php.ini file and I run WordPress 6.9.1, I get the following in the admin body class: branch-6-9000000000000003552713678800500929355621337890625. (Note that it will not be possible to reproduce this bug with the beta version, or with current trunk, because right now the version happens to be at 7.0. But this problem will obviously show up again in WordPress 7.1, WordPress 7.2, etc.)

I would argue that PHPStan has found a genuine bug here. Adding a (string) cast for this code will tell PHPStan to be quiet, but it doesn't actually fix the bug.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that for 7.0 it seems the desired result is branch-7 not branch-7-0. There would be a version-7-0, however. And when 7.0.1 comes out, it would be version-7-0-1. So I think this is what we need:

$version_without_tag = strtok( get_bloginfo( 'version' ), '-' );
$version_components  = explode( '.', $version_parts );
$version_class       = 'version-' . implode( '-', $version_components );

$branch_version_components = array_slice( $version_components, 0, 2 );
if ( '0' === array_last( $branch_version_components ) ) {
    array_pop( $branch_version_components );
}
$branch_class = 'branch-' . implode( '-', $branch_version_components );

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks everyone for the detailed feedback!
@westonruter I agree with your suggestion. it seems like a much safer and cleaner approach compared to the float casting workaround. I tested the logic locally to verify the version parsing and it appears to work correctly.

  • ✅ PHPStan level 5 now passes with no errors using this approach.
  • ✅ Tested with various versions:
    • 6.9.1-beta2branch-6-9, version-6-9-1
    • 7.0-beta3branch-7, version-7-0
    • 7.0.1branch-7, version-7-0-1
    • 10.0-beta3branch-10, version-10-0
    • 10.1-beta3branch-10-1, version-10-1

If there are no objections, I can proceed with update this in the PR.

@apermo I also agree that introducing a helper function could be useful for other places in core that use similar patterns, though that might be better handled as a separate enhancement.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Go for it 👍

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@westonruter @huzaifaalmesbah Created a new trac ticket for the improvement.

https://core.trac.wordpress.org/ticket/64830

@westonruter
Copy link
Member

One issue here is that admin-header.php is 15+ year old spaghetti code. Maybe it should be considered to modernize the file. Due to the nature of the file, this can't be unit tested. A function wp_get_admin_body_class() that wrapps all the logic would allow proper unit testing here.

Yeah, we should avoid refactoring this file or making substantial logic changes. If we do end up doing a deeper admin redesign, this would be part of that effort. But otherwise, we should avoid making changes unless we absolutely have to due to the extreme legacy nature and potential for back-compat breakage.

@westonruter
Copy link
Member

Fixes type casting issues found by PHPStan:

@huzaifaalmesbah How did you identify these two files alone to be included in this PR? Were they the only files with type casting issues, or what?

Co-authored-by: Weston Ruter <westonruter@gmail.com>
@huzaifaalmesbah
Copy link
Member Author

@huzaifaalmesbah How did you identify these two files alone to be included in this PR? Were they the only files with type casting issues, or what?

@westonruter I ran PHPStan at level 5:

  • Level 5: Found 2 errors
  1. admin-header.php:194 - str_replace() expects string but gets float from (float) get_bloginfo('version')
  2. customize.php:291 - esc_attr() expects string but gets bool from $active

These were the type casting issues I identified for this PR.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR targets PHPStan-reported type casting issues in wp-admin by adjusting how WordPress version strings and aria-pressed values are generated in admin markup.

Changes:

  • Updates Customizer device switcher buttons to output valid aria-pressed="true|false" values.
  • Refactors admin body class generation for version-* / branch-* based on get_bloginfo( 'version' ).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/wp-admin/customize.php Ensures aria-pressed is output as true/false strings for device buttons.
src/wp-admin/admin-header.php Reworks how version-* and branch-* body classes are derived from the WP version string.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +194 to +196
$version_without_tag = strtok( get_bloginfo( 'version' ), '-' );
$version_components = explode( '.', $version_without_tag );
$version_class = 'version-' . implode( '-', $version_components );
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strtok( get_bloginfo( 'version' ), '-' ) strips the pre-release suffix (e.g. -beta3/-RC1) from the version early, so the computed admin body classes can no longer include the full version string. This contradicts the PR description/testing expectation of preserving a value like 7.0-beta3 in the generated class; consider building the branch-* class from the unmodified version string (with appropriate sanitization) or otherwise retaining the suffix.

Copilot uses AI. Check for mistakes.
Comment on lines +194 to 205
$version_without_tag = strtok( get_bloginfo( 'version' ), '-' );
$version_components = explode( '.', $version_without_tag );
$version_class = 'version-' . implode( '-', $version_components );
$admin_body_class .= ' ' . $version_class;
$branch_version_components = array_slice( $version_components, 0, 2 );
if ( '0' === array_last( $branch_version_components ) ) {
array_pop( $branch_version_components );
}
$branch_class = 'branch-' . implode( '-', $branch_version_components );
$admin_body_class .= ' ' . $branch_class;
$admin_body_class .= ' admin-color-' . sanitize_html_class( get_user_option( 'admin_color' ), 'modern' );
$admin_body_class .= ' locale-' . sanitize_html_class( strtolower( str_replace( '_', '-', get_user_locale() ) ) );
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the current $branch_version_components logic, a version like 7.0-beta3 ends up producing branch-7 (slice ['7','0'], then pop the trailing 0). If the goal is to preserve the full version format in the branch class (e.g. branch-7-0-beta3 per the PR description), this needs to incorporate the full version string (including the suffix) rather than collapsing to major-only.

Suggested change
$version_without_tag = strtok( get_bloginfo( 'version' ), '-' );
$version_components = explode( '.', $version_without_tag );
$version_class = 'version-' . implode( '-', $version_components );
$admin_body_class .= ' ' . $version_class;
$branch_version_components = array_slice( $version_components, 0, 2 );
if ( '0' === array_last( $branch_version_components ) ) {
array_pop( $branch_version_components );
}
$branch_class = 'branch-' . implode( '-', $branch_version_components );
$admin_body_class .= ' ' . $branch_class;
$admin_body_class .= ' admin-color-' . sanitize_html_class( get_user_option( 'admin_color' ), 'modern' );
$admin_body_class .= ' locale-' . sanitize_html_class( strtolower( str_replace( '_', '-', get_user_locale() ) ) );
$full_version = get_bloginfo( 'version' );
$version_without_tag = strtok( $full_version, '-' );
$version_components = explode( '.', $version_without_tag );
$version_class = 'version-' . implode( '-', $version_components );
$admin_body_class .= ' ' . $version_class;
$branch_class = 'branch-' . str_replace( '.', '-', $full_version );
$admin_body_class .= ' ' . $branch_class;
$admin_body_class .= ' admin-color-' . sanitize_html_class( get_user_option( 'admin_color' ), 'modern' );
$admin_body_class .= ' locale-' . sanitize_html_class( strtolower( str_replace( '_', '-', get_user_locale() ) ) );

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants