Skip to content
Open
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
8 changes: 3 additions & 5 deletions src/wp-admin/includes/file.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,10 @@ function list_files( $folder = '', $levels = 100, $exclusions = array(), $includ

$files = array();

$dir = @opendir( $folder );
$results = @scandir( $folder );

if ( $dir ) {
while ( ( $file = readdir( $dir ) ) !== false ) {
if ( $results ) {
foreach ( $results as $file ) {
// Skip current and parent folder links.
if ( in_array( $file, array( '.', '..' ), true ) ) {
continue;
Expand All @@ -174,8 +174,6 @@ function list_files( $folder = '', $levels = 100, $exclusions = array(), $includ
$files[] = $folder . $file;
}
}

closedir( $dir );
}

return $files;
Expand Down
56 changes: 56 additions & 0 deletions src/wp-admin/includes/misc.php
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,41 @@ function update_recently_edited( $file ) {
update_option( 'recently_edited', $oldfiles );
}

/**
* Sorts a file tree array with folders before files, both in alphabetical order.
*
* @since x.x.x
* @access private
*
* @param array $tree File tree to sort, as returned by wp_make_theme_file_tree()
* or wp_make_plugin_file_tree(). Directory nodes are arrays,
* file nodes are strings.
* @return array Sorted file tree.
*/
function _wp_sort_file_tree( $tree ) {
uksort(
$tree,
function ( $a, $b ) use ( $tree ) {
$a_is_dir = is_array( $tree[ $a ] );
$b_is_dir = is_array( $tree[ $b ] );

if ( $a_is_dir !== $b_is_dir ) {
return $a_is_dir ? -1 : 1;
}

return strcasecmp( $a, $b );
}
);

foreach ( $tree as $key => $subtree ) {
if ( is_array( $subtree ) ) {
$tree[ $key ] = _wp_sort_file_tree( $subtree );
}
}

return $tree;
}

/**
* Makes a tree structure for the theme file editor's file list.
*
Expand All @@ -378,6 +413,17 @@ function wp_make_theme_file_tree( $allowed_files ) {
$last_dir = $file_name;
}

$tree_list = _wp_sort_file_tree( $tree_list );

// Move the main theme files to the top of the list, preserving the order
// established by theme-editor.php (style.css first, then functions.php).
if ( isset( $tree_list['functions.php'] ) ) {
$tree_list = array( 'functions.php' => $tree_list['functions.php'] ) + $tree_list;
}
if ( isset( $tree_list['style.css'] ) ) {
$tree_list = array( 'style.css' => $tree_list['style.css'] ) + $tree_list;
}

return $tree_list;
}

Expand Down Expand Up @@ -483,6 +529,16 @@ function wp_make_plugin_file_tree( $plugin_editable_files ) {
$last_dir = $plugin_file;
}

$tree_list = _wp_sort_file_tree( $tree_list );

// Move the main plugin file to the top of the list.
if ( ! empty( $plugin_editable_files ) ) {
$main_file_key = wp_basename( $plugin_editable_files[0] );
if ( isset( $tree_list[ $main_file_key ] ) ) {
$tree_list = array( $main_file_key => $tree_list[ $main_file_key ] ) + $tree_list;
}
}

return $tree_list;
}

Expand Down
112 changes: 112 additions & 0 deletions tests/phpunit/tests/admin/includesMisc.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,118 @@ public function test_shorten_url() {
}
}

/**
* Tests that _wp_sort_file_tree() places folders before files, both alphabetically.
*
* @ticket 47544
*
* @covers ::_wp_sort_file_tree
*/
public function test_wp_sort_file_tree_folders_before_files() {
$tree = array(
'readme.txt' => 'plugin/readme.txt',
'assets' => array(
'logo.png' => 'plugin/assets/logo.png',
),
'composer.json' => 'plugin/composer.json',
'classes' => array(
'Foo.php' => 'plugin/classes/Foo.php',
),
);

$sorted = _wp_sort_file_tree( $tree );
$keys = array_keys( $sorted );

$this->assertSame( 0, array_search( 'assets', $keys, true ), 'assets folder should be first.' );
$this->assertSame( 1, array_search( 'classes', $keys, true ), 'classes folder should be second.' );
$this->assertSame( 2, array_search( 'composer.json', $keys, true ), 'composer.json file should be third.' );
$this->assertSame( 3, array_search( 'readme.txt', $keys, true ), 'readme.txt file should be last.' );
}

/**
* Tests that _wp_sort_file_tree() sorts recursively within subdirectories.
*
* @ticket 47544
*
* @covers ::_wp_sort_file_tree
*/
public function test_wp_sort_file_tree_sorts_recursively() {
$tree = array(
'src' => array(
'zebra.php' => 'plugin/src/zebra.php',
'inc' => array(
'b.php' => 'plugin/src/inc/b.php',
'a.php' => 'plugin/src/inc/a.php',
),
'apple.php' => 'plugin/src/apple.php',
),
);

$sorted = _wp_sort_file_tree( $tree );
$src_keys = array_keys( $sorted['src'] );

$this->assertSame( 'inc', $src_keys[0], 'inc folder should be first inside src/.' );
$this->assertSame( 'apple.php', $src_keys[1], 'apple.php should be before zebra.php.' );
$this->assertSame( 'zebra.php', $src_keys[2], 'zebra.php should be last inside src/.' );

$inc_keys = array_keys( $sorted['src']['inc'] );
$this->assertSame( array( 'a.php', 'b.php' ), $inc_keys );
}

/**
* Tests that wp_make_plugin_file_tree() places the main plugin file first,
* then folders, then files, all alphabetically.
*
* @ticket 47544
*
* @covers ::wp_make_plugin_file_tree
*/
public function test_wp_make_plugin_file_tree_main_file_first_then_folders_then_files() {
$plugin_files = array(
'my-plugin/my-plugin.php',
'my-plugin/readme.txt',
'my-plugin/assets/logo.png',
'my-plugin/classes/Foo.php',
'my-plugin/composer.json',
);

$tree = wp_make_plugin_file_tree( $plugin_files );
$keys = array_keys( $tree );

$this->assertSame( 'my-plugin.php', $keys[0], 'Main plugin file should be first.' );
$this->assertSame( 'assets', $keys[1], 'assets folder should come before files.' );
$this->assertSame( 'classes', $keys[2], 'classes folder should come before files.' );
$this->assertSame( 'composer.json', $keys[3], 'composer.json should be sorted alphabetically among files.' );
$this->assertSame( 'readme.txt', $keys[4], 'readme.txt should be last.' );
}

/**
* Tests that wp_make_theme_file_tree() places style.css first, functions.php
* second, then folders, then files alphabetically.
*
* @ticket 47544
*
* @covers ::wp_make_theme_file_tree
*/
public function test_wp_make_theme_file_tree_main_files_first_then_folders_then_files() {
$allowed_files = array(
'readme.txt' => '/theme/readme.txt',
'inc/extras.php' => '/theme/inc/extras.php',
'functions.php' => '/theme/functions.php',
'style.css' => '/theme/style.css',
'404.php' => '/theme/404.php',
);

$tree = wp_make_theme_file_tree( $allowed_files );
$keys = array_keys( $tree );

$this->assertSame( 'style.css', $keys[0], 'style.css should be first.' );
$this->assertSame( 'functions.php', $keys[1], 'functions.php should be second.' );
$this->assertSame( 'inc', $keys[2], 'inc folder should come before loose files.' );
$this->assertSame( '404.php', $keys[3], '404.php should be sorted alphabetically among files.' );
$this->assertSame( 'readme.txt', $keys[4], 'readme.txt should be last.' );
}

/**
* @ticket 59520
*/
Expand Down
26 changes: 26 additions & 0 deletions tests/phpunit/tests/functions/listFiles.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,32 @@ public function test_list_files_should_optionally_include_hidden_files( $filenam
}
}

/**
* Tests that list_files() returns files in alphabetical order.
*
* @ticket 47544
*/
public function test_list_files_returns_files_in_alphabetical_order() {
$test_dir = get_temp_dir() . 'test-list-files-order/';
mkdir( $test_dir );

// Create files in reverse alphabetical order.
touch( $test_dir . 'zebra.php' );
touch( $test_dir . 'mango.php' );
touch( $test_dir . 'apple.php' );

$files = list_files( $test_dir );

unlink( $test_dir . 'zebra.php' );
unlink( $test_dir . 'mango.php' );
unlink( $test_dir . 'apple.php' );
rmdir( $test_dir );

$basenames = array_map( 'wp_basename', $files );

$this->assertSame( array( 'apple.php', 'mango.php', 'zebra.php' ), $basenames );
}

/**
* Data provider.
*
Expand Down
Loading