diff --git a/core/includes/file.inc b/core/includes/file.inc index e6490e9..5d03228 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; @@ -911,7 +912,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 @@ -944,6 +945,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 @@ -952,7 +965,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 554ff28..5c3ebc3 100644 --- a/core/lib/Drupal/Core/Extension/Discovery/RecursiveExtensionFilterIterator.php +++ b/core/lib/Drupal/Core/Extension/Discovery/RecursiveExtensionFilterIterator.php @@ -82,6 +82,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 @@ -102,6 +116,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 337629b..0d9283c 100644 --- a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php @@ -427,11 +427,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 cf29be4..9d4e2fc 100644 --- a/core/modules/system/src/Tests/Common/SystemListingTest.php +++ b/core/modules/system/src/Tests/Common/SystemListingTest.php @@ -51,4 +51,26 @@ function testDirectoryPrecedence() { } } + /** + * Tests that directories matching file_scan_ignore_directories are ignored + */ + public function testFileScanIgnoreDirectory() { + $listing = new ExtensionDiscovery(\Drupal::root(), FALSE); + $listing->setProfileDirectories(array('core/profiles/testing')); + $files = $listing->scan('module'); + $this->assertArrayHasKey('drupal_system_listing_compatible_test', $files); + + // 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->setSetting('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->assertArrayNotHasKey('drupal_system_listing_compatible_test', $files); + } + } 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/core/tests/Drupal/KernelTests/Core/File/ScanDirectoryTest.php b/core/tests/Drupal/KernelTests/Core/File/ScanDirectoryTest.php index 2e753f6..e46073c 100644 --- a/core/tests/Drupal/KernelTests/Core/File/ScanDirectoryTest.php +++ b/core/tests/Drupal/KernelTests/Core/File/ScanDirectoryTest.php @@ -142,4 +142,25 @@ function testOptionMinDepth() { $this->assertTrue(empty($files), 'Minimum-depth of 1 successfully excludes files from current directory.'); } + /** + * Tests file_scan_directory() obeys 'file_scan_ignore_directories' setting. + */ + public function testIgnoreDirectories() { + $files = file_scan_directory('core/modules/system/tests/fixtures/IgnoreDirectories', '/\.txt$/'); + $this->assertCount(2, $files, '2 text files found when not ignoring directories.'); + + $this->setSetting('file_scan_ignore_directories', ['frontend_framework']); + $files = file_scan_directory('core/modules/system/tests/fixtures/IgnoreDirectories', '/\.txt$/'); + $this->assertCount(1, $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->setSetting('file_scan_ignore_directories', ['frontend.*']); + $files = file_scan_directory('core/modules/system/tests/fixtures/IgnoreDirectories', '/\.txt$/'); + $this->assertCount(2, $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->assertCount(2, $files, '2 text files found when an "nomask" option passed in.'); + } + } diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index 770a3a7..5b11dc1 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -719,6 +719,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,