Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions src/wp-includes/theme.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down
36 changes: 36 additions & 0 deletions tests/phpunit/tests/theme/themeDir.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.' );
}
}
Loading