From 7f45a8ab8a5797e93503a54dd2744dcbbc568bcb Mon Sep 17 00:00:00 2001
From: phenaproxima <phenaproxima@205645.no-reply.drupal.org>
Date: Fri, 23 Sep 2022 15:49:59 +0000
Subject: [PATCH] Issue #3298444 by phenaproxima, omkar.podey: Symlink
 validator should delegate to Composer Stager's precondition

---
 package_manager/package_manager.services.yml  |   4 +-
 .../src/PathExcluder/GitExcluder.php          |   3 +-
 .../src/Validator/SymlinkValidator.php        | 158 +++++++--------
 .../Kernel/PathExcluder/GitExcluderTest.php   |  24 ---
 .../tests/src/Kernel/SymlinkValidatorTest.php | 183 +++++-------------
 5 files changed, 121 insertions(+), 251 deletions(-)

diff --git a/package_manager/package_manager.services.yml b/package_manager/package_manager.services.yml
index cb63a5f1b1..ad96bab6dc 100644
--- a/package_manager/package_manager.services.yml
+++ b/package_manager/package_manager.services.yml
@@ -105,8 +105,10 @@ services:
   package_manager.validator.symlink:
     class: Drupal\package_manager\Validator\SymlinkValidator
     arguments:
-      - '@module_handler'
       - '@package_manager.path_locator'
+      - '@PhpTuf\ComposerStager\Domain\Service\Precondition\CodebaseContainsNoSymlinksInterface'
+      - '@PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface'
+      - '@module_handler'
     tags:
       - { name: event_subscriber }
   package_manager.validator.duplicate_info_file:
