Skip to content
Snippets Groups Projects
ExcludedPathsSubscriber.php 8.09 KiB
Newer Older
<?php

namespace Drupal\package_manager\EventSubscriber;

use Drupal\Core\Database\Connection;
use Drupal\Core\StreamWrapper\LocalStream;
use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
use Drupal\package_manager\Event\PreApplyEvent;
use Drupal\package_manager\Event\PreCreateEvent;
use Drupal\package_manager\Event\StageEvent;
use Drupal\package_manager\PathLocator;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;

/**
 * Defines an event subscriber to exclude certain paths from staging areas.
 */
class ExcludedPathsSubscriber implements EventSubscriberInterface {

  /**
   * The current site path, relative to the Drupal root.
   *
   * @var string
   */
  protected $sitePath;

  /**
   * The Symfony file system service.
   * @var \Symfony\Component\Filesystem\Filesystem
   */
  protected $fileSystem;

  /**
   * The stream wrapper manager service.
   *
   * @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface
   */
  protected $streamWrapperManager;

  /**
   * The database connection.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $database;

  /**
   * The path locator service.
   *
   * @var \Drupal\package_manager\PathLocator
   */
  protected $pathLocator;

  /**
   * Constructs an ExcludedPathsSubscriber.
   *
   * @param string $site_path
   *   The current site path, relative to the Drupal root.
   * @param \Symfony\Component\Filesystem\Filesystem $file_system
   *   The Symfony 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.
   * @param \Drupal\package_manager\PathLocator $path_locator
   *   The path locator service.
  public function __construct(string $site_path, Filesystem $file_system, StreamWrapperManagerInterface $stream_wrapper_manager, Connection $database, PathLocator $path_locator) {
    $this->sitePath = $site_path;
    $this->fileSystem = $file_system;
    $this->streamWrapperManager = $stream_wrapper_manager;
    $this->pathLocator = $path_locator;
   * Flags paths to be excluded, relative to the web root.
   * This should only be used for paths that, if they exist at all, are
   * *guaranteed* to exist within the web root.
   *
   * @param \Drupal\package_manager\Event\PreCreateEvent|\Drupal\package_manager\Event\PreApplyEvent $event
   * @param string[] $paths
   *   The paths to exclude. These should be relative to the web root, and will
   *   be made relative to the project root.
  protected function excludeInWebRoot(StageEvent $event, array $paths): void {
    $web_root = $this->pathLocator->getWebRoot();
    if ($web_root) {
      $web_root .= '/';
    }
    foreach ($paths as $path) {
      // Make the path relative to the project root by prefixing the web root.
      $event->excludePath($web_root . $path);
    }
   * Flags paths to be excluded, relative to the project root.
   * @param \Drupal\package_manager\Event\PreCreateEvent|\Drupal\package_manager\Event\PreApplyEvent $event
   * @param string[] $paths
   *   The paths to exclude. Absolute paths will be made relative to the project
   *   root; relative paths will be assumed to already be relative to the
   *   project root, and excluded as given.
  protected function excludeInProjectRoot(StageEvent $event, array $paths): void {
    $project_root = $this->pathLocator->getProjectRoot();

    foreach ($paths as $path) {
      // Make absolute paths relative to the project root.
      $path = str_replace($project_root, '', $path);
      $path = ltrim($path, '/');
      $event->excludePath($path);
  }

  /**
   * Excludes common paths from staging operations.
   *
   * @param \Drupal\package_manager\Event\PreApplyEvent|\Drupal\package_manager\Event\PreCreateEvent $event
   *   The event object.
   *
   * @see \Drupal\package_manager\Event\ExcludedPathsTrait::excludePath()
   */
  public function ignoreCommonPaths(StageEvent $event): void {
    // Compile two lists of paths to exclude: paths that are relative to the
    // project root, and paths that are relative to the web root.
    $web = $project = [];

    // Always ignore automated test directories. If they exist, they will be in
    // the web root.
    $web[] = 'sites/simpletest';

    // If the core-vendor-hardening plugin (used in the legacy-project template)
    // is present, it may have written security hardening files in the vendor
    // directory. They should always be ignored.
    $vendor_dir = $this->pathLocator->getVendorDirectory();
    $project[] = $vendor_dir . '/web.config';
    $project[] = $vendor_dir . '/.htaccess';

    // Ignore public and private files. These paths could be either absolute or
    // relative, depending on site settings. If they are absolute, treat them
    // as relative to the project root. Otherwise, treat them as relative to
    // the web root.
    $files = array_filter([
      $this->getFilesPath('public'),
      $this->getFilesPath('private'),
    ]);
    foreach ($files as $path) {
      if ($this->fileSystem->isAbsolutePath($path)) {
        $project[] = $path;
      }
      else {
        $web[] = $path;
      }
    // Ignore site-specific settings files, which are always in the web root.
    $settings_files = [
      'settings.php',
      'settings.local.php',
      'services.yml',
    ];
    foreach ($settings_files as $settings_file) {
      $web[] = $this->sitePath . '/' . $settings_file;
      $web[] = 'sites/default/' . $settings_file;

    // If the database is SQLite, it might be located in the active directory
    // and we should ignore it. Always treat it as relative to the project root.
    if ($this->database->driver() === 'sqlite') {
      $options = $this->database->getConnectionOptions();
      $project[] = $options['database'];
      $project[] = $options['database'] . '-shm';
      $project[] = $options['database'] . '-wal';
    // Find all .git directories in the project and exclude them. We cannot do
    // this with FileSystemInterface::scanDirectory() because it unconditionally
    // excludes anything starting with a dot.
    $finder = Finder::create()
      ->in($this->pathLocator->getProjectRoot())
      ->directories()
      ->name('.git')
      ->ignoreVCS(FALSE)

    foreach ($finder as $git_directory) {
      $project[] = $git_directory->getPathname();
    }

    $this->excludeInWebRoot($event, $web);
    $this->excludeInProjectRoot($event, $project);
  }

  /**
   * Reacts before staged changes are committed the active directory.
   *
   * @param \Drupal\package_manager\Event\PreApplyEvent $event
   *   The event object.
   */
  public function preApply(PreApplyEvent $event): void {
    // Don't copy anything from the staging area's sites/default.
    // @todo Make this a lot smarter in https://www.drupal.org/i/3228955.
    $this->excludeInWebRoot($event, ['sites/default']);

    $this->ignoreCommonPaths($event);
  }

  /**
   * Returns the storage path for a stream wrapper.
   *
   * This will only work for stream wrappers that extend
   * \Drupal\Core\StreamWrapper\LocalStream, which includes the stream wrappers
   * for public and private files.
   *
   * @param string $scheme
   *   The stream wrapper scheme.
   *
   * @return string|null
   *   The storage path for files using the given scheme, relative to the Drupal
   *   root, or NULL if the stream wrapper does not extend
   *   \Drupal\Core\StreamWrapper\LocalStream.
   */
  private function getFilesPath(string $scheme): ?string {
    $wrapper = $this->streamWrapperManager->getViaScheme($scheme);
    if ($wrapper instanceof LocalStream) {
      return $wrapper->getDirectoryPath();
    }
    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    return [
      PreCreateEvent::class => 'ignoreCommonPaths',