Skip to content
Snippets Groups Projects
Commit 0f8f497e authored by Travis Carden's avatar Travis Carden Committed by Ted Bowman
Browse files

Issue #3305240 by TravisCarden: Add a link to the the symlink validation...

Issue #3305240 by TravisCarden: Add a link to the the symlink validation message in package_manager to the updated help page
parent c98cb4a5
No related branches found
No related tags found
No related merge requests found
Automatic Updates
---------------
### Requirements
- The Drupal project's codebase must be writable in order to use Automatic Updates. This includes Drupal, modules, themes and the Composer dependencies in the vendor directory. This makes Automatic Updates incompatible with some hosting platforms.
- The Composer executable must be in the PATH of the web server. If the Composer executable cannot be found the location can be set by adding
`$config['package_manager.settings']['executables']['composer'] = '/path/to/composer';` in `settings.php`
### Limitations
- Drupal multi-site installations are not supported.
- Automatic Updates does not support version control such as Git. It is the responsibility of site administrators to commit any updates to version control if needed.
- Automatic Updates does not support symlinks. See [What if Automatic Updates says I have symlinks in my codebase?](#what-if-automatic-updates-says-i-have-symlinks-in-my-codebase) for help if you have any.
### Updating contributed modules and themes
Automatic Updates includes a sub-module, Automatic Updates Extensions, which supports updating contributed modules and themes.
⚠️ ☢️️ **Automatic Updates Extensions is still experimental and under heavy development.** We encourage you to test it in your local development environment, or another low-stakes testing situation, but it is emphatically NOT ready for use in a production environment. ☢️ ⚠️
Package Manager is a framework for updating Drupal core and installing contributed modules and themes via Composer. It has no user interface, but it provides an API for creating a temporary copy of the current site, making changes to the copy, and then syncing those changes back into the live site.
### Automatic Updates Initiative
- Follow and read up on
[Ideas queue - Automatic Updates initiative](https://www.drupal.org/project/ideas/issues/2940731)
### FAQ
#### What if Automatic Updates says I have symlinks in my codebase?
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:
```shell
cd /var/www # Wherever your active directory is located.
find . -type l
```
You might see output like the below, indicating symlinks in Drush's `docs` directory, as an example:
Follow and read more on the [Automatic Updates Initiative overview and roadmap](https://www.drupal.org/project/ideas/issues/2940731).
```
./vendor/drush/drush/docs/misc/icon_PhpStorm.png
./vendor/drush/drush/docs/img/favicon.ico
./vendor/drush/drush/docs/contribute/CONTRIBUTING.md
./vendor/drush/drush/docs/drush_logo-black.png
```
### Updating contrib modules and themes
##### Composer libraries
Symlinks in Composer libraries can be addressed with [Drupal's Vendor Hardening Composer Plugin](https://www.drupal.org/docs/develop/using-composer/using-drupals-vendor-hardening-composer-plugin), which "removes extraneous directories from the project's vendor directory". Use it as follows.
First, add `drupal/core-vendor-hardening` to your Composer project:
```shell
composer require drupal/core-vendor-hardening
```
Then, add the following to the `composer.json` in your site root to handle the most common, known culprits. Add your own as necessary.
```json
"extra": {
"drupal-core-vendor-hardening": {
"drush/drush": ["docs"],
"grasmash/yaml-expander": ["scenarios"]
}
}
```
The new configuration will take effect on the next Composer install or update event. Do this to apply it immediately:
Automatic Updates includes a sub-module, Automatic Updates Extensions, which supports updating contributed modules and themes.
```shell
composer install
```
⚠️ ☢️️ **Automatic Updates Extensions is still experimental and under heavy development.** We encourage you to test it in your local development environment, or another low-stakes testing situation, but it is emphatically NOT ready for use in a production environment. ☢️ ⚠️
##### Custom code
### More info
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.
Get more details about the Package Manager module, once installed, at it help page (`admin/help/package_manager`).
......@@ -14,24 +14,71 @@ use Drupal\package_manager\Validator\ComposerExecutableValidator;
function package_manager_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.package_manager':
$output = '<h3>' . t('About') . '</h3>';
$output = '<h3 id="package-manager-about">' . t('About') . '</h3>';
$output .= '<p>' . t('Package Manager is a framework for updating Drupal core and installing contributed modules and themes via Composer. It has no user interface, but it provides an API for creating a temporary copy of the current site, making changes to the copy, and then syncing those changes back into the live site.') . '</p>';
$output .= '<p>' . t('Package Manager dispatches events before and after various operations, and external code can integrate with it by subscribing to those events. For more information, see <code>package_manager.api.php</code>.') . '</p>';
$output .= '<h3 id="package-manager-requirements">' . t('Requirements') . '</h3>';
$output .= '<p>' . t('Package Manager requires Composer @version or later available as an executable, and PHP must have permission to run it. The path to the executable may be stored in config, or it will be automatically detected. To set the path to Composer, you can add the following line to settings.php:', ['@version' => ComposerExecutableValidator::MINIMUM_COMPOSER_VERSION]) . '</p>';
$output .= "<p><code>\$config['package_manager.settings']['executables']['composer'] = '/full/path/to/composer';</code></p>";
// BEGIN: DELETE FROM CORE MERGE REQUEST
$output .= '<p>' . t('Or, if using Drush, you can run the following command:') . '</p>';
$output .= '<p><code>drush config:set package_manager.settings executables.composer /full/path/to/composer</code></p>';
// END: DELETE FROM CORE MERGE REQUEST
$output .= '<h3>' . t('Limitations') . '</h3>';
$output .= '<p>' . t("Because Package Manager modifies the current site's code base, it is intentionally limited in certain ways to prevent unexpected changes from being made to the live site:") . '</p>';
$output .= '<ul>';
$output .= '<li>' . t('Package Manager 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>';
$output .= '<li>' . t('The temporary copy of the site is associated with the user or session that originally created it, and only that user or session can make changes to it.') . '</li>';
$output .= '<li>' . t('Modules cannot be uninstalled while Package Manager is syncing changes into live site.') . '</li>';
$output .= ' <li>' . t("The Drupal application's codebase must be writable in order to use Automatic Updates. This includes Drupal core, modules, themes and the Composer dependencies in the <code>vendor</code> directory. This makes Automatic Updates incompatible with some hosting platforms.") . '</li>';
$output .= ' <li>' . t('Package Manager requires Composer @version or later available as an executable, and PHP must have permission to run it. It should be detected automatically. If not, see <a href="#package-manager-faq-composer-not-found">What if it says the "composer" executable cannot be found?</a>.', ['@version' => ComposerExecutableValidator::MINIMUM_COMPOSER_VERSION]) . '</li>';
$output .= '</ul>';
$output .= '<h3 id="package-manager-limitations">' . t('Limitations') . '</h3>';
$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 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>';
$output .= ' <li>' . t('It associates the temporary copy of the site with the user or session that originally created it, and only that user or session can make changes to it.') . '</li>';
$output .= ' <li>' . t('It does not allow modules to be uninstalled while syncing changes into live site.') . '</li>';
$output .= '</ul>';
$output .= '<p>' . t('For more information, see the <a href=":package-manager-documentation">online documentation for the Package Manager module</a>.', [':package-manager-documentation' => 'https://www.drupal.org/docs/8/core/modules/package-manager']) . '</p>';
$output .= '<h3 id="package-manager-faq">' . t('FAQ') . '</h3>';
$output .= '<h4 id="package-manager-faq-composer-not-found">' . t('What if it says the "composer" executable cannot be found?') . '</h4>';
$output .= '<p>' . t('If the <code>composer</code> executable path cannot be automatically determined, it can be explicitly set in by adding the following line to <code>settings.php</code>:') . '</p>';
$output .= "<pre><code>\$config['package_manager.settings']['executables']['composer'] = '/full/path/to/composer.phar';</code></pre>";
// BEGIN: DELETE FROM CORE MERGE REQUEST
$output .= '<p>' . t('Alternatively, you can use the following Drush command:') . '</p>';
$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 .= '<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;
}
}
......@@ -101,6 +101,7 @@ services:
package_manager.validator.symlink:
class: Drupal\package_manager\Validator\SymlinkValidator
arguments:
- '@module_handler'
- '@package_manager.path_locator'
tags:
- { name: event_subscriber }
......
......@@ -110,7 +110,7 @@ final class ComposerExecutableValidator implements PreOperationStageValidatorInt
protected function setError(string $message, PreOperationStageEvent $event): void {
if ($this->moduleHandler->moduleExists('help')) {
$url = Url::fromRoute('help.page', ['name' => 'package_manager'])
->setOption('fragment', 'package-manager-requirements')
->setOption('fragment', 'package-manager-faq-composer-not-found')
->toString();
$message = $this->t('@message See <a href=":package-manager-help">the help page</a> for information on how to configure the path to Composer.', [
......
......@@ -2,7 +2,9 @@
namespace Drupal\package_manager\Validator;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\package_manager\Event\PreApplyEvent;
use Drupal\package_manager\Event\PreCreateEvent;
use Drupal\package_manager\Event\PreOperationStageEvent;
......@@ -25,6 +27,13 @@ class SymlinkValidator implements PreOperationStageValidatorInterface {
use StringTranslationTrait;
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The path locator service.
*
......@@ -35,10 +44,13 @@ class SymlinkValidator implements PreOperationStageValidatorInterface {
/**
* Constructs a SymlinkValidator object.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\package_manager\PathLocator $path_locator
* The path locator service.
*/
public function __construct(PathLocator $path_locator) {
public function __construct(ModuleHandlerInterface $module_handler, PathLocator $path_locator) {
$this->moduleHandler = $module_handler;
$this->pathLocator = $path_locator;
}
......@@ -49,9 +61,7 @@ class SymlinkValidator implements PreOperationStageValidatorInterface {
$dir = $this->pathLocator->getProjectRoot();
if ($this->hasLinks($dir)) {
$event->addError([
$this->t('Symbolic links were found in the active directory, which are not supported at this time.'),
]);
$this->addError('Symbolic links were found in the active directory, which are not supported at this time.', $event);
}
}
......@@ -65,9 +75,7 @@ class SymlinkValidator implements PreOperationStageValidatorInterface {
$dir = $event->getStage()->getStageDirectory();
if ($this->hasLinks($dir)) {
$event->addError([
$this->t('Symbolic links were found in the staging area, which are not supported at this time.'),
]);
$this->addError('Symbolic links were found in the staging area, which are not supported at this time.', $event);
}
}
......@@ -121,4 +129,30 @@ class SymlinkValidator implements PreOperationStageValidatorInterface {
];
}
/**
* 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]);
}
}
......@@ -179,7 +179,7 @@ class ComposerExecutableValidatorTest extends PackageManagerKernelTestBase {
*/
private function assertResultsWithHelp(array $expected_results, string $event_class = NULL): void {
$url = Url::fromRoute('help.page', ['name' => 'package_manager'])
->setOption('fragment', 'package-manager-requirements')
->setOption('fragment', 'package-manager-faq-composer-not-found')
->toString();
// Reformat the provided results so that they all have the link to the
......
......@@ -3,6 +3,7 @@
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;
......@@ -39,15 +40,27 @@ class SymlinkValidatorTest extends PackageManagerKernelTestBase {
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);
}
/**
* Tests that a symlink in the staging area raises an error.
*
* @dataProvider providerHelpEnabledOrNot
*/
public function testSymlinkInStagingArea(): void {
$result = ValidationResult::createError([
'Symbolic links were found in the staging area, which are not supported at this time.',
]);
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();
......@@ -61,14 +74,16 @@ class SymlinkValidatorTest extends PackageManagerKernelTestBase {
$this->fail('Expected a validation error.');
}
catch (StageValidationException $e) {
$this->assertValidationResultsEqual([$result], $e->getResults());
$this->assertValidationResultsEqual($expected_results, $e->getResults());
}
}
/**
* Tests that symlinks in the project root and staging area raise an error.
*
* @dataProvider providerHelpEnabledOrNot
*/
public function testSymlinkInProjectRootAndStagingArea(): void {
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.',
......@@ -78,6 +93,11 @@ class SymlinkValidatorTest extends PackageManagerKernelTestBase {
]),
];
if ($enable_help) {
$this->enableModules(['help']);
$expected_results = $this->addHelpTextToResults($expected_results);
}
$stage = $this->createStage();
$stage->create();
$stage->require(['composer/semver:^3']);
......@@ -97,6 +117,60 @@ class SymlinkValidatorTest extends PackageManagerKernelTestBase {
}
}
/**
* Data provider for test methods that test with and without the Help module.
*
* @return array[]
* The test cases.
*/
public function providerHelpEnabledOrNot() {
return [
'help_module_enabled' => [TRUE],
'help_module_disabled' => [FALSE],
];
}
/**
* Asserts that a set of validation results link to the Package Manager help.
*
* @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.
*/
private function assertResultsWithHelp(array $expected_results, string $event_class = NULL): void {
$expected_results = $this->addHelpTextToResults($expected_results);
$this->assertStatusCheckResults($expected_results);
$this->assertResults($expected_results, $event_class);
}
/**
* 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();
// 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 ($results as $index => $result) {
$messages = array_map($map, $result->getMessages());
$results[$index] = ValidationResult::createError($messages);
}
return $results;
}
}
/**
......
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