diff --git a/package_manager/src/PathExcluder/GitExcluder.php b/package_manager/src/PathExcluder/GitExcluder.php
index c713ddd03e..5ec84d0680 100644
--- a/package_manager/src/PathExcluder/GitExcluder.php
+++ b/package_manager/src/PathExcluder/GitExcluder.php
@@ -56,8 +56,7 @@ final class GitExcluder implements EventSubscriberInterface {
       ->directories()
       ->name('.git')
       ->ignoreVCS(FALSE)
-      ->ignoreDotFiles(FALSE)
-      ->ignoreUnreadableDirs();
+      ->ignoreDotFiles(FALSE);
 
     $paths = [];
     foreach ($finder as $git_directory) {
diff --git a/package_manager/src/Validator/SymlinkValidator.php b/package_manager/src/Validator/SymlinkValidator.php
index 40eab73939..b8b5550826 100644
--- a/package_manager/src/Validator/SymlinkValidator.php
+++ b/package_manager/src/Validator/SymlinkValidator.php
@@ -10,7 +10,9 @@ use Drupal\package_manager\Event\PreCreateEvent;
 use Drupal\package_manager\Event\PreOperationStageEvent;
 use Drupal\package_manager\Event\StatusCheckEvent;
 use Drupal\package_manager\PathLocator;
-use Symfony\Component\Finder\Finder;
+use PhpTuf\ComposerStager\Domain\Exception\PreconditionException;
+use PhpTuf\ComposerStager\Domain\Service\Precondition\CodebaseContainsNoSymlinksInterface;
+use PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface;
 
 /**
  * Flags errors if the project root or staging area contain symbolic links.
@@ -27,13 +29,6 @@ class SymlinkValidator implements PreOperationStageValidatorInterface {
 
   use StringTranslationTrait;
 
-  /**
-   * The module handler service.
-   *
-   * @var \Drupal\Core\Extension\ModuleHandlerInterface
-   */
-  protected $moduleHandler;
-
   /**
    * The path locator service.
    *
@@ -42,77 +37,85 @@ class SymlinkValidator implements PreOperationStageValidatorInterface {
   protected $pathLocator;
 
   /**
-   * Constructs a SymlinkValidator object.
+   * The Composer Stager precondition that this validator wraps.
    *
-   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
-   *   The module handler service.
-   * @param \Drupal\package_manager\PathLocator $path_locator
-   *   The path locator service.
+   * @var \PhpTuf\ComposerStager\Domain\Service\Precondition\CodebaseContainsNoSymlinksInterface
    */
-  public function __construct(ModuleHandlerInterface $module_handler, PathLocator $path_locator) {
-    $this->moduleHandler = $module_handler;
-    $this->pathLocator = $path_locator;
-  }
+  protected $precondition;
 
   /**
-   * {@inheritdoc}
+   * The path factory service.
+   *
+   * @var \PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface
    */
-  public function validateStagePreOperation(PreOperationStageEvent $event): void {
-    $dir = $this->pathLocator->getProjectRoot();
-
-    if ($this->hasLinks($dir)) {
-      $this->addError('Symbolic links were found in the active directory, which are not supported at this time.', $event);
-    }
-  }
+  protected $pathFactory;
 
   /**
-   * Checks if the staging area has any symbolic links.
+   * The module handler service.
    *
-   * @param \Drupal\package_manager\Event\PreApplyEvent $event
-   *   The event object.
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
    */
-  public function preApply(PreApplyEvent $event): void {
-    $dir = $event->getStage()->getStageDirectory();
-
-    if ($this->hasLinks($dir)) {
-      $this->addError('Symbolic links were found in the staging area, which are not supported at this time.', $event);
-    }
-  }
+  protected $moduleHandler;
 
   /**
-   * Recursively checks if a directory has any symbolic links.
-   *
-   * @param string $dir
-   *   The path of the directory to check.
+   * Constructs a SymlinkValidator object.
    *
-   * @return bool
-   *   TRUE if the directory contains any symbolic links, FALSE otherwise.
+   * @param \Drupal\package_manager\PathLocator $path_locator
+   *   The path locator service.
+   * @param \PhpTuf\ComposerStager\Domain\Service\Precondition\CodebaseContainsNoSymlinksInterface $precondition
+   *   The Composer Stager precondition that this validator wraps.
+   * @param \PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface $path_factory
+   *   The path factory service.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler service.
    */
-  protected function hasLinks(string $dir): bool {
-    // Finder::filter() explicitly requires a closure, so create one from
-    // ::isLink() so that we can still override it for testing purposes.
-    $is_link = \Closure::fromCallable([$this, 'isLink']);
-
-    // Finder::hasResults() is more efficient than count() because it will
-    // return early if there is a match.
-    return Finder::create()
-      ->in($dir)
-      ->filter($is_link)
-      ->ignoreUnreadableDirs()
-      ->hasResults();
+  public function __construct(PathLocator $path_locator, CodebaseContainsNoSymlinksInterface $precondition, PathFactoryInterface $path_factory, ModuleHandlerInterface $module_handler) {
+    $this->pathLocator = $path_locator;
+    $this->precondition = $precondition;
+    $this->pathFactory = $path_factory;
+    $this->moduleHandler = $module_handler;
   }
 
   /**
-   * Checks if a file or directory is a symbolic link.
-   *
-   * @param \SplFileInfo $file
-   *   A value object for the file or directory.
-   *
-   * @return bool
-   *   TRUE if the file or directory is a symbolic link, FALSE otherwise.
+   * {@inheritdoc}
    */
-  protected function isLink(\SplFileInfo $file): bool {
-    return $file->isLink();
+  public function validateStagePreOperation(PreOperationStageEvent $event): void {
+    $active_dir = $this->pathFactory->create($this->pathLocator->getProjectRoot());
+
+    // The precondition requires us to pass both an active and stage directory,
+    // so if the stage hasn't been created or claimed yet, use the directory
+    // that contains this file, which contains only a few files and no symlinks,
+    // as the stage directory. The precondition itself doesn't care if the
+    // directory actually exists or not.
+    try {
+      $stage_dir = $event->getStage()->getStageDirectory();
+    }
+    catch (\LogicException $e) {
+      $stage_dir = __DIR__;
+    }
+    $stage_dir = $this->pathFactory->create($stage_dir);
+
+    try {
+      $this->precondition->assertIsFulfilled($active_dir, $stage_dir);
+    }
+    catch (PreconditionException $e) {
+      $message = $e->getMessage();
+
+      // If the Help module is enabled, append a link to Package Manager's help
+      // page.
+      // @see package_manager_help()
+      if ($this->moduleHandler->moduleExists('help')) {
+        $url = Url::fromRoute('help.page', ['name' => 'package_manager'])
+          ->setOption('fragment', 'package-manager-faq-symlinks-found')
+          ->toString();
+
+        $message = $this->t('@message See <a href=":package-manager-help">the help page</a> for information on how to resolve the problem.', [
+          '@message' => $message,
+          ':package-manager-help' => $url,
+        ]);
+      }
+      $event->addError([$message]);
+    }
   }
 
   /**
@@ -121,38 +124,9 @@ class SymlinkValidator implements PreOperationStageValidatorInterface {
   public static function getSubscribedEvents() {
     return [
       PreCreateEvent::class => 'validateStagePreOperation',
+      PreApplyEvent::class => 'validateStagePreOperation',
       StatusCheckEvent::class => 'validateStagePreOperation',
-      PreApplyEvent::class => [
-        ['validateStagePreOperation'],
-        ['preApply'],
-      ],
     ];
   }
 
-  /**
-   * Adds a validation error to a given event.
-   *
-   * @param string $message
-   *   The error message. If the Help module is enabled, a link to Package
-   *   Manager's help page will be appended.
-   * @param \Drupal\package_manager\Event\PreApplyEvent|\Drupal\package_manager\Event\PreOperationStageEvent $event
-   *   The event to add the error to.
-   *
-   * @see package_manager_help()
-   */
-  protected function addError(string $message, $event): void {
-    if ($this->moduleHandler->moduleExists('help')) {
-      $url = Url::fromRoute('help.page', ['name' => 'package_manager'])
-        ->setOption('fragment', 'package-manager-faq-symlinks-found')
-        ->toString();
-
-      $message = $this->t('@message See <a href=":package-manager-help">the help page</a> for information on how to resolve the problem.', [
-        '@message' => $message,
-        ':package-manager-help' => $url,
-      ]);
-    }
-
-    $event->addError([$message]);
-  }
-
 }
diff --git a/package_manager/tests/src/Kernel/PathExcluder/GitExcluderTest.php b/package_manager/tests/src/Kernel/PathExcluder/GitExcluderTest.php
index 0966b1fa55..4da701a076 100644
--- a/package_manager/tests/src/Kernel/PathExcluder/GitExcluderTest.php
+++ b/package_manager/tests/src/Kernel/PathExcluder/GitExcluderTest.php
@@ -2,7 +2,6 @@
 
 namespace Drupal\Tests\package_manager\Kernel\PathExcluder;
 
-use Drupal\package_manager_bypass\Beginner;
 use Drupal\Tests\package_manager\Kernel\PackageManagerKernelTestBase;
 
 /**
@@ -23,29 +22,6 @@ class GitExcluderTest extends PackageManagerKernelTestBase {
     parent::setUp();
   }
 
-  /**
-   * Tests that unreadable directories are ignored by the event subscriber.
-   */
-  public function testUnreadableDirectoriesAreIgnored(): void {
-    $active_dir = $this->container->get('package_manager.path_locator')
-      ->getProjectRoot();
-
-    // Create an unreadable directory within the active directory, which will
-    // raise an exception as the event subscriber tries to scan for .git
-    // directories...unless unreadable directories are being ignored, as they
-    // should be.
-    $unreadable_dir = $active_dir . '/unreadable';
-    mkdir($unreadable_dir, 0000);
-    $this->assertDirectoryIsNotReadable($unreadable_dir);
-
-    // Don't mirror the active directory into the virtual staging area, since
-    // the active directory contains an unreadable directory which will cause
-    // an exception.
-    Beginner::setFixturePath(NULL);
-
-    $this->createStage()->create();
-  }
-
   /**
    * Tests that Git directories are excluded from staging operations.
    */
diff --git a/package_manager/tests/src/Kernel/SymlinkValidatorTest.php b/package_manager/tests/src/Kernel/SymlinkValidatorTest.php
index fdda7d469e..acf84ca7f1 100644
--- a/package_manager/tests/src/Kernel/SymlinkValidatorTest.php
+++ b/package_manager/tests/src/Kernel/SymlinkValidatorTest.php
@@ -5,9 +5,10 @@ namespace Drupal\Tests\package_manager\Kernel;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\Url;
 use Drupal\package_manager\Event\PreCreateEvent;
-use Drupal\package_manager\Exception\StageValidationException;
 use Drupal\package_manager\ValidationResult;
-use Drupal\package_manager\Validator\SymlinkValidator;
+use PhpTuf\ComposerStager\Domain\Exception\PreconditionException;
+use PhpTuf\ComposerStager\Domain\Service\Precondition\CodebaseContainsNoSymlinksInterface;
+use Prophecy\Argument;
 
 /**
  * @covers \Drupal\package_manager\Validator\SymlinkValidator
@@ -17,144 +18,75 @@ use Drupal\package_manager\Validator\SymlinkValidator;
 class SymlinkValidatorTest extends PackageManagerKernelTestBase {
 
   /**
-   * {@inheritdoc}
+   * The mocked precondition that checks for symlinks.
+   *
+   * @var \PhpTuf\ComposerStager\Domain\Service\Precondition\CodebaseContainsNoSymlinksInterface|\Prophecy\Prophecy\ObjectProphecy
    */
-  public function register(ContainerBuilder $container) {
-    parent::register($container);
-
-    $container->getDefinition('package_manager.validator.symlink')
-      ->setClass(TestSymlinkValidator::class);
-  }
+  private $precondition;
 
   /**
-   * Tests that a symlink in the project root raises an error.
+   * {@inheritdoc}
    */
-  public function testSymlinkInProjectRoot(): void {
-    $result = ValidationResult::createError([
-      'Symbolic links were found in the active directory, which are not supported at this time.',
-    ]);
-
-    $active_dir = $this->container->get('package_manager.path_locator')
-      ->getProjectRoot();
-    // @see \Drupal\Tests\package_manager\Kernel\TestSymlinkValidator::isLink()
-    touch($active_dir . '/modules/a_link');
-    $this->assertStatusCheckResults([$result]);
-    $this->assertResults([$result], PreCreateEvent::class);
-
-    $this->enableModules(['help']);
-    $this->assertStatusCheckResults($this->addHelpTextToResults([$result]));
-    $this->assertResultsWithHelp([$result], PreCreateEvent::class);
+  protected function setUp(): void {
+    $this->precondition = $this->prophesize(CodebaseContainsNoSymlinksInterface::class);
+    parent::setUp();
   }
 
   /**
-   * Tests that a symlink in the staging area raises an error.
-   *
-   * @dataProvider providerHelpEnabledOrNot
-   */
-  public function testSymlinkInStagingArea(bool $enable_help): void {
-    $expected_results = [ValidationResult::createError([
-        'Symbolic links were found in the staging area, which are not supported at this time.',
-      ]),
-    ];
-
-    if ($enable_help) {
-      $this->enableModules(['help']);
-      $expected_results = $this->addHelpTextToResults($expected_results);
-    }
-
-    $stage = $this->createStage();
-    $stage->create();
-    $stage->require(['composer/semver:^3']);
-
-    // @see \Drupal\Tests\package_manager\Kernel\TestSymlinkValidator::isLink()
-    touch($stage->getStageDirectory() . '/modules/a_link');
-
-    try {
-      $stage->apply();
-      $this->fail('Expected a validation error.');
-    }
-    catch (StageValidationException $e) {
-      $this->assertValidationResultsEqual($expected_results, $e->getResults());
-    }
-  }
-
-  /**
-   * Tests that symlinks in the project root and staging area raise an error.
-   *
-   * @dataProvider providerHelpEnabledOrNot
+   * {@inheritdoc}
    */
-  public function testSymlinkInProjectRootAndStagingArea(bool $enable_help): void {
-    $expected_results = [
-      ValidationResult::createError([
-        'Symbolic links were found in the active directory, which are not supported at this time.',
-      ]),
-      ValidationResult::createError([
-        'Symbolic links were found in the staging area, which are not supported at this time.',
-      ]),
-    ];
-
-    if ($enable_help) {
-      $this->enableModules(['help']);
-      $expected_results = $this->addHelpTextToResults($expected_results);
-    }
-
-    $stage = $this->createStage();
-    $stage->create();
-    $stage->require(['composer/semver:^3']);
-
-    $active_dir = $this->container->get('package_manager.path_locator')
-      ->getProjectRoot();
-    // @see \Drupal\Tests\package_manager\Kernel\TestSymlinkValidator::isLink()
-    touch($active_dir . '/modules/a_link');
-    touch($stage->getStageDirectory() . '/modules/a_link');
+  public function register(ContainerBuilder $container) {
+    parent::register($container);
 
-    try {
-      $stage->apply();
-      $this->fail('Expected a validation error.');
-    }
-    catch (StageValidationException $e) {
-      $this->assertValidationResultsEqual($expected_results, $e->getResults());
-    }
+    $container->getDefinition('package_manager.validator.symlink')
+      ->setArgument('$precondition', $this->precondition->reveal());
   }
 
   /**
-   * Data provider for test methods that test with and without the Help module.
+   * Data provider for ::testSymlink().
    *
    * @return array[]
    *   The test cases.
    */
-  public function providerHelpEnabledOrNot() {
+  public function providerSymlink(): array {
     return [
-      'help_module_enabled' => [TRUE],
-      'help_module_disabled' => [FALSE],
+      'no symlinks' => [FALSE],
+      'symlinks' => [TRUE],
     ];
   }
 
   /**
-   * Asserts that a set of validation results link to the Package Manager help.
+   * Tests that the validator invokes Composer Stager's symlink precondition.
+   *
+   * @param bool $symlinks_exist
+   *   Whether or not the precondition will detect symlinks.
    *
-   * @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.
+   * @dataProvider providerSymlink
    */
-  private function assertResultsWithHelp(array $expected_results, string $event_class = NULL): void {
-    $expected_results = $this->addHelpTextToResults($expected_results);
+  public function testSymlink(bool $symlinks_exist): void {
+    $arguments = Argument::cetera();
+    // The precondition should always be invoked.
+    $this->precondition->assertIsFulfilled($arguments)->shouldBeCalled();
+
+    if ($symlinks_exist) {
+      $exception = new PreconditionException($this->precondition->reveal(), 'Symlinks were found.');
+      $this->precondition->assertIsFulfilled($arguments)->willThrow($exception);
+
+      $expected_results = [
+        ValidationResult::createError([
+          $exception->getMessage(),
+        ]),
+      ];
+    }
+    else {
+      $expected_results = [];
+    }
+
     $this->assertStatusCheckResults($expected_results);
-    $this->assertResults($expected_results, $event_class);
-  }
+    $this->assertResults($expected_results, PreCreateEvent::class);
+
+    $this->enableModules(['help']);
 
-  /**
-   * Adds help text to results messages.
-   *
-   * @param \Drupal\package_manager\ValidationResult[] $results
-   *   The expected validation results.
-   *
-   * @return array
-   *   The new results.
-   */
-  public function addHelpTextToResults(array $results): array {
     $url = Url::fromRoute('help.page', ['name' => 'package_manager'])
       ->setOption('fragment', 'package-manager-faq-symlinks-found')
       ->toString();
@@ -164,25 +96,12 @@ class SymlinkValidatorTest extends PackageManagerKernelTestBase {
     $map = function (string $message) use ($url): string {
       return $message . ' See <a href="' . $url . '">the help page</a> for information on how to resolve the problem.';
     };
-    foreach ($results as $index => $result) {
+    foreach ($expected_results as $index => $result) {
       $messages = array_map($map, $result->getMessages());
-      $results[$index] = ValidationResult::createError($messages);
+      $expected_results[$index] = ValidationResult::createError($messages);
     }
-    return $results;
-  }
-
-}
-
-/**
- * A test validator that considers anything named 'a_link' to be a symlink.
- */
-class TestSymlinkValidator extends SymlinkValidator {
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function isLink(\SplFileInfo $file): bool {
-    return $file->getBasename() === 'a_link' || parent::isLink($file);
+    $this->assertStatusCheckResults($expected_results);
+    $this->assertResults($expected_results, PreCreateEvent::class);
   }
 
 }
-- 
GitLab