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

Issue #3231992 by phenaproxima: Make the staged sites/default writable before cleaning

parent 19f54310
No related branches found
No related tags found
1 merge request!30Issue #3231992: Make the staged sites/default writable before cleaning
...@@ -14,9 +14,11 @@ services: ...@@ -14,9 +14,11 @@ services:
arguments: arguments:
['@automatic_updates.composer_runner', '@automatic_updates.file_system' ] ['@automatic_updates.composer_runner', '@automatic_updates.file_system' ]
automatic_updates.cleaner: automatic_updates.cleaner:
class: PhpTuf\ComposerStager\Domain\Cleaner class: Drupal\automatic_updates\ComposerStager\Cleaner
arguments: arguments:
['@automatic_updates.file_system' ] - '@automatic_updates.file_system'
- '%site.path%'
- '@automatic_updates.path_locator'
automatic_updates.committer: automatic_updates.committer:
class: PhpTuf\ComposerStager\Domain\Committer class: PhpTuf\ComposerStager\Domain\Committer
arguments: arguments:
...@@ -90,3 +92,4 @@ services: ...@@ -90,3 +92,4 @@ services:
class: Drupal\automatic_updates\PathLocator class: Drupal\automatic_updates\PathLocator
arguments: arguments:
- '@config.factory' - '@config.factory'
- '%app.root%'
<?php
namespace Drupal\automatic_updates\ComposerStager;
use Drupal\automatic_updates\PathLocator;
use PhpTuf\ComposerStager\Domain\Cleaner as ComposerStagerCleaner;
use PhpTuf\ComposerStager\Domain\CleanerInterface;
use PhpTuf\ComposerStager\Domain\Output\ProcessOutputCallbackInterface;
use PhpTuf\ComposerStager\Infrastructure\Filesystem\FilesystemInterface;
use Symfony\Component\Filesystem\Filesystem;
/**
* Defines a cleaner service that makes the staged site directory writable.
*/
class Cleaner implements CleanerInterface {
/**
* The decorated cleaner service.
*
* @var \PhpTuf\ComposerStager\Domain\CleanerInterface
*/
protected $decorated;
/**
* The current site path, without leading or trailing slashes.
*
* @var string
*/
protected $sitePath;
/**
* The path locator service.
*
* @var \Drupal\automatic_updates\PathLocator
*/
protected $pathLocator;
/**
* Constructs a Cleaner object.
*
* @param \PhpTuf\ComposerStager\Infrastructure\Filesystem\FilesystemInterface $file_system
* The Composer Stager file system service.
* @param string $site_path
* The current site path (e.g., 'sites/default'), without leading or
* trailing slashes.
* @param \Drupal\automatic_updates\PathLocator $locator
* The path locator service.
*/
public function __construct(FilesystemInterface $file_system, string $site_path, PathLocator $locator) {
// @todo Inject the decorated cleaner once the Composer Stager wiring has
// been moved into a separate module.
$this->decorated = new ComposerStagerCleaner($file_system);
$this->sitePath = $site_path;
$this->pathLocator = $locator;
}
/**
* {@inheritdoc}
*/
public function clean(string $stagingDir, ?ProcessOutputCallbackInterface $callback = NULL, ?int $timeout = 120): void {
// Ensure that the staged site directory is writable so we can delete it.
$site_dir = implode(DIRECTORY_SEPARATOR, [
$stagingDir,
$this->pathLocator->getWebRoot() ?: '.',
$this->sitePath,
]);
if ($this->directoryExists($site_dir)) {
(new Filesystem())->chmod($site_dir, 0777);
}
$this->decorated->clean($stagingDir, $callback, $timeout);
}
/**
* {@inheritdoc}
*/
public function directoryExists(string $stagingDir): bool {
return $this->decorated->directoryExists($stagingDir);
}
}
...@@ -18,14 +18,24 @@ class PathLocator { ...@@ -18,14 +18,24 @@ class PathLocator {
*/ */
protected $configFactory; protected $configFactory;
/**
* The absolute path of the running Drupal code base.
*
* @var string
*/
protected $appRoot;
/** /**
* Constructs a PathLocator object. * Constructs a PathLocator object.
* *
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service. * The config factory service.
* @param string $app_root
* The absolute path of the running Drupal code base.
*/ */
public function __construct(ConfigFactoryInterface $config_factory) { public function __construct(ConfigFactoryInterface $config_factory, string $app_root) {
$this->configFactory = $config_factory; $this->configFactory = $config_factory;
$this->appRoot = $app_root;
} }
/** /**
...@@ -41,8 +51,13 @@ class PathLocator { ...@@ -41,8 +51,13 @@ class PathLocator {
/** /**
* Returns the path of the directory where updates should be staged. * Returns the path of the directory where updates should be staged.
* *
* This directory may be made world-writeable for clean-up, so it should be
* somewhere that doesn't put the Drupal installation at risk.
*
* @return string * @return string
* The absolute path of the directory where updates should be staged. * The absolute path of the directory where updates should be staged.
*
* @see \Drupal\automatic_updates\ComposerStager\Cleaner::clean()
*/ */
public function getStageDirectory(): string { public function getStageDirectory(): string {
// Append the site ID to the directory in order to support parallel test // Append the site ID to the directory in order to support parallel test
...@@ -76,4 +91,17 @@ class PathLocator { ...@@ -76,4 +91,17 @@ class PathLocator {
return dirname($reflector->getFileName(), 2); return dirname($reflector->getFileName(), 2);
} }
/**
* Returns the path of the Drupal installation, relative to the project root.
*
* @return string
* The path of the Drupal installation, relative to the project root and
* without leading or trailing slashes. Will return an empty string if the
* project root and Drupal root are the same.
*/
public function getWebRoot(): string {
$web_root = str_replace($this->getProjectRoot(), NULL, $this->appRoot);
return trim($web_root, DIRECTORY_SEPARATOR);
}
} }
...@@ -2,17 +2,18 @@ ...@@ -2,17 +2,18 @@
namespace Drupal\Tests\automatic_updates\Functional; namespace Drupal\Tests\automatic_updates\Functional;
use Drupal\automatic_updates\ComposerStager\Cleaner;
use Drupal\automatic_updates\PathLocator; use Drupal\automatic_updates\PathLocator;
use Drupal\automatic_updates\Updater; use Drupal\automatic_updates\Updater;
use Drupal\Core\Site\Settings; use Drupal\Core\Site\Settings;
use Drupal\Tests\BrowserTestBase; use Drupal\Tests\BrowserTestBase;
/** /**
* Tests exclusion of certain files and directories from the staging area. * Tests handling of files and directories during an update.
* *
* @group automatic_updates * @group automatic_updates
*/ */
class ExclusionsTest extends BrowserTestBase { class FileSystemOperationsTest extends BrowserTestBase {
/** /**
* {@inheritdoc} * {@inheritdoc}
...@@ -25,37 +26,69 @@ class ExclusionsTest extends BrowserTestBase { ...@@ -25,37 +26,69 @@ class ExclusionsTest extends BrowserTestBase {
protected $defaultTheme = 'stark'; protected $defaultTheme = 'stark';
/** /**
* Tests that certain files and directories are not staged. * The updater service under test.
* *
* @covers \Drupal\automatic_updates\Updater::getExclusions * @var \Drupal\automatic_updates\Updater
*/ */
public function testExclusions(): void { private $updater;
$stage_dir = "$this->siteDirectory/stage";
/**
* The full path of the staging directory.
*
* @var string
*/
protected $stageDir;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a mocked path locator that uses the fake site fixture as its
// active directory, and has a staging area within the site directory for
// this test.
$drupal_root = $this->getDrupalRoot();
/** @var \Drupal\automatic_updates\PathLocator|\Prophecy\Prophecy\ObjectProphecy $locator */ /** @var \Drupal\automatic_updates\PathLocator|\Prophecy\Prophecy\ObjectProphecy $locator */
$locator = $this->prophesize(PathLocator::class); $locator = $this->prophesize(PathLocator::class);
$locator->getActiveDirectory()->willReturn(__DIR__ . '/../../fixtures/fake-site'); $locator->getActiveDirectory()->willReturn(__DIR__ . '/../../fixtures/fake-site');
$locator->getStageDirectory()->willReturn($stage_dir); $this->stageDir = implode(DIRECTORY_SEPARATOR, [
$locator->getProjectRoot()->willReturn($this->getDrupalRoot()); $drupal_root,
$this->siteDirectory,
'stage',
]);
$locator->getStageDirectory()->willReturn($this->stageDir);
$locator->getProjectRoot()->willReturn($drupal_root);
$locator->getWebRoot()->willReturn('');
// Create a cleaner that uses 'sites/default' as its site path, since it
// will otherwise default to the site path being used for the test site,
// which doesn't exist in the fake site fixture.
$cleaner = new Cleaner(
$this->container->get('automatic_updates.file_system'),
'sites/default',
$locator->reveal()
);
$updater = new Updater( $this->updater = new Updater(
$this->container->get('state'), $this->container->get('state'),
$this->container->get('string_translation'), $this->container->get('string_translation'),
$this->container->get('automatic_updates.beginner'), $this->container->get('automatic_updates.beginner'),
$this->container->get('automatic_updates.stager'), $this->container->get('automatic_updates.stager'),
$this->container->get('automatic_updates.cleaner'), $cleaner,
$this->container->get('automatic_updates.committer'), $this->container->get('automatic_updates.committer'),
$this->container->get('event_dispatcher'), $this->container->get('event_dispatcher'),
$locator->reveal() $locator->reveal()
); );
// Use the public and private files directories in the fake site fixture.
$settings = Settings::getAll(); $settings = Settings::getAll();
$settings['file_public_path'] = 'files/public'; $settings['file_public_path'] = 'files/public';
$settings['file_private_path'] = 'files/private'; $settings['file_private_path'] = 'files/private';
new Settings($settings); new Settings($settings);
// Updater::begin() will trigger update validators, such as // Updater::begin() will trigger update validators, such as
// \Drupal\automatic_updates\Event\UpdateVersionSubscriber, that need to // \Drupal\automatic_updates\Validator\UpdateVersionValidator, that need to
// fetch release metadata. We need to ensure that those HTTP request(s) // fetch release metadata. We need to ensure that those HTTP request(s)
// succeed, so set them up to point to our fake release metadata. // succeed, so set them up to point to our fake release metadata.
$this->config('update_test.settings') $this->config('update_test.settings')
...@@ -69,20 +102,43 @@ class ExclusionsTest extends BrowserTestBase { ...@@ -69,20 +102,43 @@ class ExclusionsTest extends BrowserTestBase {
$this->config('update_test.settings') $this->config('update_test.settings')
->set('system_info.#all.version', '9.8.0') ->set('system_info.#all.version', '9.8.0')
->save(); ->save();
}
$updater->begin(['drupal' => '9.8.1']); /**
$this->assertFileDoesNotExist("$stage_dir/sites/default/settings.php"); * Tests that certain files and directories are not staged.
$this->assertFileDoesNotExist("$stage_dir/sites/default/settings.local.php"); *
$this->assertFileDoesNotExist("$stage_dir/sites/default/services.yml"); * @covers \Drupal\automatic_updates\Updater::getExclusions
*/
public function testExclusions(): void {
$this->updater->begin(['drupal' => '9.8.1']);
$this->assertFileDoesNotExist("$this->stageDir/sites/default/settings.php");
$this->assertFileDoesNotExist("$this->stageDir/sites/default/settings.local.php");
$this->assertFileDoesNotExist("$this->stageDir/sites/default/services.yml");
// A file in sites/default, that isn't one of the site-specific settings // A file in sites/default, that isn't one of the site-specific settings
// files, should be staged. // files, should be staged.
$this->assertFileExists("$stage_dir/sites/default/staged.txt"); $this->assertFileExists("$this->stageDir/sites/default/staged.txt");
$this->assertDirectoryDoesNotExist("$stage_dir/sites/simpletest"); $this->assertDirectoryDoesNotExist("$this->stageDir/sites/simpletest");
$this->assertDirectoryDoesNotExist("$stage_dir/files/public"); $this->assertDirectoryDoesNotExist("$this->stageDir/files/public");
$this->assertDirectoryDoesNotExist("$stage_dir/files/private"); $this->assertDirectoryDoesNotExist("$this->stageDir/files/private");
// A file that's in the general files directory, but not in the public or // A file that's in the general files directory, but not in the public or
// private directories, should be staged. // private directories, should be staged.
$this->assertFileExists("$stage_dir/files/staged.txt"); $this->assertFileExists("$this->stageDir/files/staged.txt");
}
/**
* Tests that the staging directory is properly cleaned up.
*
* @covers \Drupal\automatic_updates\Cleaner
*/
public function testClean(): void {
$this->updater->begin(['drupal' => '9.8.1']);
// Make the staged site directory read-only, so we can test that it will be
// made writable on clean-up.
$this->assertTrue(chmod("$this->stageDir/sites/default", 0400));
$this->assertNotIsWritable("$this->stageDir/sites/default/staged.txt");
// If the site directory is not writable, this will throw an exception.
$this->updater->clean();
$this->assertDirectoryDoesNotExist($this->stageDir);
} }
} }
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