Skip to content
Snippets Groups Projects
Forked from project / automatic_updates
228 commits behind the upstream repository.
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
UnknownPathExcluder.php 4.33 KiB
<?php

declare(strict_types = 1);

namespace Drupal\package_manager\PathExcluder;

use Drupal\Component\Serialization\Json;
use Drupal\package_manager\ComposerInspector;
use Drupal\package_manager\Event\CollectIgnoredPathsEvent;
use Drupal\package_manager\PathLocator;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Excludes unknown paths from stage operations.
 *
 * Any paths in the root directory of the project that are NOT one of the
 * following are considered unknown paths:
 * 1. The vendor directory
 * 2. The web root
 * 3. composer.json
 * 4. composer.lock
 * 5. Scaffold files as determined by the drupal/core-composer-scaffold plugin
 *
 * If web root and project root are the same, nothing is excluded.
 *
 * @internal
 *   This is an internal part of Package Manager and may be changed or removed
 *   at any time without warning. External code should not interact with this
 *   class.
 */
final class UnknownPathExcluder implements EventSubscriberInterface {

  use PathExclusionsTrait;

  /**
   * Constructs a UnknownPathExcluder object.
   *
   * @param \Drupal\package_manager\ComposerInspector $composerInspector
   *   The Composer inspector service.
   * @param \Drupal\package_manager\PathLocator $path_locator
   *   The path locator service.
   */
  public function __construct(private ComposerInspector $composerInspector, PathLocator $path_locator) {
    $this->pathLocator = $path_locator;
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents(): array {
    return [
      CollectIgnoredPathsEvent::class => 'excludeUnknownPaths',
    ];
  }

  /**
   * Excludes unknown paths from stage operations.
   *
   * @param \Drupal\package_manager\Event\CollectIgnoredPathsEvent $event
   *   The event object.
   *
   * @throws \Exception
   *   See \Drupal\package_manager\ComposerInspector::validate().
   */
  public function excludeUnknownPaths(CollectIgnoredPathsEvent $event): void {
    $project_root = $this->pathLocator->getProjectRoot();
    $web_root = $project_root . DIRECTORY_SEPARATOR . $this->pathLocator->getWebRoot();
    if (realpath($web_root) === $project_root) {
      return;
    }

    // To determine the scaffold files to exclude, the installed packages must
    // be known, and that requires Composer commands to be able to run. This
    // intentionally does not catch exceptions: failed Composer validation in
    // the project root implies that this excluder cannot function correctly.
    // Note: the call to ComposerInspector::getInstalledPackagesList() would
    // also have triggered this, but explicitness is preferred here.
    // @see \Drupal\package_manager\StatusCheckTrait::runStatusCheck()
    $this->composerInspector->validate($project_root);

    $vendor_dir = $this->pathLocator->getVendorDirectory();
    $scaffold_files_paths = $this->getScaffoldFiles();
    // Search for all files (including hidden ones) in project root.
    $paths_in_project_root = glob("$project_root/{,.}*", GLOB_BRACE);
    $paths = [];
    $known_paths = array_merge([$vendor_dir, $web_root, "$project_root/composer.json", "$project_root/composer.lock"], $scaffold_files_paths);
    foreach ($paths_in_project_root as $path_in_project_root) {
      if (!in_array($path_in_project_root, $known_paths, TRUE)) {
        $paths[] = $path_in_project_root;
      }
    }
    $this->excludeInProjectRoot($event, $paths);
  }

  /**
   * Gets the path of scaffold files, for example 'index.php' and 'robots.txt'.
   *
   * @return array
   *   The array of scaffold file paths.
   */
  private function getScaffoldFiles(): array {
    $project_root = $this->pathLocator->getProjectRoot();
    $core_packages = $this->composerInspector->getInstalledPackagesList($project_root)->getCorePackages();
    $scaffold_file_paths = [];
    // @todo The only core package that provides scaffold files is `drupal/core`
    //   so do we really need to loop across all core packages here?
    //   Intelligently load scaffold files in https://drupal.org/i/3343802.
    foreach ($core_packages as $package) {
      $extra = Json::decode($this->composerInspector->getConfig('extra', $package->path . '/composer.json'));
      if (isset($extra['drupal-scaffold']['file-mapping'])) {
        $scaffold_file_paths = array_merge($scaffold_file_paths, array_keys($extra['drupal-scaffold']['file-mapping']));
      }
    }
    return $scaffold_file_paths;
  }

}