From 33a0ff50b75cf928624b2c886ace3a593e944c4c Mon Sep 17 00:00:00 2001 From: Gary Pendergast Date: Tue, 10 Oct 2017 05:33:57 +0000 Subject: [PATCH] File Editor: Add support for more than one sub-directory level. The theme and plugin editors now list all files in the selected theme or plugin, recursing through subdirectories as necessary. Props WraithKenny, schlessera, chsxf, MikeHansenMe, Daedalon, valendesigns, westonruter, pento. Fixes #6531. git-svn-id: https://develop.svn.wordpress.org/trunk@41806 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/file.php | 42 ++++++++++---- src/wp-admin/includes/plugin.php | 60 +++++++++++--------- src/wp-admin/theme-editor.php | 6 +- src/wp-includes/class-wp-theme.php | 46 ++++++++++++--- tests/phpunit/tests/admin/includesPlugin.php | 25 ++++++++ tests/phpunit/tests/functions/listFiles.php | 20 +++++++ 6 files changed, 149 insertions(+), 50 deletions(-) create mode 100644 tests/phpunit/tests/functions/listFiles.php diff --git a/src/wp-admin/includes/file.php b/src/wp-admin/includes/file.php index 456114b1bc..83c91232e2 100644 --- a/src/wp-admin/includes/file.php +++ b/src/wp-admin/includes/file.php @@ -120,35 +120,53 @@ function get_home_path() { * The depth of the recursiveness can be controlled by the $levels param. * * @since 2.6.0 + * @since 4.9.0 Added the `$exclusions` parameter. * * @param string $folder Optional. Full path to folder. Default empty. * @param int $levels Optional. Levels of folders to follow, Default 100 (PHP Loop limit). + * @param array $exclusions Optional. List of folders and files to skip. * @return bool|array False on failure, Else array of files */ -function list_files( $folder = '', $levels = 100 ) { - if ( empty($folder) ) +function list_files( $folder = '', $levels = 100, $exclusions = array() ) { + if ( empty( $folder ) ) { return false; + } - if ( ! $levels ) + $folder = trailingslashit( $folder ); + + if ( ! $levels ) { return false; + } $files = array(); - if ( $dir = @opendir( $folder ) ) { - while (($file = readdir( $dir ) ) !== false ) { - if ( in_array($file, array('.', '..') ) ) + + $dir = @opendir( $folder ); + if ( $dir ) { + while ( ( $file = readdir( $dir ) ) !== false ) { + // Skip current and parent folder links. + if ( in_array( $file, array( '.', '..' ), true ) ) { continue; - if ( is_dir( $folder . '/' . $file ) ) { - $files2 = list_files( $folder . '/' . $file, $levels - 1); - if ( $files2 ) + } + + // Skip hidden and excluded files. + if ( '.' === $file[0] || in_array( $file, $exclusions, true ) ) { + continue; + } + + if ( is_dir( $folder . $file ) ) { + $files2 = list_files( $folder . $file, $levels - 1 ); + if ( $files2 ) { $files = array_merge($files, $files2 ); - else - $files[] = $folder . '/' . $file . '/'; + } else { + $files[] = $folder . $file . '/'; + } } else { - $files[] = $folder . '/' . $file; + $files[] = $folder . $file; } } } @closedir( $dir ); + return $files; } diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index 69aeacda2f..56d241c16f 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -190,35 +190,43 @@ function _get_plugin_data_markup_translate( $plugin_file, $plugin_data, $markup * @param string $plugin Path to the main plugin file from plugins directory. * @return array List of files relative to the plugin root. */ -function get_plugin_files($plugin) { +function get_plugin_files( $plugin ) { $plugin_file = WP_PLUGIN_DIR . '/' . $plugin; - $dir = dirname($plugin_file); - $plugin_files = array($plugin); - if ( is_dir($dir) && $dir != WP_PLUGIN_DIR ) { - $plugins_dir = @ opendir( $dir ); - if ( $plugins_dir ) { - while (($file = readdir( $plugins_dir ) ) !== false ) { - if ( substr($file, 0, 1) == '.' ) - continue; - if ( is_dir( $dir . '/' . $file ) ) { - $plugins_subdir = @ opendir( $dir . '/' . $file ); - if ( $plugins_subdir ) { - while (($subfile = readdir( $plugins_subdir ) ) !== false ) { - if ( substr($subfile, 0, 1) == '.' ) - continue; - $plugin_files[] = plugin_basename("$dir/$file/$subfile"); - } - @closedir( $plugins_subdir ); - } - } else { - if ( plugin_basename("$dir/$file") != $plugin ) - $plugin_files[] = plugin_basename("$dir/$file"); - } - } - @closedir( $plugins_dir ); - } + $dir = dirname( $plugin_file ); + + $data = get_plugin_data( $plugin_file ); + $label = isset( $data['Version'] ) + ? sanitize_key( 'files_' . $plugin . '-' . $data['Version'] ) + : sanitize_key( 'files_' . $plugin ); + $transient_key = substr( $label, 0, 29 ) . md5( $label ); + + $plugin_files = get_transient( $transient_key ); + if ( false !== $plugin_files ) { + return $plugin_files; } + $plugin_files = array( plugin_basename( $plugin_file ) ); + + if ( is_dir( $dir ) && WP_PLUGIN_DIR !== $dir ) { + + /** + * Filters the array of excluded directories and files while scanning the folder. + * + * @since 4.9.0 + * + * @param array $exclusions Array of excluded directories and files. + */ + $exclusions = (array) apply_filters( 'plugin_files_exclusions', array( 'CVS', 'node_modules', 'vendor', 'bower_components' ) ); + + $list_files = list_files( $dir, 100, $exclusions ); + $list_files = array_map( 'plugin_basename', $list_files ); + + $plugin_files = array_merge( $plugin_files, $list_files ); + $plugin_files = array_values( array_unique( $plugin_files ) ); + } + + set_transient( $transient_key, $plugin_files, HOUR_IN_SECONDS ); + return $plugin_files; } diff --git a/src/wp-admin/theme-editor.php b/src/wp-admin/theme-editor.php index ebac61b538..70629d914d 100644 --- a/src/wp-admin/theme-editor.php +++ b/src/wp-admin/theme-editor.php @@ -75,16 +75,16 @@ $file_types = wp_get_theme_file_editable_extensions( $theme ); foreach ( $file_types as $type ) { switch ( $type ) { case 'php': - $allowed_files += $theme->get_files( 'php', 1 ); + $allowed_files += $theme->get_files( 'php', -1 ); $has_templates = ! empty( $allowed_files ); break; case 'css': - $style_files = $theme->get_files( 'css' ); + $style_files = $theme->get_files( 'css', -1 ); $allowed_files['style.css'] = $style_files['style.css']; $allowed_files += $style_files; break; default: - $allowed_files += $theme->get_files( $type ); + $allowed_files += $theme->get_files( $type, -1 ); break; } } diff --git a/src/wp-includes/class-wp-theme.php b/src/wp-includes/class-wp-theme.php index 30e9226132..4dd0a75016 100644 --- a/src/wp-includes/class-wp-theme.php +++ b/src/wp-includes/class-wp-theme.php @@ -981,13 +981,38 @@ final class WP_Theme implements ArrayAccess { * @param int $depth Optional. How deep to search for files. Defaults to a flat scan (0 depth). -1 depth is infinite. * @param bool $search_parent Optional. Whether to return parent files. Defaults to false. * @return array Array of files, keyed by the path to the file relative to the theme's directory, with the values - * being absolute paths. + * being absolute paths. */ public function get_files( $type = null, $depth = 0, $search_parent = false ) { - $files = (array) self::scandir( $this->get_stylesheet_directory(), $type, $depth ); + // get and cache all theme files to start with. + $label = sanitize_key( 'files_' . $this->cache_hash . '-' . $this->get( 'Version' ) ); + $transient_key = substr( $label, 0, 29 ) . md5( $label ); - if ( $search_parent && $this->parent() ) - $files += (array) self::scandir( $this->get_template_directory(), $type, $depth ); + $all_files = get_transient( $transient_key ); + if ( false === $all_files ) { + $all_files = (array) self::scandir( $this->get_stylesheet_directory(), null, -1 ); + + if ( $search_parent && $this->parent() ) { + $all_files += (array) self::scandir( $this->get_template_directory(), null, -1 ); + } + + set_transient( $transient_key, $all_files, HOUR_IN_SECONDS ); + } + + // Filter $all_files by $type & $depth. + $files = array(); + if ( $type ) { + $type = (array) $type; + $_extensions = implode( '|', $type ); + } + foreach ( $all_files as $key => $file ) { + if ( $depth >= 0 && substr_count( $key, '/' ) > $depth ) { + continue; // Filter by depth. + } + if ( ! $type || preg_match( '~\.(' . $_extensions . ')$~', $file ) ) { // Filter by type. + $files[ $key ] = $file; + } + } return $files; } @@ -1107,8 +1132,9 @@ final class WP_Theme implements ArrayAccess { * with `$relative_path`, with the values being absolute paths. False otherwise. */ private static function scandir( $path, $extensions = null, $depth = 0, $relative_path = '' ) { - if ( ! is_dir( $path ) ) + if ( ! is_dir( $path ) ) { return false; + } if ( $extensions ) { $extensions = (array) $extensions; @@ -1116,8 +1142,9 @@ final class WP_Theme implements ArrayAccess { } $relative_path = trailingslashit( $relative_path ); - if ( '/' == $relative_path ) + if ( '/' == $relative_path ) { $relative_path = ''; + } $results = scandir( $path ); $files = array(); @@ -1125,19 +1152,20 @@ final class WP_Theme implements ArrayAccess { /** * Filters the array of excluded directories and files while scanning theme folder. * - * @since 4.7.4 + * @since 4.7.4 * * @param array $exclusions Array of excluded directories and files. */ - $exclusions = (array) apply_filters( 'theme_scandir_exclusions', array( 'CVS', 'node_modules' ) ); + $exclusions = (array) apply_filters( 'theme_scandir_exclusions', array( 'CVS', 'node_modules', 'vendor', 'bower_components' ) ); foreach ( $results as $result ) { if ( '.' == $result[0] || in_array( $result, $exclusions, true ) ) { continue; } if ( is_dir( $path . '/' . $result ) ) { - if ( ! $depth ) + if ( ! $depth ) { continue; + } $found = self::scandir( $path . '/' . $result, $extensions, $depth - 1 , $relative_path . $result ); $files = array_merge_recursive( $files, $found ); } elseif ( ! $extensions || preg_match( '~\.(' . $_extensions . ')$~', $result ) ) { diff --git a/tests/phpunit/tests/admin/includesPlugin.php b/tests/phpunit/tests/admin/includesPlugin.php index b7ed20dd90..29bc84dfe5 100644 --- a/tests/phpunit/tests/admin/includesPlugin.php +++ b/tests/phpunit/tests/admin/includesPlugin.php @@ -93,6 +93,31 @@ class Tests_Admin_includesPlugin extends WP_UnitTestCase { $this->assertEquals( array( $name ), get_plugin_files( $name ) ); } + /** + * @covers ::get_plugin_files + */ + public function test_get_plugin_files_folder() { + $plugin_dir = WP_PLUGIN_DIR . '/list_files_test_plugin'; + @mkdir( $plugin_dir ); + $plugin = $this->_create_plugin(null, 'list_files_test_plugin.php', $plugin_dir ); + + $sub_dir = trailingslashit( dirname( $plugin[1] ) ) . 'subdir'; + @mkdir( $sub_dir ); + @file_put_contents( $sub_dir . '/subfile.php', 'assertEquals( $expected, $plugin_files ); + + unlink( $sub_dir . '/subfile.php' ); + unlink( $plugin[1] ); + rmdir( $sub_dir ); + rmdir( $plugin_dir ); + } + /** * @covers ::get_mu_plugins */ diff --git a/tests/phpunit/tests/functions/listFiles.php b/tests/phpunit/tests/functions/listFiles.php new file mode 100644 index 0000000000..d461e0c195 --- /dev/null +++ b/tests/phpunit/tests/functions/listFiles.php @@ -0,0 +1,20 @@ +assertInternalType( 'array', $admin_files ); + $this->assertNotEmpty( $admin_files ); + $this->assertContains( ABSPATH . 'wp-admin/index.php', $admin_files ); + } + + public function test_list_files_can_exclude_files() { + $admin_files = list_files( ABSPATH . 'wp-admin/', 100, array( 'index.php' ) ); + $this->assertNotContains( ABSPATH . 'wp-admin/index.php', $admin_files ); + } +}