diff --git a/composer.json b/composer.json index a8602e3335..d963e849e2 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ "drupal/core": "self.version", "drupal/core-project-message": "self.version", "drupal/core-vendor-hardening": "self.version", + "drupal/core-composer-scaffold": "self.version", "wikimedia/composer-merge-plugin": "^1.4" }, "require-dev": { @@ -113,6 +114,10 @@ "type": "path", "url": "composer/Plugin/ProjectMessage" }, + { + "type": "path", + "url": "composer/Plugin/Scaffold" + }, { "type": "path", "url": "composer/Plugin/VendorHardening" diff --git a/composer.lock b/composer.lock index 894991f429..f23aaa272d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c6c500e6567b37ef3e100b285ab9910c", + "content-hash": "a0eb4f888c0c8737accaacb2dc5a6388", "packages": [ { "name": "asm89/stack-cors", @@ -879,6 +879,45 @@ ], "description": "Drupal is an open source content management platform powering millions of websites and applications." }, + { + "name": "drupal/core-composer-scaffold", + "version": "8.9.x-dev", + "dist": { + "type": "path", + "url": "composer/Plugin/Scaffold", + "reference": "44d7293a1551068de987139dcb2586d3ac3f9a46" + }, + "require": { + "composer-plugin-api": "^1.0.0", + "php": ">=7.0.8" + }, + "conflict": { + "drupal-composer/drupal-scaffold": "*" + }, + "require-dev": { + "composer/composer": "^1.8@stable" + }, + "type": "composer-plugin", + "extra": { + "class": "Drupal\\Composer\\Plugin\\Scaffold\\Plugin", + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Drupal\\Composer\\Plugin\\Scaffold\\": "" + } + }, + "license": [ + "GPL-2.0-or-later" + ], + "description": "A flexible Composer project scaffold builder.", + "homepage": "https://www.drupal.org/project/drupal", + "keywords": [ + "drupal" + ] + }, { "name": "drupal/core-project-message", "version": "8.9.x-dev", @@ -6378,6 +6417,7 @@ "drupal/core": 20, "drupal/core-project-message": 20, "drupal/core-vendor-hardening": 20, + "drupal/core-composer-scaffold": 20, "behat/mink": 20, "behat/mink-selenium2-driver": 20 }, diff --git a/composer/Plugin/Scaffold/ComposerScaffoldCommand.php b/composer/Plugin/Scaffold/ComposerScaffoldCommand.php index fd8becb3d0..0c946281e6 100644 --- a/composer/Plugin/Scaffold/ComposerScaffoldCommand.php +++ b/composer/Plugin/Scaffold/ComposerScaffoldCommand.php @@ -5,6 +5,7 @@ use Composer\Command\BaseCommand; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Input\InputOption; /** * The "drupal:scaffold" command class. @@ -22,6 +23,7 @@ protected function configure() { ->setName('drupal:scaffold') ->setAliases(['scaffold']) ->setDescription('Update the Drupal scaffold files.') + ->addOption('force-changes', NULL, InputOption::VALUE_NONE, 'Try to force changes despite \'hardened\' destination files.') ->setHelp( <<drupal:scaffold command places the scaffold files in their @@ -46,7 +48,7 @@ protected function configure() { * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { - $handler = new Handler($this->getComposer(), $this->getIO()); + $handler = new Handler($this->getComposer(), $this->getIO(), $input->getOption('force-changes')); $handler->scaffold(); return 0; } diff --git a/composer/Plugin/Scaffold/Handler.php b/composer/Plugin/Scaffold/Handler.php index 92a2e8f69a..160bbb68d3 100644 --- a/composer/Plugin/Scaffold/Handler.php +++ b/composer/Plugin/Scaffold/Handler.php @@ -66,6 +66,13 @@ class Handler { */ protected $postPackageListeners = []; + /** + * Whether the user wants to attempt to force changes for hardened files. + * + * @var bool + */ + protected $forceChanges; + /** * Handler constructor. * @@ -74,9 +81,10 @@ class Handler { * @param \Composer\IO\IOInterface $io * The Composer I/O service. */ - public function __construct(Composer $composer, IOInterface $io) { + public function __construct(Composer $composer, IOInterface $io, $force_changes = FALSE) { $this->composer = $composer; $this->io = $io; + $this->forceChanges = $force_changes; $this->manageOptions = new ManageOptions($composer); $this->manageAllowedPackages = new AllowedPackages($composer, $io, $this->manageOptions); } @@ -124,7 +132,7 @@ public function onPostPackageEvent(PackageEvent $event) { * A list of scaffolding operation objects */ protected function createScaffoldOperations(PackageInterface $package, array $package_file_mappings) { - $scaffold_op_factory = new OperationFactory($this->composer); + $scaffold_op_factory = new OperationFactory($this->composer, $this->forceChanges); $scaffold_ops = []; foreach ($package_file_mappings as $dest_rel_path => $data) { $operation_data = new OperationData($dest_rel_path, $data); @@ -168,11 +176,11 @@ public function scaffold() { $error_destinations = []; foreach ($scaffold_results as $result) { if ($result->failed()) { - $error_destinations[] = $result->destination(); + $error_destinations[] = $result->destination()->fullPath(); } } if ($error_destinations) { - array_unshift('The following destinations were not properly scaffolded:', $error_destinations); + array_unshift($error_destinations, 'The following destinations were not properly scaffolded:'); throw new \RuntimeException(implode("\n", $error_destinations)); } diff --git a/composer/Plugin/Scaffold/Operations/AppendOp.php b/composer/Plugin/Scaffold/Operations/AppendOp.php index cf92259c07..62dc6db49f 100644 --- a/composer/Plugin/Scaffold/Operations/AppendOp.php +++ b/composer/Plugin/Scaffold/Operations/AppendOp.php @@ -5,6 +5,7 @@ use Composer\IO\IOInterface; use Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath; use Drupal\Composer\Plugin\Scaffold\ScaffoldOptions; +use Drupal\Composer\Plugin\Scaffold\Unhardener; /** * Scaffold operation to add to the beginning and/or end of a scaffold file. @@ -47,6 +48,13 @@ class AppendOp extends AbstractOperation { */ protected $forceAppend; + /** + * Whether we should attempt to force changes to hardened files. + * + * @var bool + */ + protected $forceChanges; + /** * Constructs an AppendOp. * @@ -59,12 +67,13 @@ class AppendOp extends AbstractOperation { * @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath $default_path * The relative path to the default data. */ - public function __construct(ScaffoldFilePath $prepend_path = NULL, ScaffoldFilePath $append_path = NULL, $force_append = FALSE, ScaffoldFilePath $default_path = NULL) { + public function __construct(ScaffoldFilePath $prepend_path = NULL, ScaffoldFilePath $append_path = NULL, $force_append = FALSE, ScaffoldFilePath $default_path = NULL, $force_changes = FALSE) { $this->forceAppend = $force_append; $this->prepend = $prepend_path; $this->append = $append_path; $this->default = $default_path; $this->managed = TRUE; + $this->forceChanges = $force_changes; } /** @@ -112,6 +121,11 @@ public function process(ScaffoldFilePath $destination, IOInterface $io, Scaffold // that is all 'trim'ed away. Then we get a message that we are appending, // although nothing will in fact actually happen. $failed = FALSE; + $unhardener = NULL; + if ($this->forceChanges) { + $unhardener = new Unhardener($destination_path); + $unhardener->unharden(); + } if (!empty(trim($prepend_contents)) || !empty(trim($append_contents))) { // None of our asset files are very large, so we will load each one into // memory for processing. @@ -121,11 +135,14 @@ public function process(ScaffoldFilePath $destination, IOInterface $io, Scaffold // Silence errors because we only care whether it worked. if (@file_put_contents($destination_path, $altered_contents) === FALSE) { $failed = TRUE; - $messages[] = " - Could not prepend/append to [dest-rel-path]"; + $messages = [" - Could not prepend/append to [dest-rel-path]"]; } $io->write($interpolator->interpolate(implode("\n", $messages))); } + if ($unhardener) { + $unhardener->reharden(); + } // Return a ScaffoldResult with knowledge of whether this file is managed. return new ScaffoldResult($destination, $this->managed, $failed); } diff --git a/composer/Plugin/Scaffold/Operations/OperationFactory.php b/composer/Plugin/Scaffold/Operations/OperationFactory.php index 7742e4bb9d..6c08556df0 100644 --- a/composer/Plugin/Scaffold/Operations/OperationFactory.php +++ b/composer/Plugin/Scaffold/Operations/OperationFactory.php @@ -18,6 +18,13 @@ class OperationFactory { */ protected $composer; + /** + * Whether the user wants to attempt to force changes for hardened files. + * + * @var bool + */ + protected $forceChanges; + /** * OperationFactory constructor. * @@ -26,8 +33,9 @@ class OperationFactory { * is also responsible for evaluating relative package paths as it creates * scaffold operations. */ - public function __construct(Composer $composer) { + public function __construct(Composer $composer, $force_changes) { $this->composer = $composer; + $this->forceChanges = $force_changes; } /** @@ -79,7 +87,7 @@ protected function createReplaceOp(PackageInterface $package, OperationData $ope $package_name = $package->getName(); $package_path = $this->getPackagePath($package); $source = ScaffoldFilePath::sourcePath($package_name, $package_path, $operation_data->destination(), $operation_data->path()); - $op = new ReplaceOp($source, $operation_data->overwrite()); + $op = new ReplaceOp($source, $operation_data->overwrite(), $this->forceChanges); return $op; } @@ -114,7 +122,7 @@ protected function createAppendOp(PackageInterface $package, OperationData $oper return new SkipOp($message); } - return new AppendOp($prepend_source_file, $append_source_file, $operation_data->forceAppend(), $default_data_file); + return new AppendOp($prepend_source_file, $append_source_file, $operation_data->forceAppend(), $default_data_file, $this->forceChanges); } /** diff --git a/composer/Plugin/Scaffold/Operations/ReplaceOp.php b/composer/Plugin/Scaffold/Operations/ReplaceOp.php index b047da64b2..eaa8cb67d9 100644 --- a/composer/Plugin/Scaffold/Operations/ReplaceOp.php +++ b/composer/Plugin/Scaffold/Operations/ReplaceOp.php @@ -6,6 +6,7 @@ use Composer\Util\Filesystem; use Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath; use Drupal\Composer\Plugin\Scaffold\ScaffoldOptions; +use Drupal\Composer\Plugin\Scaffold\Unhardener; /** * Scaffold operation to copy or symlink from source to destination. @@ -31,6 +32,13 @@ class ReplaceOp extends AbstractOperation { */ protected $overwrite; + /** + * Whether we should attempt to force changes to hardened files. + * + * @var bool + */ + protected $forceChanges; + /** * Constructs a ReplaceOp. * @@ -40,9 +48,10 @@ class ReplaceOp extends AbstractOperation { * Whether to allow this scaffold file to overwrite files already at * the destination. Defaults to TRUE. */ - public function __construct(ScaffoldFilePath $sourcePath, $overwrite = TRUE) { + public function __construct(ScaffoldFilePath $sourcePath, $overwrite = TRUE, $force_changes = FALSE) { $this->source = $sourcePath; $this->overwrite = $overwrite; + $this->forceChanges = $force_changes; } /** @@ -51,22 +60,46 @@ public function __construct(ScaffoldFilePath $sourcePath, $overwrite = TRUE) { public function process(ScaffoldFilePath $destination, IOInterface $io, ScaffoldOptions $options) { $fs = new Filesystem(); $destination_path = $destination->fullPath(); + $interpolator = $destination->getInterpolator(); // Do nothing if overwrite is 'false' and a file already exists at the // destination. if ($this->overwrite === FALSE && file_exists($destination_path)) { - $interpolator = $destination->getInterpolator(); $io->write($interpolator->interpolate(" - Skip [dest-rel-path] because it already exists and overwrite is false.")); return new ScaffoldResult($destination, FALSE); } - // Get rid of the destination if it exists, and make sure that - // the directory where it's going to be placed exists. - $fs->remove($destination_path); - $fs->ensureDirectoryExists(dirname($destination_path)); - if ($options->symlink()) { - return $this->symlinkScaffold($destination, $io); + // Deal with 'hardened' permissions. + $unhardener = NULL; + if ($this->forceChanges) { + $unhardener = new Unhardener($destination_path); + $unhardener->unharden(); + } + + // Get rid of the destination if it exists, and make sure that the directory + // where it's going to be placed exists. + $result = NULL; + try { + $fs->remove($destination_path); + $fs->ensureDirectoryExists(dirname($destination_path)); + } + // Catch runtime exceptions and let the user know. + catch (\RuntimeException $exception) { + $io->write($interpolator->interpolate(" - Unable to replace [dest-rel-path] because of a permissions error.")); + $result = new ScaffoldResult($destination, $this->overwrite, TRUE); + } + if (!$result) { + if ($options->symlink()) { + $result = $this->symlinkScaffold($destination, $io); + } + else { + $result = $this->copyScaffold($destination, $io); + } + } + + if ($unhardener) { + $unhardener->reharden(); } - return $this->copyScaffold($destination, $io); + return $result; } /** @@ -109,15 +142,17 @@ protected function copyScaffold(ScaffoldFilePath $destination, IOInterface $io) */ protected function symlinkScaffold(ScaffoldFilePath $destination, IOInterface $io) { $interpolator = $destination->getInterpolator(); - try { - $fs = new Filesystem(); - $fs->relativeSymlink($this->source->fullPath(), $destination->fullPath()); + $failed = FALSE; + $fs = new Filesystem(); + // Silence errors because we only care whether it worked. + if (@$fs->relativeSymlink($this->source->fullPath(), $destination->fullPath())) { $io->write($interpolator->interpolate(" - Link [dest-rel-path] from [src-rel-path]")); } - catch (\Exception $e) { + else { $io->writeError($interpolator->interpolate(" - Couldn't link [dest-rel-path] from [src-rel-path]")); + $failed = TRUE; } - return new ScaffoldResult($destination, $this->overwrite); + return new ScaffoldResult($destination, $this->overwrite, $failed); } } diff --git a/composer/Plugin/Scaffold/Unhardener.php b/composer/Plugin/Scaffold/Unhardener.php new file mode 100644 index 0000000000..6178e373a5 --- /dev/null +++ b/composer/Plugin/Scaffold/Unhardener.php @@ -0,0 +1,59 @@ +destinationPath = $destination_path; + } + + /** + * Change the permissions of the hardened file and its parent. + */ + public function unharden() { + $hardened = [dirname($this->destinationPath), $this->destinationPath]; + $this->unhardened = []; + foreach ($hardened as $unharden) { + if (!is_writable($unharden)) { + $this->unhardened[$unharden] = (stat($unharden))['mode'] & 000777; + chmod($unharden, 0744); + } + } + } + + /** + * Return to the previous file system modes of the hardened file and its parent. + */ + public function reharden() { + if ($this->unhardened) { + foreach ($this->unhardened as $path => $mode) { + chmod($path, $mode); + } + } + } + +} diff --git a/core/tests/Drupal/BuildTests/Composer/Plugin/Scaffold/AppendOpForceChangeTest.php b/core/tests/Drupal/BuildTests/Composer/Plugin/Scaffold/AppendOpForceChangeTest.php new file mode 100644 index 0000000000..75120e0888 --- /dev/null +++ b/core/tests/Drupal/BuildTests/Composer/Plugin/Scaffold/AppendOpForceChangeTest.php @@ -0,0 +1,153 @@ +ensureDirectoryExists($root . '/scaffold'); + $fs->ensureDirectoryExists($root . '/sites/default'); + + file_put_contents($root . '/scaffold/default.services.yml', 'appended: true'); + file_put_contents($root . '/sites/default/default.services.yml', 'replaced: false'); + } + + protected function createMockSourcePath($root) { + $source_path = $this->getMockBuilder(ScaffoldFilePath::class) + ->disableOriginalConstructor() + ->setMethods(['addInterpolationData', 'fullPath']) + ->getMock(); + $source_path->expects($this->any()) + ->method('fullPath') + ->willReturn($root . '/scaffold/default.services.yml'); + return $source_path; + } + + protected function createMockDestinationPath($root) { + $dest_interpolator = $this->getMockBuilder(Interpolator::class) + ->disableOriginalConstructor() + ->setMethods(['interpolate']) + ->getMock(); + $dest_interpolator->expects($this->any()) + ->method('interpolate') + // Return whatever was input. + ->willReturnCallback(function ($arg) { + return $arg; + }); + + $dest_path = $this->getMockBuilder(ScaffoldFilePath::class) + ->disableOriginalConstructor() + ->setMethods(['getInterpolator', 'fullPath']) + ->getMock(); + $dest_path->expects($this->any()) + ->method('getInterpolator') + ->willReturn($dest_interpolator); + $dest_path->expects($this->any()) + ->method('fullPath') + ->willReturn($root . '/sites/default/default.services.yml'); + return $dest_path; + } + + public function provideAppend() { + return [ + [" - Append to [dest-rel-path] from [append-rel-path]\n", "replaced: false\nappended: true", FALSE, 0444], + [" - Append to [dest-rel-path] from [append-rel-path]\n", "replaced: false\nappended: true", FALSE, 0644], + ]; + } + + /** + * @covers ::process + * @dataProvider provideAppend + */ + public function testAppendForceChange($expected_message, $expected_content, $expected_fail, $test_chmod) { + $ws = $this->getWorkspaceDirectory(); + $this->createMockFilesystem($ws); + + $io = new BufferIO(); + + // Set up our tricky permissions. + chmod($ws . '/sites/default', 0555); + chmod($ws . '/sites/default/default.services.yml', $test_chmod); + + // Make a ReplaceOp object, set to force changes. + $append = new AppendOp(NULL, $this->createMockSourcePath($ws), FALSE, NULL, TRUE); + + $result = $append->process( + $this->createMockDestinationPath($ws), + $io, + ScaffoldOptions::create(['drupal-scaffold' => ['symlink' => FALSE]]) + ); + + // Make sure we set the mode back to what it was. + $this->assertEquals($test_chmod, (stat($ws . '/sites/default/default.services.yml'))['mode'] & 000777); + + // Make sure we got the fail state we expected. + $this->assertInstanceOf(ScaffoldResult::class, $result); + $this->assertEquals($expected_fail, $result->failed()); + + $this->assertEquals($expected_message, $io->getOutput()); + $this->assertEquals($expected_content, file_get_contents($ws . '/sites/default/default.services.yml')); + } + + public function provideAppendNoForceChange() { + return [ + [" - Could not prepend/append to [dest-rel-path]\n", 'replaced: false', TRUE, 0444], + [" - Append to [dest-rel-path] from [append-rel-path]\n", "replaced: false\nappended: true", FALSE, 0644], + ]; + } + + /** + * @covers ::process + * @dataProvider provideAppendNoForceChange + */ + public function testAppendNoForceChange($expected_message, $expected_content, $expected_fail, $test_chmod) { + $ws = $this->getWorkspaceDirectory(); + $this->createMockFilesystem($ws); + + $io = new BufferIO(); + + // Set up our tricky permissions. + chmod($ws . '/sites/default', 0555); + chmod($ws . '/sites/default/default.services.yml', $test_chmod); + + // Make a ReplaceOp object, set to NOT force changes. + $append = new AppendOp(NULL, $this->createMockSourcePath($ws), FALSE, NULL, FALSE); + + $result = $append->process( + $this->createMockDestinationPath($ws), + $io, + ScaffoldOptions::create(['drupal-scaffold' => ['symlink' => FALSE]]) + ); + + // Make sure we set the mode back to what it was. + $this->assertEquals($test_chmod, (stat($ws . '/sites/default/default.services.yml'))['mode'] & 000777); + + // Make sure we got the fail state we expected. + $this->assertInstanceOf(ScaffoldResult::class, $result); + $this->assertEquals($expected_fail, $result->failed()); + + $this->assertEquals($expected_message, $io->getOutput()); + $this->assertEquals($expected_content, file_get_contents($ws . '/sites/default/default.services.yml')); + } + +} diff --git a/core/tests/Drupal/BuildTests/Composer/Plugin/Scaffold/ReplaceOpForceChangeTest.php b/core/tests/Drupal/BuildTests/Composer/Plugin/Scaffold/ReplaceOpForceChangeTest.php new file mode 100644 index 0000000000..18b24705ac --- /dev/null +++ b/core/tests/Drupal/BuildTests/Composer/Plugin/Scaffold/ReplaceOpForceChangeTest.php @@ -0,0 +1,151 @@ +ensureDirectoryExists($root . '/scaffold'); + $fs->ensureDirectoryExists($root . '/sites/default'); + + file_put_contents($root . '/scaffold/default.services.yml', 'replaced: true'); + file_put_contents($root . '/sites/default/default.services.yml', 'replaced: false'); + } + + protected function createMockSourcePath($root) { + $source_path = $this->getMockBuilder(ScaffoldFilePath::class) + ->disableOriginalConstructor() + ->setMethods(['addInterpolationData', 'fullPath']) + ->getMock(); + $source_path->expects($this->any()) + ->method('fullPath') + ->willReturn($root . '/scaffold/default.services.yml'); + return $source_path; + } + + protected function createMockDestinationPath($root) { + $dest_interpolator = $this->getMockBuilder(Interpolator::class) + ->disableOriginalConstructor() + ->setMethods(['interpolate']) + ->getMock(); + $dest_interpolator->expects($this->once()) + ->method('interpolate') + // Return whatever was input. + ->willReturnCallback(function ($arg) { + return $arg; + }); + + $dest_path = $this->getMockBuilder(ScaffoldFilePath::class) + ->disableOriginalConstructor() + ->setMethods(['getInterpolator', 'fullPath']) + ->getMock(); + $dest_path->expects($this->any()) + ->method('getInterpolator') + ->willReturn($dest_interpolator); + $dest_path->expects($this->any()) + ->method('fullPath') + ->willReturn($root . '/sites/default/default.services.yml'); + return $dest_path; + } + + public function provideCopyScaffold() { + return [ + [" - Copy [dest-rel-path] from [src-rel-path]\n", 'replaced: true', FALSE, 0444], + [" - Copy [dest-rel-path] from [src-rel-path]\n", 'replaced: true', FALSE, 0644], + ]; + } + + /** + * @covers ::copyScaffold + * @dataProvider provideCopyScaffold + */ + public function testCopyScaffoldForceChange($expected_message, $expected_content, $expected_fail, $test_chmod) { + $ws = $this->getWorkspaceDirectory(); + $this->createMockFilesystem($ws); + + $io = new BufferIO(); + + // Set up our tricky permissions. + chmod($ws . '/sites/default', 0555); + chmod($ws . '/sites/default/default.services.yml', $test_chmod); + + // Make a ReplaceOp object, set to force changes. + $replace = new ReplaceOp($this->createMockSourcePath($ws), TRUE, TRUE); + $result = $replace->process( + $this->createMockDestinationPath($ws), + $io, + ScaffoldOptions::create(['drupal-scaffold' => ['symlink' => FALSE]]) + ); + + // Make sure we set the mode back to what it was. + $this->assertEquals($test_chmod, (stat($ws . '/sites/default/default.services.yml'))['mode'] & 000777); + + // Make sure we got the fail state we expected. + $this->assertInstanceOf(ScaffoldResult::class, $result); + $this->assertEquals($expected_fail, $result->failed()); + + $this->assertEquals($expected_message, $io->getOutput()); + $this->assertEquals($expected_content, file_get_contents($ws . '/sites/default/default.services.yml')); + } + + public function provideCopyScaffoldNoForce() { + return [ + [" - Unable to replace [dest-rel-path] because of a permissions error.\n", 'replaced: false', TRUE, 0644], + [" - Unable to replace [dest-rel-path] because of a permissions error.\n", 'replaced: false', TRUE, 0444], + ]; + } + + /** + * @covers ::copyScaffold + * @dataProvider provideCopyScaffoldNoForce + */ + public function testCopyScaffoldNoForceChange($expected_message, $expected_content, $expected_fail, $test_chmod) { + $this->destroyBuild = FALSE; + $ws = $this->getWorkspaceDirectory(); + $this->createMockFilesystem($ws); + + $io = new BufferIO(); + + // Set up our tricky permissions. Note that we will always fail to replace the + // destination file because sites/default/ is set to 0555. Drupal core does this + // when we perform a Drupal installation. + chmod($ws . '/sites/default', 0555); + chmod($ws . '/sites/default/default.services.yml', $test_chmod); + + // Make a ReplaceOp object, set to NOT force changes. + $replace = new ReplaceOp($this->createMockSourcePath($ws), TRUE, FALSE); + $result = $replace->process( + $this->createMockDestinationPath($ws), + $io, + ScaffoldOptions::create(['drupal-scaffold' => ['symlink' => FALSE]]) + ); + + // Make sure we got the fail state we expected. + $this->assertInstanceOf(ScaffoldResult::class, $result); + $this->assertEquals($expected_fail, $result->failed()); + + $this->assertEquals($expected_message, $io->getOutput()); + $this->assertEquals($expected_content, file_get_contents($ws . '/sites/default/default.services.yml')); + } + +} diff --git a/core/tests/Drupal/BuildTests/Composer/Plugin/Scaffold/UnhardenerTest.php b/core/tests/Drupal/BuildTests/Composer/Plugin/Scaffold/UnhardenerTest.php new file mode 100644 index 0000000000..c641199a72 --- /dev/null +++ b/core/tests/Drupal/BuildTests/Composer/Plugin/Scaffold/UnhardenerTest.php @@ -0,0 +1,53 @@ +getWorkspaceDirectory() . '/parent'; + $child_path = $parent_path . '/child.txt'; + + (new Filesystem())->ensureDirectoryExists($parent_path); + file_put_contents($child_path, 'content'); + + chmod($child_path, $child_mode); + chmod($parent_path, $parent_mode); + + $unhardener = new Unhardener($child_path); + $unhardener->unharden(); + + file_put_contents($child_path, 'modified'); + + $unhardener->reharden(); + + $this->assertEquals($parent_mode, (stat($parent_path))['mode'] & 000777); + $this->assertEquals($child_mode, (stat($child_path))['mode'] & 000777); + } + +} diff --git a/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Integration/AppendOpTest.php b/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Integration/AppendOpTest.php index f98326625f..aaf7454c96 100644 --- a/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Integration/AppendOpTest.php +++ b/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Integration/AppendOpTest.php @@ -63,7 +63,7 @@ public function testProcess() { public function provideProcessPermissions() { return [ [ - " - Append to [dest-rel-path] from [append-rel-path]\n - Could not prepend/append to [dest-rel-path]\n", + " - Could not prepend/append to [dest-rel-path]\n", 'some_service: true', TRUE, 0444,