WordPress.org

Make WordPress Core

Ticket #6531: 6531.10.diff

File 6531.10.diff, 13.8 KB (added by schlessera, 8 months ago)

Iterator implementation.

  • src/wp-admin/includes/file.php

    diff --git src/wp-admin/includes/file.php src/wp-admin/includes/file.php
    index 553880a46c..0135771f6c 100644
    function get_home_path() { 
    116116}
    117117
    118118/**
    119  * Returns a listing of all files in the specified folder and all subdirectories up to 100 levels deep.
     119 * Returns a listing of all files in the specified folder and all subdirectories.
    120120 * The depth of the recursiveness can be controlled by the $levels param.
    121121 *
    122122 * @since 2.6.0
     123 * @since 4.9.0 Deprecated $levels and added $options.
     124 * @since 4.9.0 Returns an iterator object instead of an array.
    123125 *
    124  * @param string $folder Optional. Full path to folder. Default empty.
    125  * @param int    $levels Optional. Levels of folders to follow, Default 100 (PHP Loop limit).
    126  * @return bool|array False on failure, Else array of files
     126 * @param string $folder  Optional. Full path to folder. Default empty.
     127 * @param int    $levels  Deprecated.
     128 * @param array  $options {
     129 *     Associative array of options for the iterator.
     130 *
     131 *     @type array  $exclusions  Files or folders to exclude.
     132 *     @type string $regex       Regular expression the filename must match.
     133 *     @type bool   $skip_hidden Whether to skip hidden files and folders.
     134 * }
     135 * @return Iterator|false Iterator on filtered files, or false on failure.
    127136 */
    128 function list_files( $folder = '', $levels = 100 ) {
    129         if ( empty($folder) )
     137function list_files( $folder = '', $levels = 100, $options = array() ) {
     138        if ( empty( $folder ) ) {
    130139                return false;
     140        }
    131141
    132         if ( ! $levels )
    133                 return false;
     142        $options = wp_parse_args( $options, array(
     143                'exclusions'  => array(),
     144                'regex'       => false,
     145                'skip_hidden' => true,
     146        ) );
    134147
    135         $files = array();
    136         if ( $dir = @opendir( $folder ) ) {
    137                 while (($file = readdir( $dir ) ) !== false ) {
    138                         if ( in_array($file, array('.', '..') ) )
    139                                 continue;
    140                         if ( is_dir( $folder . '/' . $file ) ) {
    141                                 $files2 = list_files( $folder . '/' . $file, $levels - 1);
    142                                 if ( $files2 )
    143                                         $files = array_merge($files, $files2 );
    144                                 else
    145                                         $files[] = $folder . '/' . $file . '/';
    146                         } else {
    147                                 $files[] = $folder . '/' . $file;
    148                         }
    149                 }
     148        if ( ! class_exists( 'WP_ListFiles_FilterIterator' ) ) {
     149                require ABSPATH . WPINC . '/class-wp-list-files-filter-iterator.php';
     150        }
     151
     152        $iterator = new RecursiveDirectoryIterator( $folder );
     153        $iterator = new WP_ListFiles_FilterIterator( $iterator, $options );
     154
     155        if ( $options['regex'] ) {
     156                $iterator = new RecursiveRegexIterator(
     157                        $iterator,
     158                        $options['regex'],
     159                        RecursiveRegexIterator::MATCH
     160                );
    150161        }
    151         @closedir( $dir );
    152         return $files;
     162
     163        return new RecursiveIteratorIterator( $iterator );
    153164}
    154165
    155166/**
  • src/wp-admin/includes/plugin.php

    diff --git src/wp-admin/includes/plugin.php src/wp-admin/includes/plugin.php
    index 69aeacda2f..f54b662d16 100644
    function _get_plugin_data_markup_translate( $plugin_file, $plugin_data, $markup 
    190190 * @param string $plugin Path to the main plugin file from plugins directory.
    191191 * @return array List of files relative to the plugin root.
    192192 */
    193 function get_plugin_files($plugin) {
     193function get_plugin_files( $plugin ) {
    194194        $plugin_file = WP_PLUGIN_DIR . '/' . $plugin;
    195         $dir = dirname($plugin_file);
    196         $plugin_files = array($plugin);
    197         if ( is_dir($dir) && $dir != WP_PLUGIN_DIR ) {
    198                 $plugins_dir = @ opendir( $dir );
    199                 if ( $plugins_dir ) {
    200                         while (($file = readdir( $plugins_dir ) ) !== false ) {
    201                                 if ( substr($file, 0, 1) == '.' )
    202                                         continue;
    203                                 if ( is_dir( $dir . '/' . $file ) ) {
    204                                         $plugins_subdir = @ opendir( $dir . '/' . $file );
    205                                         if ( $plugins_subdir ) {
    206                                                 while (($subfile = readdir( $plugins_subdir ) ) !== false ) {
    207                                                         if ( substr($subfile, 0, 1) == '.' )
    208                                                                 continue;
    209                                                         $plugin_files[] = plugin_basename("$dir/$file/$subfile");
    210                                                 }
    211                                                 @closedir( $plugins_subdir );
    212                                         }
    213                                 } else {
    214                                         if ( plugin_basename("$dir/$file") != $plugin )
    215                                                 $plugin_files[] = plugin_basename("$dir/$file");
    216                                 }
    217                         }
    218                         @closedir( $plugins_dir );
    219                 }
     195        $dir = dirname( $plugin_file );
     196        $data = get_plugin_data( $plugin_file );
     197
     198        $label = isset( $data['Version'] )
     199                ? 'list_files_cache_' . $dir . '-' . $data['Version']
     200                : 'list_files_cache_' . $dir;
     201        $transient_key = substr( $label, 0, 29 ) . md5( $label );
     202
     203        $plugin_files = get_transient( $transient_key );
     204        if ( false !== $plugin_files ) {
     205                return $plugin_files;
    220206        }
    221207
     208        $plugin_files = array( $plugin );
     209        if ( is_dir( $dir ) && WP_PLUGIN_DIR !== $dir ) {
     210
     211                /**
     212                 * Filters the array of excluded directories and files while scanning
     213                 * for plugin files.
     214                 *
     215                 * @since 4.9
     216                 *
     217                 * @param array $exclusions Array of excluded directories and files.
     218                 */
     219                $exclusions = (array) apply_filters(
     220                        'get_plugin_files_exclusions',
     221                        array( 'CVS', 'node_modules', 'vendor', 'bower_components' )
     222                );
     223
     224                $plugin_files = list_files( $dir, $exclusions );
     225        }
     226
     227        set_transient( $transient_key, $plugin_files, HOUR_IN_SECONDS );
     228
    222229        return $plugin_files;
    223230}
    224231
  • src/wp-admin/theme-editor.php

    diff --git src/wp-admin/theme-editor.php src/wp-admin/theme-editor.php
    index b49013ffd8..97eb6ddaad 100644
    $file_types = wp_get_theme_file_editable_extensions( $theme ); 
    7575foreach ( $file_types as $type ) {
    7676        switch ( $type ) {
    7777                case 'php':
    78                         $allowed_files += $theme->get_files( 'php', 1 );
     78                        $allowed_files += $theme->get_files( 'php', -1 );
    7979                        $has_templates = ! empty( $allowed_files );
    8080                        break;
    8181                case 'css':
    82                         $style_files = $theme->get_files( 'css' );
     82                        $style_files = $theme->get_files( 'css', -1 );
    8383                        $allowed_files['style.css'] = $style_files['style.css'];
    8484                        $allowed_files += $style_files;
    8585                        break;
    8686                default:
    87                         $allowed_files += $theme->get_files( $type );
     87                        $allowed_files += $theme->get_files( $type, -1 );
    8888                        break;
    8989        }
    9090}
  • new file src/wp-includes/class-wp-list-files-filter-iterator.php

    diff --git src/wp-includes/class-wp-list-files-filter-iterator.php src/wp-includes/class-wp-list-files-filter-iterator.php
    new file mode 100644
    index 0000000000..0454d91dd9
    - +  
     1<?php
     2/**
     3 * ListFiles FilterIterator class.
     4 *
     5 * Used internally by the `list_files()` function to allow filtering of the
     6 * recursive iterator.
     7 *
     8 * @package WordPress
     9 */
     10
     11/**
     12 * Helper class to allow filtering of the `list_files()` recursive iterator.
     13 *
     14 * @since 4.9.0
     15 */
     16final class WP_ListFiles_FilterIterator extends RecursiveFilterIterator {
     17
     18        const EXCLUSIONS  = 'exclusions';
     19        const SKIP_HIDDEN = 'skip_hidden';
     20
     21        private $exclusions  = array();
     22        private $skip_hidden = true;
     23
     24        /**
     25         * Instantiate a WP_ListFiles_FilterIterator object.
     26         *
     27         * @since 4.9.0
     28         *
     29         * @param RecursiveIterator $iterator Iterator to create a filter on.
     30         * @param array             $options {
     31         *     Optional. Associative array of options for the iterator.
     32         *
     33         *     @type array  $exclusions  Files or folders to exclude.
     34         *     @type bool   $skip_hidden Whether to skip hidden files and folders.
     35         * }
     36         */
     37        public function __construct( RecursiveIterator $iterator, $options ) {
     38                $this->parse_options( $options );
     39                parent::__construct( $iterator );
     40        }
     41
     42        /**
     43         * Parse the options that were passed in and store them in properties.
     44         *
     45         * @since 4.9.0
     46         *
     47         * @param array $options Associative array of options to parse.
     48         */
     49        private function parse_options( $options ) {
     50                $keys = array( self::EXCLUSIONS, self::SKIP_HIDDEN );
     51                foreach( $keys as $key ) {
     52                        $this->$key = $options[ $key ];
     53                }
     54        }
     55
     56        /**
     57         * Check whether the current element of the iterator is acceptable.
     58         *
     59         * @since 4.9.0
     60         *
     61         * @return bool Whether the current element is acceptable.
     62         */
     63        public function accept() {
     64                $name = $this->current()->getFilename();
     65
     66                // Skip hidden files and folders depending on options.
     67                if ( $this->skip_hidden && '.' === $name[0] ) {
     68                        return false;
     69                }
     70
     71                // Skip files and folders that figure on the list of exclusions.
     72                if ( ! empty( $this->exclusions )
     73                     && in_array( $name, $this->exclusions, true ) ) {
     74                        return false;
     75                }
     76
     77                // Accept everything else.
     78                return true;
     79        }
     80}
  • src/wp-includes/class-wp-theme.php

    diff --git src/wp-includes/class-wp-theme.php src/wp-includes/class-wp-theme.php
    index 30e9226132..70c0fe2be8 100644
    final class WP_Theme implements ArrayAccess { 
    981981         * @param int $depth Optional. How deep to search for files. Defaults to a flat scan (0 depth). -1 depth is infinite.
    982982         * @param bool $search_parent Optional. Whether to return parent files. Defaults to false.
    983983         * @return array Array of files, keyed by the path to the file relative to the theme's directory, with the values
    984          *                   being absolute paths.
     984         *               being absolute paths.
    985985         */
    986986        public function get_files( $type = null, $depth = 0, $search_parent = false ) {
    987                 $files = (array) self::scandir( $this->get_stylesheet_directory(), $type, $depth );
     987                // get and cache all theme files to start with.
     988                $label = 'list_files_cache_' . $this->get( 'Name' ) . '-' . $this->get( 'Version' );
    988989
    989                 if ( $search_parent && $this->parent() )
    990                         $files += (array) self::scandir( $this->get_template_directory(), $type, $depth );
     990                $transient_key = substr( $label, 0, 29 ) . md5( $label );
     991
     992                $all_files = get_transient( $transient_key );
     993                if ( false === $all_files ) {
     994                        $all_files = (array) self::scandir( $this->get_stylesheet_directory(), null, -1 );
     995
     996                        if ( $search_parent && $this->parent() ) {
     997                                $all_files += (array) self::scandir( $this->get_template_directory(), null, -1 );
     998                        }
     999
     1000                        set_transient( $transient_key, $all_files, HOUR_IN_SECONDS );
     1001                }
     1002
     1003                // Filter $all_files by $type & $depth.
     1004                $files = array();
     1005                if ( $type ) {
     1006                        $type = (array) $type;
     1007                        $_extensions = implode( '|', $type );
     1008                }
     1009                foreach ( $all_files as $key => $file ) {
     1010                        if ( $depth >= 0 && substr_count( $key, '/' ) > $depth ) {
     1011                                continue; // Filter by depth.
     1012                        }
     1013                        if ( ! $type || preg_match( '~\.(' . $_extensions . ')$~', $file ) ) { // Filter by type.
     1014                                $files[ $key ] = $file;
     1015                        }
     1016                }
    9911017
    9921018                return $files;
    9931019        }
    final class WP_Theme implements ArrayAccess { 
    11071133         *                     with `$relative_path`, with the values being absolute paths. False otherwise.
    11081134         */
    11091135        private static function scandir( $path, $extensions = null, $depth = 0, $relative_path = '' ) {
    1110                 if ( ! is_dir( $path ) )
     1136                if ( ! is_dir( $path ) ) {
    11111137                        return false;
     1138                }
    11121139
    11131140                if ( $extensions ) {
    11141141                        $extensions = (array) $extensions;
    final class WP_Theme implements ArrayAccess { 
    11161143                }
    11171144
    11181145                $relative_path = trailingslashit( $relative_path );
    1119                 if ( '/' == $relative_path )
     1146                if ( '/' == $relative_path ) {
    11201147                        $relative_path = '';
     1148                }
    11211149
    11221150                $results = scandir( $path );
    11231151                $files = array();
    final class WP_Theme implements ArrayAccess { 
    11251153                /**
    11261154                 * Filters the array of excluded directories and files while scanning theme folder.
    11271155                 *
    1128                  * @since 4.7.4
     1156                 * @since 4.7.4
    11291157                 *
    11301158                 * @param array $exclusions Array of excluded directories and files.
    11311159                 */
    1132                 $exclusions = (array) apply_filters( 'theme_scandir_exclusions', array( 'CVS', 'node_modules' ) );
     1160                $exclusions = (array) apply_filters( 'theme_scandir_exclusions', array( 'CVS', 'node_modules', 'vendor', 'bower_components' ) );
    11331161
    11341162                foreach ( $results as $result ) {
    11351163                        if ( '.' == $result[0] || in_array( $result, $exclusions, true ) ) {
    11361164                                continue;
    11371165                        }
    11381166                        if ( is_dir( $path . '/' . $result ) ) {
    1139                                 if ( ! $depth )
     1167                                if ( ! $depth ) {
    11401168                                        continue;
     1169                                }
    11411170                                $found = self::scandir( $path . '/' . $result, $extensions, $depth - 1 , $relative_path . $result );
    11421171                                $files = array_merge_recursive( $files, $found );
    11431172                        } elseif ( ! $extensions || preg_match( '~\.(' . $_extensions . ')$~', $result ) ) {
  • new file tests/phpunit/tests/functions/listFiles.php

    diff --git tests/phpunit/tests/functions/listFiles.php tests/phpunit/tests/functions/listFiles.php
    new file mode 100644
    index 0000000000..645a351e9b
    - +  
     1<?php
     2
     3/**
     4 * Test list_files().
     5 *
     6 * @group functions.php
     7 */
     8class Tests_Functions_ListFiles extends WP_UnitTestCase {
     9        public function test_list_files_returns_iterator() {
     10                $admin_files = list_files( ABSPATH . 'wp-admin/' );
     11                $this->assertInstanceOf( 'RecursiveIteratorIterator', $admin_files );
     12                $admin_files->rewind();
     13                $file = $admin_files->current();
     14                $this->assertInstanceOf( 'SplFileInfo', $file );
     15        }
     16
     17        public function test_list_files_returns_spl_file_info_objects() {
     18                $admin_files = list_files( ABSPATH . 'wp-admin/' );
     19                $admin_files->rewind();
     20                $file = $admin_files->current();
     21                $this->assertInstanceOf( 'SplFileInfo', $file );
     22                $this->assertContains( '/src/wp-admin/about.php', $file->getPathName() );
     23        }
     24
     25        public function test_list_files_can_be_cast_to_string() {
     26                $admin_files = list_files( ABSPATH . 'wp-admin/' );
     27                $admin_files->rewind();
     28                $file = $admin_files->current();
     29                $file = (string) $file;
     30                $this->assertInternalType( 'string', $file );
     31                $this->assertContains( '/src/wp-admin/about.php', $file );
     32        }
     33
     34        public function test_list_files_can_exclude_files() {
     35                $options = array(
     36                        'exclusions' => array( 'about.php' ),
     37                );
     38                $admin_files = list_files( ABSPATH . 'wp-admin/', null, $options );
     39                $admin_files->rewind();
     40                $file = $admin_files->current();
     41                $this->assertNotContains( '/src/wp-admin/about.php', $file->getPathName() );
     42        }
     43
     44        public function test_list_files_can_match_regex() {
     45                $options = array(
     46                        'regex' => '/.*\/options\.php$/i',
     47                );
     48                $admin_files = list_files( ABSPATH . 'wp-admin/', null, $options );
     49                $admin_files->rewind();
     50                $file = $admin_files->current();
     51                $this->assertContains( '/src/wp-admin/options.php', $file->getPathName() );
     52        }
     53}