From af2db32d2d856008e5a3f5815d88805eb937ca32 Mon Sep 17 00:00:00 2001
From: Jakob Perry <japerry@45640.no-reply.drupal.org>
Date: Mon, 1 Aug 2016 22:36:06 -0700
Subject: [PATCH] Issue 2329453

---
 core/includes/file.inc                              | 15 ++++++++++++++-
 .../Discovery/RecursiveExtensionFilterIterator.php  | 16 ++++++++++++++++
 .../Drupal/Core/Extension/ExtensionDiscovery.php    |  8 +++++++-
 .../system/src/Tests/Common/SystemListingTest.php   | 21 +++++++++++++++++++++
 .../system/src/Tests/File/ScanDirectoryTest.php     | 17 +++++++++++++++++
 .../system/tests/fixtures/IgnoreDirectories/a.txt   |  0
 .../IgnoreDirectories/frontend_framework/b.txt      |  0
 sites/default/default.settings.php                  | 11 +++++++++++
 8 files changed, 86 insertions(+), 2 deletions(-)
 create mode 100644 core/modules/system/tests/fixtures/IgnoreDirectories/a.txt
 create mode 100644 core/modules/system/tests/fixtures/IgnoreDirectories/frontend_framework/b.txt

diff --git a/core/includes/file.inc b/core/includes/file.inc
index 017a3d6..2cb7d7a 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;
 
@@ -1011,6 +1012,15 @@ 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', []);
+    $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
@@ -1019,7 +1029,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 e2d5c6f..ebdaa79 100644
--- a/core/modules/system/src/Tests/Common/SystemListingTest.php
+++ b/core/modules/system/src/Tests/Common/SystemListingTest.php
@@ -51,4 +51,25 @@ 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 d372671..4f3717d 100644
--- a/core/modules/system/src/Tests/File/ScanDirectoryTest.php
+++ b/core/modules/system/src/Tests/File/ScanDirectoryTest.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\system\Tests\File;
 
+use Drupal\Core\Site\Settings;
+
 /**
  * Tests the file_scan_directory() function.
  *
@@ -142,4 +144,19 @@ 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.
+   */
+  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".');
+
+    $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 d42962d..7d14535 100644
--- a/sites/default/default.settings.php
+++ b/sites/default/default.settings.php
@@ -702,6 +702,17 @@
  */
 
 /**
+ * The list of folders disabled for directory scanning.
+ *
+ * By default ignore node_modules and bower_components folders to avoid issues
+ * with Drupal recursive scanning.
+ */
+$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,
-- 
2.6.4 (Apple Git-63)

