Skip to content
Snippets Groups Projects
Commit 09b5dd20 authored by Adam G-H's avatar Adam G-H
Browse files

Issue #3319507 by phenaproxima, Wim Leers, tedbow, TravisCarden: Add symlink...

Issue #3319507 by phenaproxima, Wim Leers, tedbow, TravisCarden: Add symlink support to Composer Stager 2.0, require that version, and simplify UX & tests accordingly
parent 28e682cf
No related branches found
No related tags found
No related merge requests found
{
"name": "drupal/automatic_updates",
"type": "drupal-module",
"description": "Drupal Automatic Updates",
"keywords": ["Drupal"],
"license": "GPL-2.0-or-later",
"homepage": "https://www.drupal.org/project/automatic_updates",
"minimum-stability": "dev",
"support": {
"issues": "https://www.drupal.org/project/issues/automatic_updates",
"source": "http://cgit.drupalcode.org/automatic_updates"
},
"require": {
"ext-json": "*",
"drupal/core": "^9.7 || ^10",
"php-tuf/composer-stager": "^1.2",
"composer-runtime-api": "^2.1"
},
"scripts": {
"phpcbf": "scripts/phpcbf.sh",
"phpcs": "scripts/phpcs.sh",
"test": [
"Composer\\Config::disableProcessTimeout",
"scripts/phpunit.sh"
"name": "drupal/automatic_updates",
"type": "drupal-module",
"description": "Drupal Automatic Updates",
"keywords": [
"Drupal"
],
"core-convert": "Drupal\\automatic_updates\\Development\\Converter::doConvert"
},
"scripts-descriptions": {
"phpcbf": "Automatically fixes standards violations where possible.",
"phpcs": "Checks code for standards compliance.",
"test": "Runs PHPUnit tests.",
"core-convert": "Converts this module to a core merge request. Excepts 2 arguments. 1) The core clone directory. 2) The core merge request branch."
},
"require-dev": {
"colinodell/psr-testlogger": "^1"
},
"license": "GPL-2.0-or-later",
"homepage": "https://www.drupal.org/project/automatic_updates",
"minimum-stability": "dev",
"support": {
"issues": "https://www.drupal.org/project/issues/automatic_updates",
"source": "http://cgit.drupalcode.org/automatic_updates"
},
"require": {
"ext-json": "*",
"drupal/core": "^9.7 || ^10",
"php-tuf/composer-stager": "2.0-alpha1",
"composer-runtime-api": "^2.1"
},
"scripts": {
"phpcbf": "scripts/phpcbf.sh",
"phpcs": "scripts/phpcs.sh",
"test": [
"Composer\\Config::disableProcessTimeout",
"scripts/phpunit.sh"
],
"core-convert": "Drupal\\automatic_updates\\Development\\Converter::doConvert"
},
"scripts-descriptions": {
"phpcbf": "Automatically fixes standards violations where possible.",
"phpcs": "Checks code for standards compliance.",
"test": "Runs PHPUnit tests.",
"core-convert": "Converts this module to a core merge request. Excepts 2 arguments. 1) The core clone directory. 2) The core merge request branch."
},
"autoload": {
"psr-4": {
"Drupal\\automatic_updates\\Development\\": "scripts/src"
......
......@@ -174,9 +174,9 @@
* - There is enough free disk space to do stage operations.
* - The Drupal site root and vendor directory are writable.
* - The current site is not part of a multisite.
* - The project root and stage directory don't contain any symbolic links.
*
* @todo Clarify symbolic link support in https://drupal.org/i/3319507.
* - The project root and stage directory don't contain any unsupported links.
* See https://github.com/php-tuf/composer-stager/tree/develop/src/Domain/Service/Precondition#symlinks
* for information about which types of symlinks are supported.
*
* Apart from base requirements, Package Manager also enforces certain
* constraints at various points of the stage life cycle (typically
......
......@@ -32,7 +32,6 @@ function package_manager_help($route_name, RouteMatchInterface $route_match) {
$output .= '<p>' . t("Because Package Manager modifies the current site's code base, it is intentionally limited in certain ways to prevent unexpected changes to the live site:") . '</p>';
$output .= '<ul>';
$output .= ' <li>' . t('It does not support Drupal multi-site installations.') . '</li>';
$output .= ' <li>' . t('It does not support symlinks. If you have any, see <a href="#package-manager-faq-composer-not-found">What if it says I have symlinks in my codebase?</a>.') . '</li>';
$output .= ' <li>' . t('It only allows supported Composer plugins. If you have any, see <a href="#package-manager-faq-unsupported-composer-plugin">What if it says I have unsupported Composer plugins in my codebase?</a>.') . '</li>';
$output .= ' <li>' . t('It does not automatically perform version control operations, e.g., with Git. Site administrators are responsible for committing updates.') . '</li>';
$output .= ' <li>' . t('It can only maintain one copy of the site at any given time. If a copy of the site already exists, another one cannot be created until the existing copy is destroyed.') . '</li>';
......@@ -51,36 +50,6 @@ function package_manager_help($route_name, RouteMatchInterface $route_match) {
$output .= '<pre><code>drush config:set package_manager.settings executables.composer /full/path/to/composer.phar</code></pre>';
// END: DELETE FROM CORE MERGE REQUEST
$output .= '<h4 id="package-manager-faq-symlinks-found">' . t('What if it says I have symlinks in my codebase?') . '</h4>';
$output .= '<p>' . t('A fresh Drupal installation should not have any symlinks, but third party libraries and custom code can add them. If Automatic Updates says you have some, run the following command in your terminal to find them:') . '</p>';
$output .= '<pre><code>';
$output .= 'cd /var/www # Wherever your active directory is located.' . PHP_EOL;
$output .= 'find . -type l';
$output .= '</code></pre>';
$output .= '<p>' . t("You might see output like the below, indicating symlinks in Drush's <code>docs</code> directory, as an example:") . '</p>';
$output .= '<pre><code>';
$output .= './vendor/drush/drush/docs/misc/icon_PhpStorm.png' . PHP_EOL;
$output .= './vendor/drush/drush/docs/img/favicon.ico' . PHP_EOL;
$output .= './vendor/drush/drush/docs/contribute/CONTRIBUTING.md' . PHP_EOL;
$output .= './vendor/drush/drush/docs/drush_logo-black.png' . PHP_EOL;
$output .= '</code></pre>';
$output .= '<h5>' . t('Composer libraries') . '</h5>';
$output .= '<p>' . t('Symlinks in Composer libraries can be addressed with <a href=":vendor-hardening-composer-plugin-documentation">Drupal\'s Vendor Hardening Composer Plugin</a>, which "removes extraneous directories from the project\'s vendor directory". Use it as follows.', [':vendor-hardening-composer-plugin-documentation' => 'https://www.drupal.org/docs/develop/using-composer/using-drupals-vendor-hardening-composer-plugin']) . '</p>';
$output .= '<p>' . t('First, add `drupal/core-vendor-hardening` to your Composer project:') . '</p>';
$output .= '<pre><code>composer require drupal/core-vendor-hardening</code></pre>';
$output .= '<p>' . t('Then, add the following to the `composer.json` in your site root to handle the most common, known culprits. Add your own as necessary.') . '</p>';
$output .= '<pre><code>';
$output .= '"extra": {' . PHP_EOL;
$output .= ' "drupal-core-vendor-hardening": {' . PHP_EOL;
$output .= ' "drush/drush": ["docs"],' . PHP_EOL;
$output .= ' "grasmash/yaml-expander": ["scenarios"]' . PHP_EOL;
$output .= ' }' . PHP_EOL;
$output .= '}' . PHP_EOL;
$output .= '</code></pre>';
$output .= '<p>' . t('The new configuration will take effect on the next Composer install or update event. Do this to apply it immediately:') . '</p>';
$output .= '<pre><code>composer install</code></pre>';
$output .= '<h4 id="package-manager-faq-unsupported-composer-plugin">' . t('What if it says I have unsupported Composer plugins in my codebase?') . '</h4>';
$output .= '<p>' . t('A fresh Drupal installation only uses supported Composer plugins, but some modules or themes may depend on additional Composer plugins. Please <a href=":new-issue">create a new issue</a> when you encounter this.', [
':new-issue' => 'https://www.drupal.org/node/add/project-issue/automatic_updates',
......@@ -98,9 +67,6 @@ function package_manager_help($route_name, RouteMatchInterface $route_match) {
$output .= '<p>' . t('If <code>cweagans/composer-patches</code> is installed, it must be defined as a dependency of the main project (i.e., it must be listed in the <code>require</code> or <code>require-dev</code> section of <code>composer.json</code>). You can run the following command in your site root to add it as a dependency of the main project:') . '</p>';
$output .= "<pre><code>composer require cweagans/composer-patches</code></pre>";
$output .= '<h5>' . t('Custom code') . '</h5>';
$output .= '<p>' . t('Symlinks are seldom truly necessary and should be avoided in your own code. No solution currently exists to get around them--they must be removed in order to use Automatic Updates.') . '</p>';
return $output;
}
}
<?php
declare(strict_types = 1);
namespace Drupal\package_manager;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use PhpTuf\ComposerStager\Domain\Service\Precondition\NoSymlinksPointToADirectoryInterface;
use PhpTuf\ComposerStager\Domain\Value\Path\PathInterface;
use PhpTuf\ComposerStager\Domain\Value\PathList\PathListInterface;
/**
* Checks if the code base contains any symlinks that point to a directory.
*
* Since rsync supports copying symlinks to directories, but Composer Stager's
* PHP file syncer doesn't, this precondition is automatically fulfilled if
* Package Manager is *explicitly* configured to use rsync.
*
* @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 NoSymlinksPointToADirectory implements NoSymlinksPointToADirectoryInterface {
use StringTranslationTrait;
/**
* Constructs a NoSymlinksPointToADirectory object.
*
* @param \PhpTuf\ComposerStager\Domain\Service\Precondition\NoSymlinksPointToADirectoryInterface $decorated
* The decorated precondition.
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* The config factory.
*/
public function __construct(
private NoSymlinksPointToADirectoryInterface $decorated,
private ConfigFactoryInterface $configFactory
) {}
/**
* {@inheritdoc}
*/
public function getName(): string {
return $this->decorated->getName();
}
/**
* {@inheritdoc}
*/
public function getDescription(): string {
return $this->decorated->getDescription();
}
/**
* {@inheritdoc}
*/
public function getStatusMessage(PathInterface $activeDir, PathInterface $stagingDir, ?PathListInterface $exclusions = NULL,): string {
if ($this->isUsingRsync()) {
return $this->t('Symlinks to directories are supported by the rsync file syncer.');
}
return $this->decorated->getStatusMessage($activeDir, $stagingDir, $exclusions);
}
/**
* {@inheritdoc}
*/
public function isFulfilled(PathInterface $activeDir, PathInterface $stagingDir, ?PathListInterface $exclusions = NULL,): bool {
return $this->isUsingRsync() || $this->decorated->isFulfilled($activeDir, $stagingDir, $exclusions);
}
/**
* {@inheritdoc}
*/
public function assertIsFulfilled(PathInterface $activeDir, PathInterface $stagingDir, ?PathListInterface $exclusions = NULL,): void {
if ($this->isUsingRsync()) {
return;
}
$this->decorated->assertIsFulfilled($activeDir, $stagingDir, $exclusions);
}
/**
* Indicates if Package Manager is explicitly configured to use rsync.
*
* @return bool
* TRUE if Package Manager is explicitly configured to use the rsync file
* syncer, FALSE otherwise.
*/
private function isUsingRsync(): bool {
$syncer = $this->configFactory->get('package_manager.settings')
->get('file_syncer');
return $syncer === 'rsync';
}
}
......@@ -7,6 +7,7 @@ namespace Drupal\package_manager;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;
use PhpTuf\ComposerStager\Domain\Core\Beginner\BeginnerInterface;
use PhpTuf\ComposerStager\Domain\Service\Precondition\NoSymlinksPointToADirectoryInterface;
/**
* Defines dynamic container services for Package Manager.
......@@ -84,6 +85,12 @@ final class PackageManagerServiceProvider extends ServiceProviderBase {
$container->setAlias($interface_name, $implementations[0]);
}
}
// Decorate certain Composer Stager preconditions.
$container->register(NoSymlinksPointToADirectory::class)
->setPublic(FALSE)
->setAutowired(TRUE)
->setDecoratedService(NoSymlinksPointToADirectoryInterface::class);
}
}
......@@ -4,22 +4,18 @@ declare(strict_types = 1);
namespace Drupal\package_manager\Validator;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\package_manager\Event\PreOperationStageEvent;
use Drupal\package_manager\PathLocator;
use PhpTuf\ComposerStager\Domain\Aggregate\PreconditionsTree\NoUnsupportedLinksExistInterface;
use PhpTuf\ComposerStager\Domain\Exception\PreconditionException;
use PhpTuf\ComposerStager\Domain\Service\Precondition\CodebaseContainsNoSymlinksInterface;
use PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface;
use PhpTuf\ComposerStager\Infrastructure\Value\PathList\PathList;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Flags errors if the project root or stage directory contain symbolic links.
* Flags errors if any unsupported symlinks exist.
*
* @todo Remove this when Composer Stager's PHP file copier handles symlinks
* without issues.
* @see https://github.com/php-tuf/composer-stager/tree/develop/src/Domain/Service/Precondition#symlinks
*
* @internal
* This is an internal part of Package Manager and may be changed or removed
......@@ -29,25 +25,21 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class SymlinkValidator implements EventSubscriberInterface {
use BaseRequirementValidatorTrait;
use StringTranslationTrait;
/**
* Constructs a SymlinkValidator object.
*
* @param \Drupal\package_manager\PathLocator $pathLocator
* The path locator service.
* @param \PhpTuf\ComposerStager\Domain\Service\Precondition\CodebaseContainsNoSymlinksInterface $precondition
* @param \PhpTuf\ComposerStager\Domain\Aggregate\PreconditionsTree\NoUnsupportedLinksExistInterface $precondition
* The Composer Stager precondition that this validator wraps.
* @param \PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface $pathFactory
* The path factory service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
* The module handler service.
*/
public function __construct(
protected PathLocator $pathLocator,
protected CodebaseContainsNoSymlinksInterface $precondition,
protected NoUnsupportedLinksExistInterface $precondition,
protected PathFactoryInterface $pathFactory,
protected ModuleHandlerInterface $moduleHandler,
) {}
/**
......@@ -82,22 +74,7 @@ class SymlinkValidator implements EventSubscriberInterface {
$this->precondition->assertIsFulfilled($active_dir, $stage_dir, new PathList($ignored_paths));
}
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]);
$event->addErrorFromThrowable($e);
}
}
......
......@@ -352,6 +352,12 @@ END;
// terminology), and the autoload information, so that the classes
// provided by the package will actually be loadable in the test site
// we're building.
if (str_starts_with($version, 'dev-')) {
[$version, $reference] = explode(' ', $version, 2);
}
else {
$reference = $version;
}
$packages[$name][$version] = [
'name' => $name,
'version' => $version,
......@@ -363,6 +369,11 @@ END;
'type' => 'path',
'url' => $path,
],
'source' => [
'type' => 'path',
'url' => $path,
'reference' => $reference,
],
'autoload' => $package_info['autoload'] ?? [],
];
// Composer plugins are loaded and activated as early as possible, and
......
......@@ -4,20 +4,11 @@ declare(strict_types = 1);
namespace Drupal\Tests\package_manager\Kernel;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Url;
use Drupal\package_manager\Event\CollectIgnoredPathsEvent;
use Drupal\package_manager\Event\PreApplyEvent;
use Drupal\package_manager\Event\PreCreateEvent;
use Drupal\package_manager\Event\StatusCheckEvent;
use Drupal\package_manager\Exception\StageEventException;
use Drupal\package_manager\PathLocator;
use Drupal\package_manager\ValidationResult;
use PhpTuf\ComposerStager\Domain\Exception\PreconditionException;
use PhpTuf\ComposerStager\Domain\Service\Precondition\CodebaseContainsNoSymlinksInterface;
use PhpTuf\ComposerStager\Domain\Value\Path\PathInterface;
use PhpTuf\ComposerStager\Domain\Value\PathList\PathListInterface;
use PHPUnit\Framework\Assert;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use PhpTuf\ComposerStager\Domain\Service\Host\HostInterface;
/**
* @covers \Drupal\package_manager\Validator\SymlinkValidator
......@@ -27,135 +18,205 @@ use Prophecy\Prophecy\ObjectProphecy;
class SymlinkValidatorTest extends PackageManagerKernelTestBase {
/**
* The mocked precondition that checks for symlinks.
*
* @var \PhpTuf\ComposerStager\Domain\Service\Precondition\CodebaseContainsNoSymlinksInterface|\Prophecy\Prophecy\ObjectProphecy
* Tests that relative symlinks within the same package are supported.
*/
private $precondition;
public function testSymlinksWithinSamePackage(): void {
$project_root = $this->container->get(PathLocator::class)
->getProjectRoot();
$drush_dir = $project_root . '/vendor/drush/drush';
mkdir($drush_dir . '/docs', 0777, TRUE);
touch($drush_dir . '/drush_logo-black.png');
// Relative symlinks must be made from their actual directory to be
// correctly evaluated.
chdir($drush_dir . '/docs');
symlink('../drush_logo-black.png', 'drush_logo-black.png');
// Switch back to the project root to ensure that the check isn't affected
// by which directory we happen to be in.
chdir($project_root);
$this->assertStatusCheckResults([]);
}
/**
* {@inheritdoc}
* Tests that hard links are not supported.
*/
protected function setUp(): void {
$this->precondition = $this->prophesize(CodebaseContainsNoSymlinksInterface::class);
parent::setUp();
public function testHardLinks(): void {
$project_root = $this->container->get(PathLocator::class)
->getProjectRoot();
link($project_root . '/composer.json', $project_root . '/composer.link');
$result = ValidationResult::createError([
t('The active directory at "@dir" contains hard links, which is not supported. The first one is "@dir/composer.json".', [
'@dir' => $project_root,
]),
]);
$this->assertStatusCheckResults([$result]);
}
/**
* {@inheritdoc}
* Tests that symlinks with absolute paths are not supported.
*/
public function register(ContainerBuilder $container) {
parent::register($container);
public function testAbsoluteSymlinks(): void {
$project_root = $this->container->get(PathLocator::class)
->getProjectRoot();
symlink($project_root . '/composer.json', $project_root . '/composer.link');
$result = ValidationResult::createError([
t('The active directory at "@dir" contains absolute links, which is not supported. The first one is "@dir/composer.link".', [
'@dir' => $project_root,
]),
]);
$this->assertStatusCheckResults([$result]);
}
/**
* Tests that relative symlinks cannot point outside the project root.
*/
public function testSymlinkPointingOutsideProjectRoot(): void {
$project_root = $this->container->get(PathLocator::class)
->getProjectRoot();
$parent_dir = dirname($project_root);
touch($parent_dir . '/hello.txt');
// Relative symlinks must be made from their actual directory to be
// correctly evaluated.
chdir($project_root);
symlink('../hello.txt', 'fail.txt');
$result = ValidationResult::createError([
t('The active directory at "@dir" contains links that point outside the codebase, which is not supported. The first one is "@dir/fail.txt".', [
'@dir' => $project_root,
]),
]);
$this->assertStatusCheckResults([$result]);
$this->assertResults([$result], PreCreateEvent::class);
}
$container->getDefinition('package_manager.validator.symlink')
->setArgument('$precondition', $this->precondition->reveal());
/**
* Tests that relative symlinks cannot point outside the stage directory.
*/
public function testSymlinkPointingOutsideStageDirectory(): void {
// The same check should apply to symlinks in the stage directory that
// point outside of it.
$stage = $this->createStage();
$stage->create();
$stage->require(['ext-json:*']);
$stage_dir = $stage->getStageDirectory();
$parent_dir = dirname($stage_dir);
touch($parent_dir . '/hello.txt');
// Relative symlinks must be made from their actual directory to be
// correctly evaluated.
chdir($stage_dir);
symlink('../hello.txt', 'fail.txt');
$result = ValidationResult::createError([
t('The staging directory at "@dir" contains links that point outside the codebase, which is not supported. The first one is "@dir/fail.txt".', [
'@dir' => $stage_dir,
]),
]);
try {
$stage->apply();
$this->fail('Expected an exception, but none was thrown.');
}
catch (StageEventException $e) {
$this->assertExpectedResultsFromException([$result], $e);
}
}
/**
* Data provider for ::testSymlink().
* Data provider for ::testSymlinkToDirectory().
*
* @return array[]
* The test cases.
*/
public function providerSymlink(): array {
$test_cases = [];
foreach ([PreApplyEvent::class, PreCreateEvent::class, StatusCheckEvent::class] as $event) {
$test_cases["$event event with no symlinks"] = [
FALSE,
[],
$event,
];
$test_cases["$event event with symlinks"] = [
TRUE,
public function providerSymlinkToDirectory(): array {
return [
'php' => [
'php',
[
ValidationResult::createError([t('Symlinks were found.')]),
ValidationResult::createError([
t('The active directory at "<PROJECT_ROOT>" contains symlinks that point to a directory, which is not supported. The first one is "<PROJECT_ROOT>/modules/custom/example_module".'),
]),
],
$event,
];
}
return $test_cases;
],
'rsync' => [
'rsync',
[],
],
];
}
/**
* Tests that the validator invokes Composer Stager's symlink precondition.
* Tests what happens when there is a symlink to a directory.
*
* @param bool $symlinks_exist
* Whether or not the precondition will detect symlinks.
* @param string $file_syncer
* The file syncer to use. Can be `php` or `rsync`.
* @param \Drupal\package_manager\ValidationResult[] $expected_results
* The expected validation results.
* @param string $event
* The event to test.
*
* @dataProvider providerSymlink
* @dataProvider providerSymlinkToDirectory
*/
public function testSymlink(bool $symlinks_exist, array $expected_results, string $event): void {
$add_ignored_path = function (CollectIgnoredPathsEvent $event): void {
$event->add(['ignore/me']);
};
$this->addEventTestListener($add_ignored_path, CollectIgnoredPathsEvent::class);
// Expected argument types for active directory, stage directory and ignored
// paths passed while checking if precondition is fulfilled.
// @see \PhpTuf\ComposerStager\Domain\Service\Precondition\PreconditionInterface::assertIsFulfilled()
$arguments = [
Argument::type(PathInterface::class),
Argument::type(PathInterface::class),
Argument::type(PathListInterface::class),
];
$listener = function () use ($arguments, $symlinks_exist): void {
// Ensure that the Composer Stager's symlink precondition is invoked.
$this->precondition->assertIsFulfilled(...$arguments)
->will(function (array $arguments) use ($symlinks_exist): void {
assert($this instanceof ObjectProphecy);
// Ensure that 'ignore/me' is present in ignored paths.
Assert::assertContains('ignore/me', $arguments[2]->getAll());
// Whether to simulate or not that a symlink is found in the active
// or staging directory (but outside the ignored paths).
if ($symlinks_exist) {
throw new PreconditionException($this->reveal(), 'Symlinks were found.');
}
})
->shouldBeCalled();
};
$this->addEventTestListener($listener, $event);
if ($event === StatusCheckEvent::class) {
$this->assertStatusCheckResults($expected_results);
}
else {
$this->assertResults($expected_results, $event);
}
public function testSymlinkToDirectory(string $file_syncer, array $expected_results): void {
$project_root = $this->container->get('package_manager.path_locator')
->getProjectRoot();
mkdir($project_root . '/modules/custom');
// Relative symlinks must be made from their actual directory to be
// correctly evaluated.
chdir($project_root . '/modules/custom');
symlink('../example', 'example_module');
$this->config('package_manager.settings')
->set('file_syncer', $file_syncer)
->save();
$this->assertStatusCheckResults($expected_results);
}
/**
* Tests the Composer Stager's symlink precondition with richer help.
* Tests that symlinks are not supported on Windows, even if they're safe.
*/
public function testSymlinksNotAllowedOnWindows(): void {
$host = $this->prophesize(HostInterface::class);
$host->isWindows()->willReturn(TRUE);
$this->container->set(HostInterface::class, $host->reveal());
$project_root = $this->container->get(PathLocator::class)
->getProjectRoot();
// Relative symlinks must be made from their actual directory to be
// correctly evaluated.
chdir($project_root);
symlink('composer.json', 'composer.link');
$result = ValidationResult::createError([
t('The active directory at "@dir" contains links, which is not supported on Windows. The first one is "@dir/composer.link".', [
'@dir' => $project_root,
]),
]);
$this->assertStatusCheckResults([$result]);
}
/**
* Tests that unsupported links are ignored if they're beneath excluded paths.
*
* @param bool $symlinks_exist
* Whether or not the precondition will detect symlinks.
* @param array $expected_results
* The expected validation results.
* @param string $event
* The event to test.
* @depends testAbsoluteSymlinks
*
* @dataProvider providerSymlink
* @covers \Drupal\package_manager\PathExcluder\GitExcluder
* @covers \Drupal\package_manager\PathExcluder\NodeModulesExcluder
*/
public function testHelpLink(bool $symlinks_exist, array $expected_results, string $event): void {
$this->enableModules(['help']);
$url = Url::fromRoute('help.page', ['name' => 'package_manager'])
->setOption('fragment', 'package-manager-faq-symlinks-found')
->toString();
// Reformat the provided results so that they all have the link to the
// online documentation appended to them.
$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 ($expected_results as $index => $result) {
$messages = array_map($map, $result->getMessages());
$expected_results[$index] = ValidationResult::createError($messages);
}
$this->testSymlink($symlinks_exist, $expected_results, $event);
public function testUnsupportedLinkBeneathExcludedPath(): void {
$project_root = $this->container->get(PathLocator::class)
->getProjectRoot();
// Create absolute symlinks (which are not supported by Composer Stager) in
// both `node_modules`, which is a regular directory, and `.git`, which is a
// hidden directory.
mkdir($project_root . '/node_modules');
symlink($project_root . '/composer.json', $project_root . '/node_modules/composer.link');
symlink($project_root . '/composer.json', $project_root . '/.git/composer.link');
$this->assertStatusCheckResults([]);
}
}
......@@ -52,12 +52,6 @@ composer config \
# Prevent Composer from symlinking path repositories.
export COMPOSER_MIRROR_PATH_REPOS=1
# Prevent Composer from installing symlinks from common packages known to
# contain them.
# @see https://www.drupal.org/docs/develop/using-composer/using-drupals-vendor-hardening-composer-plugin
composer config --json extra.drupal-core-vendor-hardening.drush/drush '["docs"]'
composer config --json extra.drupal-core-vendor-hardening.grasmash/yaml-expander '["scenarios"]'
# Require the module using the checked out dev branch.
composer require \
--no-ansi \
......
......@@ -159,12 +159,6 @@ composer config \
# Prevent Composer from symlinking path repositories.
export COMPOSER_MIRROR_PATH_REPOS=1
# Prevent Composer from installing symlinks from common packages known to
# contain them.
# @see https://www.drupal.org/docs/develop/using-composer/using-drupals-vendor-hardening-composer-plugin
composer config --json extra.drupal-core-vendor-hardening.drush/drush '["docs"]'
composer config --json extra.drupal-core-vendor-hardening.grasmash/yaml-expander '["scenarios"]'
# Require the module using the checked out dev branch.
composer require \
--no-ansi \
......
......@@ -264,11 +264,6 @@ class CronUpdaterTest extends AutomaticUpdatesKernelTestBase {
'drupal' => __DIR__ . "/../../../package_manager/tests/fixtures/release-history/drupal.9.8.1-security.xml",
]);
// Disable the symlink validator so that this test isn't affected by
// symlinks that might be present in the running code base.
$validator = $this->container->get('package_manager.validator.symlink');
$this->container->get('event_dispatcher')->removeSubscriber($validator);
// If the pre- or post-destroy events throw an exception, it will not be
// caught by the cron updater, but it *will* be caught by the main cron
// service, which will log it as a cron error that we'll want to check for.
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment