From 41d71503d73b0c690763cec3fdadcb2c4277554c Mon Sep 17 00:00:00 2001
From: phenaproxima <phenaproxima@205645.no-reply.drupal.org>
Date: Tue, 9 Nov 2021 15:26:08 +0000
Subject: [PATCH] Issue #3226570 by phenaproxima, tedbow: Ensure that SQLite
 databases are excluded from the staging area

---
 package_manager/package_manager.services.yml  |  1 +
 .../ExcludedPathsSubscriber.php               | 26 ++++-
 .../fake_site/sites/example.com/db.sqlite     |  1 +
 .../fake_site/sites/example.com/db.sqlite-shm |  1 +
 .../fake_site/sites/example.com/db.sqlite-wal |  1 +
 .../src/Functional/ExcludedPathsTest.php      | 31 ++++--
 .../Kernel/ExcludedPathsSubscriberTest.php    | 99 +++++++++++++++++++
 .../Kernel/PackageManagerKernelTestBase.php   | 26 +++--
 8 files changed, 170 insertions(+), 16 deletions(-)
 create mode 100644 package_manager/tests/fixtures/fake_site/sites/example.com/db.sqlite
 create mode 100644 package_manager/tests/fixtures/fake_site/sites/example.com/db.sqlite-shm
 create mode 100644 package_manager/tests/fixtures/fake_site/sites/example.com/db.sqlite-wal
 create mode 100644 package_manager/tests/src/Kernel/ExcludedPathsSubscriberTest.php

diff --git a/package_manager/package_manager.services.yml b/package_manager/package_manager.services.yml
index 59bfe79c0e..63da64d195 100644
--- a/package_manager/package_manager.services.yml
+++ b/package_manager/package_manager.services.yml
@@ -131,5 +131,6 @@ services:
       - '%site.path%'
       - '@file_system'
       - '@stream_wrapper_manager'
+      - '@database'
     tags:
       - { name: event_subscriber }
diff --git a/package_manager/src/EventSubscriber/ExcludedPathsSubscriber.php b/package_manager/src/EventSubscriber/ExcludedPathsSubscriber.php
index f7e601c3f0..4663284c79 100644
--- a/package_manager/src/EventSubscriber/ExcludedPathsSubscriber.php
+++ b/package_manager/src/EventSubscriber/ExcludedPathsSubscriber.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\package_manager\EventSubscriber;
 
+use Drupal\Core\Database\Connection;
 use Drupal\Core\File\FileSystemInterface;
 use Drupal\Core\StreamWrapper\LocalStream;
 use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
