diff --git a/core/includes/file.inc b/core/includes/file.inc index 3e5eb8c..d241e15 100644 --- a/core/includes/file.inc +++ b/core/includes/file.inc @@ -10,6 +10,7 @@ use Drupal\Component\PhpStorage\FileStorage; use Drupal\Component\Utility\Bytes; use Drupal\Core\File\FileSystem; +use Drupal\Core\Site\Settings; use Drupal\Core\StreamWrapper\PublicStream; use Drupal\Core\StreamWrapper\PrivateStream; @@ -913,7 +914,7 @@ function file_unmanaged_save_data($data, $destination = NULL, $replace = FILE_EX * @param $options * An associative array of additional options, with the following elements: * - 'nomask': The preg_match() regular expression for files to be excluded. - * There is no default. + * Defaults to the 'file_scan_ignore_directories' setting. * - 'callback': The callback function to call for each match. There is no * default callback. * - 'recurse': When TRUE, the directory scan will recurse the entire tree @@ -946,6 +947,18 @@ function file_scan_directory($dir, $mask, $options = array(), $depth = 0) { $dir_has_slash = (substr($dir, -1) === '/'); } + // Allow directories specified in settings.php to be ignored. You can use this + // to not check for files in common special-purpose directories. For example, + // node_modules and bower_components. Ignoring irrelevant directories is a + // performance boost. + if (!isset($options['nomask'])) { + $ignore_directories = Settings::get('file_scan_ignore_directories', []); + array_walk($ignore_directories, function(&$value) { + $value = preg_quote($value, '/'); + }); + $default_nomask = '/^' . implode('|', $ignore_directories) . '$/'; + } + $options['key'] = in_array($options['key'], array('uri', 'filename', 'name')) ? $options['key'] : 'uri'; $files = array(); // Avoid warnings when opendir does not have the permissions to open a @@ -954,7 +967,10 @@ function file_scan_directory($dir, $mask, $options = array(), $depth = 0) { if ($handle = @opendir($dir)) { while (FALSE !== ($filename = readdir($handle))) { // Skip this file if it matches the nomask or starts with a dot. - if ($filename[0] != '.' && !(isset($options['nomask']) && preg_match($options['nomask'], $filename))) { + if ($filename[0] != '.' + && !(isset($options['nomask']) && preg_match($options['nomask'], $filename)) + && !(!empty($default_nomask) && preg_match($default_nomask, $filename)) + ) { if ($depth == 0 && $dir_has_slash) { $uri = "$dir$filename"; } diff --git a/core/lib/Drupal/Core/Extension/Discovery/RecursiveExtensionFilterIterator.php b/core/lib/Drupal/Core/Extension/Discovery/RecursiveExtensionFilterIterator.php index a74dcfc..79a2726 100644 --- a/core/lib/Drupal/Core/Extension/Discovery/RecursiveExtensionFilterIterator.php +++ b/core/lib/Drupal/Core/Extension/Discovery/RecursiveExtensionFilterIterator.php @@ -87,6 +87,20 @@ class RecursiveExtensionFilterIterator extends \RecursiveFilterIterator { protected $acceptTests = FALSE; /** + * Construct a RecursiveExtensionFilterIterator. + * + * @param \RecursiveIterator $iterator + * The iterator to filter. + * @param array $blacklist + * (optional) Add to the blacklist of directories that should be filtered + * out during the iteration. + */ + public function __construct(\RecursiveIterator $iterator, array $blacklist = []) { + parent::__construct($iterator); + $this->blacklist = array_merge($this->blacklist, $blacklist); + } + + /** * Controls whether test directories will be scanned. * * @param bool $flag @@ -107,6 +121,8 @@ public function acceptTests($flag = FALSE) { */ public function getChildren() { $filter = parent::getChildren(); + // Pass on the blacklist. + $filter->blacklist = $this->blacklist; // Pass the $acceptTests flag forward to child iterators. $filter->acceptTests($this->acceptTests); return $filter; diff --git a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php index 3d68561..9da0b01 100644 --- a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php @@ -433,11 +433,17 @@ protected function scanDirectory($dir, $include_tests) { $flags |= \FilesystemIterator::CURRENT_AS_SELF; $directory_iterator = new \RecursiveDirectoryIterator($absolute_dir, $flags); + // Allow directories specified in settings.php to be ignored. You can use + // this to not check for files in common special-purpose directories. For + // example, node_modules and bower_components. Ignoring irrelevant + // directories is a performance boost. + $ignore_directories = Settings::get('file_scan_ignore_directories', []); + // Filter the recursive scan to discover extensions only. // Important: Without a RecursiveFilterIterator, RecursiveDirectoryIterator // would recurse into the entire filesystem directory tree without any kind // of limitations. - $filter = new RecursiveExtensionFilterIterator($directory_iterator); + $filter = new RecursiveExtensionFilterIterator($directory_iterator, $ignore_directories); $filter->acceptTests($include_tests); // The actual recursive filesystem scan is only invoked by instantiating the diff --git a/core/modules/system/src/Tests/Common/SystemListingTest.php b/core/modules/system/src/Tests/Common/SystemListingTest.php index a9d1549..ec3fcda 100644 --- a/core/modules/system/src/Tests/Common/SystemListingTest.php +++ b/core/modules/system/src/Tests/Common/SystemListingTest.php @@ -55,4 +55,27 @@ function testDirectoryPrecedence() { ))); } } + + /** + * Tests that directories matching file_scan_ignore_directories are ignored + */ + function testFileScanIgnoreDirectory() { + $listing = new ExtensionDiscovery(\Drupal::root(), FALSE); + $listing->setProfileDirectories(array('core/profiles/testing')); + $files = $listing->scan('module'); + $this->assertTrue(isset($files['drupal_system_listing_compatible_test'])); + + // Reset the static to force a rescan of the directories. + $reflected_class = new \ReflectionClass(ExtensionDiscovery::class); + $reflected_property = $reflected_class->getProperty('files'); + $reflected_property->setAccessible(true); + $reflected_property->setValue($reflected_class, []); + + $this->settingsSet('file_scan_ignore_directories', ['drupal_system_listing_compatible_test']); + $listing = new ExtensionDiscovery(\Drupal::root(), FALSE); + $listing->setProfileDirectories(array('core/profiles/testing')); + $files = $listing->scan('module'); + $this->assertFalse(isset($files['drupal_system_listing_compatible_test'])); + } + } diff --git a/core/modules/system/src/Tests/File/ScanDirectoryTest.php b/core/modules/system/src/Tests/File/ScanDirectoryTest.php index 4b067f0..9c85d16 100644 --- a/core/modules/system/src/Tests/File/ScanDirectoryTest.php +++ b/core/modules/system/src/Tests/File/ScanDirectoryTest.php @@ -146,4 +146,26 @@ function testOptionMinDepth() { $files = file_scan_directory($this->path, '/^javascript-/', array('min_depth' => 1)); $this->assertTrue(empty($files), 'Minimum-depth of 1 successfully excludes files from current directory.'); } + + /** + * Tests file_scan_directory() obeys 'file_scan_ignore_directories' setting. + */ + function testIgnoreDirectories() { + $files = file_scan_directory('core/modules/system/tests/fixtures/IgnoreDirectories', '/\.txt$/'); + $this->assertEqual(2, count($files), '2 text files found when not ignoring directories.'); + + $this->settingsSet('file_scan_ignore_directories', ['frontend_framework']); + $files = file_scan_directory('core/modules/system/tests/fixtures/IgnoreDirectories', '/\.txt$/'); + $this->assertEqual(1, count($files), '1 text files found when ignoring directories called "frontend_framework".'); + + // Ensure that the directories in file_scan_ignore_directories are escaped + // using preg_quote. + $this->settingsSet('file_scan_ignore_directories', ['frontend.*']); + $files = file_scan_directory('core/modules/system/tests/fixtures/IgnoreDirectories', '/\.txt$/'); + $this->assertEqual(2, count($files), '2 text files found when ignoring a directory that is not there.'); + + $files = file_scan_directory('core/modules/system/tests/fixtures/IgnoreDirectories', '/\.txt$/', ['nomask' => '/^something_thing_else$/']); + $this->assertEqual(2, count($files), '2 text files found when an "nomasl" option passed in.'); + } + } diff --git a/core/modules/system/tests/fixtures/IgnoreDirectories/a.txt b/core/modules/system/tests/fixtures/IgnoreDirectories/a.txt new file mode 100644 index 0000000..e69de29 diff --git a/core/modules/system/tests/fixtures/IgnoreDirectories/frontend_framework/b.txt b/core/modules/system/tests/fixtures/IgnoreDirectories/frontend_framework/b.txt new file mode 100644 index 0000000..e69de29 diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index 7f28c29..c99aba9 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -705,6 +705,21 @@ */ /** + * The default list of directories that will be ignored by Drupal's file API's. + * + * By default ignore node_modules and bower_components folders to avoid issues + * with common frontend tools and recursive scanning of directories looking for + * extensions. + * + * @see file_scan_directory() + * @see \Drupal\Core\Extension\ExtensionDiscovery::scanDirectory() + */ +$settings['file_scan_ignore_directories'] = [ + 'node_modules', + 'bower_components', +]; + +/** * Load local development override configuration, if available. * * Use settings.local.php to override variables on secondary (staging,