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 ); + } +}