@@ -43,7 +44,14 @@ class ExcludedPathsSubscriber implements EventSubscriberInterface {
   protected $streamWrapperManager;
 
   /**
-   * Constructs an UpdateSubscriber.
+   * The database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * Constructs an ExcludedPathsSubscriber.
    *
    * @param string $app_root
    *   The Drupal root.
@@ -53,12 +61,15 @@ class ExcludedPathsSubscriber implements EventSubscriberInterface {
    *   The file system service.
    * @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager
    *   The stream wrapper manager service.
+   * @param \Drupal\Core\Database\Connection $database
+   *   The database connection.
    */
-  public function __construct(string $app_root, string $site_path, FileSystemInterface $file_system, StreamWrapperManagerInterface $stream_wrapper_manager) {
+  public function __construct(string $app_root, string $site_path, FileSystemInterface $file_system, StreamWrapperManagerInterface $stream_wrapper_manager, Connection $database) {
     $this->appRoot = $app_root;
     $this->sitePath = $site_path;
     $this->fileSystem = $file_system;
     $this->streamWrapperManager = $stream_wrapper_manager;
+    $this->database = $database;
   }
 
   /**
@@ -113,6 +124,17 @@ class ExcludedPathsSubscriber implements EventSubscriberInterface {
       $event->excludePath($this->sitePath . DIRECTORY_SEPARATOR . $settings_file);
       $event->excludePath($default_site . DIRECTORY_SEPARATOR . $settings_file);
     }
+
+    // If the database is SQLite, it might be located in the active directory
+    // and we should not stage it.
+    if ($this->database->driver() === 'sqlite') {
+      $options = $this->database->getConnectionOptions();
+      $database = str_replace($this->appRoot, NULL, $options['database']);
+      $database = ltrim($database, '/');
+      $event->excludePath($database);
+      $event->excludePath("$database-shm");
+      $event->excludePath("$database-wal");
+    }
   }
 
   /**
diff --git a/package_manager/tests/fixtures/fake_site/sites/example.com/db.sqlite b/package_manager/tests/fixtures/fake_site/sites/example.com/db.sqlite
new file mode 100644
index 0000000000..08874eba8b
--- /dev/null
+++ b/package_manager/tests/fixtures/fake_site/sites/example.com/db.sqlite
@@ -0,0 +1 @@
+This file should never be staged.
diff --git a/package_manager/tests/fixtures/fake_site/sites/example.com/db.sqlite-shm b/package_manager/tests/fixtures/fake_site/sites/example.com/db.sqlite-shm
new file mode 100644
index 0000000000..08874eba8b
--- /dev/null
+++ b/package_manager/tests/fixtures/fake_site/sites/example.com/db.sqlite-shm
@@ -0,0 +1 @@
+This file should never be staged.
diff --git a/package_manager/tests/fixtures/fake_site/sites/example.com/db.sqlite-wal b/package_manager/tests/fixtures/fake_site/sites/example.com/db.sqlite-wal
new file mode 100644
index 0000000000..08874eba8b
--- /dev/null
+++ b/package_manager/tests/fixtures/fake_site/sites/example.com/db.sqlite-wal
@@ -0,0 +1 @@
+This file should never be staged.
diff --git a/package_manager/tests/src/Functional/ExcludedPathsTest.php b/package_manager/tests/src/Functional/ExcludedPathsTest.php
index 3dae832352..fd28b082ab 100644
--- a/package_manager/tests/src/Functional/ExcludedPathsTest.php
+++ b/package_manager/tests/src/Functional/ExcludedPathsTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\package_manager\Functional;
 
+use Drupal\Core\Database\Driver\sqlite\Connection;
 use Drupal\Core\Site\Settings;
 use Drupal\package_manager\PathLocator;
 use Drupal\package_manager\Stage;
@@ -64,10 +65,12 @@ class ExcludedPathsTest extends BrowserTestBase {
     $path_locator->getActiveDirectory()->willReturn($active_dir);
     $path_locator->getStageDirectory()->willReturn($stage_dir);
 
+    $site_path = 'sites/example.com';
+
     // Ensure that we are using directories within the fake site fixture for
     // public and private files.
     $settings = Settings::getAll();
-    $settings['file_public_path'] = 'sites/example.com/files';
+    $settings['file_public_path'] = "$site_path/files";
     $settings['file_private_path'] = 'private';
     new Settings($settings);
 
@@ -76,7 +79,18 @@ class ExcludedPathsTest extends BrowserTestBase {
     $reflector = new \ReflectionObject($subscriber);
     $property = $reflector->getProperty('sitePath');
     $property->setAccessible(TRUE);
-    $property->setValue($subscriber, 'sites/example.com');
+    $property->setValue($subscriber, $site_path);
+
+    // Mock a SQLite database connection to a file in the active directory. The
+    // file should not be staged.
+    $database = $this->prophesize(Connection::class);
+    $database->driver()->willReturn('sqlite');
+    $database->getConnectionOptions()->willReturn([
+      'database' => $site_path . '/db.sqlite',
+    ]);
+    $property = $reflector->getProperty('database');
+    $property->setAccessible(TRUE);
+    $property->setValue($subscriber, $database->reveal());
 
     $stage = new Stage(
       $path_locator->reveal(),
@@ -91,11 +105,16 @@ class ExcludedPathsTest extends BrowserTestBase {
     $this->assertDirectoryExists($stage_dir);
     $this->assertDirectoryNotExists("$stage_dir/sites/simpletest");
     $this->assertFileNotExists("$stage_dir/vendor/web.config");
-    $this->assertDirectoryNotExists("$stage_dir/sites/example.com/files");
+    $this->assertDirectoryNotExists("$stage_dir/$site_path/files");
     $this->assertDirectoryNotExists("$stage_dir/private");
-    $this->assertFileNotExists("$stage_dir/sites/example.com/settings.php");
-    $this->assertFileNotExists("$stage_dir/sites/example.com/settings.local.php");
-    $this->assertFileNotExists("$stage_dir/sites/example.com/services.yml");
+    $this->assertFileNotExists("$stage_dir/$site_path/settings.php");
+    $this->assertFileNotExists("$stage_dir/$site_path/settings.local.php");
+    $this->assertFileNotExists("$stage_dir/$site_path/services.yml");
+    // SQLite databases and their support files should never be staged.
+    $this->assertFileNotExists("$stage_dir/$site_path/db.sqlite");
+    $this->assertFileNotExists("$stage_dir/$site_path/db.sqlite-shm");
+    $this->assertFileNotExists("$stage_dir/$site_path/db.sqlite-wal");
+    // Default site-specific settings files should never be staged.
     $this->assertFileNotExists("$stage_dir/sites/default/settings.php");
     $this->assertFileNotExists("$stage_dir/sites/default/settings.local.php");
     $this->assertFileNotExists("$stage_dir/sites/default/services.yml");
diff --git a/package_manager/tests/src/Kernel/ExcludedPathsSubscriberTest.php b/package_manager/tests/src/Kernel/ExcludedPathsSubscriberTest.php
new file mode 100644
index 0000000000..b44f7811d6
--- /dev/null
+++ b/package_manager/tests/src/Kernel/ExcludedPathsSubscriberTest.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace Drupal\Tests\package_manager\Kernel;
+
+use Drupal\Core\Database\Driver\sqlite\Connection;
+use Drupal\package_manager\Event\PreCreateEvent;
+use Drupal\package_manager\EventSubscriber\ExcludedPathsSubscriber;
+
+/**
+ * @covers \Drupal\package_manager\EventSubscriber\ExcludedPathsSubscriber
+ *
+ * @group package_manager
+ */
+class ExcludedPathsSubscriberTest extends PackageManagerKernelTestBase {
+
+  /**
+   * Data provider for ::testSqliteDatabaseExcluded().
+   *
+   * @return array[]
+   *   Sets of arguments to pass to the test method.
+   */
+  public function providerSqliteDatabaseExcluded(): array {
+    $drupal_root = $this->getDrupalRoot();
+
+    return [
+      'relative path, in site directory' => [
+        'sites/example.com/db.sqlite',
+        [
+          'sites/example.com/db.sqlite',
+          'sites/example.com/db.sqlite-shm',
+          'sites/example.com/db.sqlite-wal',
+        ],
+      ],
+      'relative path, at root' => [
+        'db.sqlite',
+        [
+          'db.sqlite',
+          'db.sqlite-shm',
+          'db.sqlite-wal',
+        ],
+      ],
+      'absolute path, in site directory' => [
+        $drupal_root . '/sites/example.com/db.sqlite',
+        [
+          'sites/example.com/db.sqlite',
+          'sites/example.com/db.sqlite-shm',
+          'sites/example.com/db.sqlite-wal',
+        ],
+      ],
+      'absolute path, at root' => [
+        $drupal_root . '/db.sqlite',
+        [
+          'db.sqlite',
+          'db.sqlite-shm',
+          'db.sqlite-wal',
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Tests that SQLite database paths are excluded from the staging area.
+   *
+   * The exclusion of SQLite databases from the staging area is functionally
+   * tested by \Drupal\Tests\package_manager\Functional\ExcludedPathsTest. The
+   * purpose of this test is to ensure that SQLite database paths are processed
+   * properly (e.g., converting an absolute path to a relative path) before
+   * being flagged for exclusion.
+   *
+   * @param string $database
+   *   The path of the SQLite database, as set in the database connection
+   *   options.
+   * @param string[] $expected_exclusions
+   *   The database paths which should be flagged for exclusion.
+   *
+   * @dataProvider providerSqliteDatabaseExcluded
+   *
+   * @see \Drupal\Tests\package_manager\Functional\ExcludedPathsTest
+   */
+  public function testSqliteDatabaseExcluded(string $database, array $expected_exclusions): void {
+    $connection = $this->prophesize(Connection::class);
+    $connection->driver()->willReturn('sqlite');
+    $connection->getConnectionOptions()->willReturn(['database' => $database]);
+
+    $subscriber = new ExcludedPathsSubscriber(
+      $this->getDrupalRoot(),
+      'sites/default',
+      $this->container->get('file_system'),
+      $this->container->get('stream_wrapper_manager'),
+      $connection->reveal()
+    );
+
+    $event = new PreCreateEvent($this->createStage());
+    $subscriber->preCreate($event);
+    // All of the expected exclusions should be flagged.
+    $this->assertEmpty(array_diff($expected_exclusions, $event->getExcludedPaths()));
+  }
+
+}
diff --git a/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php b/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
index 431220461f..846426f14f 100644
--- a/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
+++ b/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
@@ -46,16 +46,13 @@ abstract class PackageManagerKernelTestBase extends KernelTestBase {
   }
 
   /**
-   * Asserts validation results are returned from a stage life cycle event.
+   * Creates a stage object for testing purposes.
    *
-   * @param \Drupal\package_manager\ValidationResult[] $expected_results
-   *   The expected validation results.
-   * @param string|null $event_class
-   *   (optional) The class of the event which should return the results. Must
-   *   be passed if $expected_results is not empty.
+   * @return \Drupal\Tests\package_manager\Kernel\TestStage
+   *   A stage object, with test-only modifications.
    */
-  protected function assertResults(array $expected_results, string $event_class = NULL): void {
-    $stage = new TestStage(
+  protected function createStage(): TestStage {
+    return new TestStage(
       $this->container->get('package_manager.path_locator'),
       $this->container->get('package_manager.beginner'),
       $this->container->get('package_manager.stager'),
@@ -63,6 +60,19 @@ abstract class PackageManagerKernelTestBase extends KernelTestBase {
       $this->container->get('package_manager.cleaner'),
       $this->container->get('event_dispatcher'),
     );
+  }
+
+  /**
+   * Asserts validation results are returned from a stage life cycle event.
+   *
+   * @param \Drupal\package_manager\ValidationResult[] $expected_results
+   *   The expected validation results.
+   * @param string|null $event_class
+   *   (optional) The class of the event which should return the results. Must
+   *   be passed if $expected_results is not empty.
+   */
+  protected function assertResults(array $expected_results, string $event_class = NULL): void {
+    $stage = $this->createStage();
 
     try {
       $stage->create();
-- 
GitLab