diff --git a/src/wp-includes/theme.php b/src/wp-includes/theme.php index cd83d15d1b914..8012a5947ae67 100644 --- a/src/wp-includes/theme.php +++ b/src/wp-includes/theme.php @@ -428,7 +428,15 @@ function register_theme_directory( $directory ) { $wp_theme_directories = array(); } - $untrailed = untrailingslashit( $directory ); + /* + * Normalize the path so that Windows backslashes are converted to forward + * slashes. This ensures consistent path storage and comparison across + * operating systems, preventing issues when a database dump made on Windows + * is loaded on a Unix-based system (and vice versa). + * + * See https://core.trac.wordpress.org/ticket/29051 + */ + $untrailed = untrailingslashit( wp_normalize_path( $directory ) ); if ( ! empty( $untrailed ) && ! in_array( $untrailed, $wp_theme_directories, true ) ) { $wp_theme_directories[] = $untrailed; } @@ -469,8 +477,17 @@ function search_theme_directories( $force = false ) { * to use in get_theme_root(). */ foreach ( $wp_theme_directories as $theme_root ) { - if ( str_starts_with( $theme_root, WP_CONTENT_DIR ) ) { - $relative_theme_roots[ str_replace( WP_CONTENT_DIR, '', $theme_root ) ] = $theme_root; + /* + * Normalize the theme root path and the content directory to forward slashes + * so that the prefix strip works correctly on Windows, where directory + * separators may differ from those stored in WP_CONTENT_DIR. + * + * See https://core.trac.wordpress.org/ticket/29051 + */ + $normalized_theme_root = wp_normalize_path( $theme_root ); + $normalized_content_dir = wp_normalize_path( WP_CONTENT_DIR ); + if ( str_starts_with( $normalized_theme_root, $normalized_content_dir ) ) { + $relative_theme_roots[ str_replace( $normalized_content_dir, '', $normalized_theme_root ) ] = $theme_root; } else { $relative_theme_roots[ $theme_root ] = $theme_root; } diff --git a/tests/phpunit/tests/theme/themeDir.php b/tests/phpunit/tests/theme/themeDir.php index a953a04bc5533..5277787183840 100644 --- a/tests/phpunit/tests/theme/themeDir.php +++ b/tests/phpunit/tests/theme/themeDir.php @@ -319,4 +319,40 @@ public function test_theme_dir_slashes() { rmdir( WP_CONTENT_DIR . '/themes/foo' ); rmdir( WP_CONTENT_DIR . '/themes/foo-themes' ); } + + /** + * @ticket 29051 + */ + public function test_register_theme_directory_stores_normalized_path() { + @mkdir( WP_CONTENT_DIR . '/themes/custom-win-dir', 0777, true ); + + $dir = WP_CONTENT_DIR . '/themes/custom-win-dir'; + register_theme_directory( $dir ); + + // Stored path must equal wp_normalize_path() output (no backslashes on any OS). + $stored = end( $GLOBALS['wp_theme_directories'] ); + $this->assertSame( wp_normalize_path( $dir ), $stored ); + $this->assertStringNotContainsString( '\\', $stored, 'Backslashes must not appear in registered theme dirs.' ); + + rmdir( WP_CONTENT_DIR . '/themes/custom-win-dir' ); + } + + /** + * @ticket 29051 + */ + public function test_search_theme_directories_finds_themes_under_content_dir() { + // self::THEME_ROOT is outside WP_CONTENT_DIR — themes there must still be found. + $results = search_theme_directories( true ); + + $this->assertIsArray( $results ); + + // Every result must carry a 'theme_root' key. + foreach ( $results as $slug => $data ) { + $this->assertArrayHasKey( 'theme_root', $data, "theme_root missing for slug: $slug" ); + } + + // At least one theme from self::THEME_ROOT must be present. + $roots = array_column( $results, 'theme_root' ); + $this->assertContains( self::THEME_ROOT, $roots, 'Themes from non-content-dir root must be discoverable.' ); + } }