Verified Commit 60d7868a authored by godotislate's avatar godotislate
Browse files

fix: #3091285 Composer scaffolding fails when permissions on...

fix: #3091285 Composer scaffolding fails when permissions on default.settings.yml or default.settings.php is not writable.

By: mglaman
By: mile23
By: afeijo
By: Mixologic
By: lolandese
By: larowlan
By: jannakha
By: mmjvb
By: thursday_bw
By: xmacinfo
By: tedbow
By: berdir
By: vakulrai
By: emersonreis.dev
By: trackleft2
By: nmudgal
By: greg.1.anderson
By: Marsell key
By: wim leers
By: catch
By: alexpott
By: godotislate
By: longwave
(cherry picked from commit 06650578)
parent c1ace2bc
Loading
Loading
Loading
Loading
Loading
+20 −0
Original line number Diff line number Diff line
@@ -50,4 +50,24 @@ public function scaffoldAtNewLocation(ScaffoldFilePath $destination) {
    return $this;
  }

  /**
   * Adds the owner write permission bit to the path.
   *
   * @param string $filepath
   *   Path to the file or directory.
   *
   * @return bool
   *   TRUE on success, FALSE if the path does not exist or chmod failed.
   */
  protected function makeWritable(string $filepath): bool {
    if (!file_exists($filepath)) {
      return FALSE;
    }
    $mod = fileperms($filepath);
    if ($mod === FALSE) {
      return FALSE;
    }
    return @chmod($filepath, ($mod & 0777) | 0200);
  }

}
+33 −8
Original line number Diff line number Diff line
@@ -60,23 +60,48 @@ protected function generateContents() {
  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 <info>[dest-rel-path]</info> because it already exists and overwrite is <comment>false</comment>."));
      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.
    $destination_directory = dirname($destination_path);
    $reset_directory_perms = FALSE;
    // Capture perms before any changes. NULL means the path does not exist yet.
    $original_directory_perms = file_exists($destination_directory) ? (fileperms($destination_directory) ?: NULL) : NULL;
    $original_file_perms = file_exists($destination_path) ? (fileperms($destination_path) ?: NULL) : NULL;

    if ($original_directory_perms !== NULL && !is_writable($destination_directory)) {
      if (!$this->makeWritable($destination_directory)) {
        throw new \RuntimeException($interpolator->interpolate("Failed to make the directory containing <info>[dest-rel-path]</info> writable."));
      }
      $reset_directory_perms = TRUE;
    }

    try {
      // Remove the destination if it exists,
      // and ensure the destination directory exists.
      $fs->remove($destination_path);
    $fs->ensureDirectoryExists(dirname($destination_path));
      $fs->ensureDirectoryExists($destination_directory);

      if ($options->symlink()) {
        return $this->symlinkScaffold($destination, $io);
      }

      return $this->copyScaffold($destination, $io);
    }
    finally {
      if ($reset_directory_perms && $original_directory_perms !== NULL) {
        @chmod($destination_directory, $original_directory_perms);
      }
      if ($original_file_perms !== NULL && !$options->symlink() && file_exists($destination_path)) {
        @chmod($destination_path, $original_file_perms);
      }
    }
  }

  /**
   * Copies the scaffold file.
+6 −2
Original line number Diff line number Diff line
@@ -50,12 +50,16 @@ abstract class FixturesBase {
  /**
   * Gets an IO fixture.
   *
   * @param bool $reset
   *   Whether to reset the io fixture. This is useful for tests that need to
   *   write to the io fixture multiple times.
   *
   * @return \Composer\IO\IOInterface
   *   A Composer IOInterface to write to; output may be retrieved via
   *   Fixtures::getOutput().
   */
  public function io(): IOInterface {
    if (!isset($this->io)) {
  public function io(bool $reset = FALSE): IOInterface {
    if ($reset || !isset($this->io)) {
      $this->io = new BufferIO();
    }
    return $this->io;
+33 −0
Original line number Diff line number Diff line
@@ -39,6 +39,39 @@ public function testProcess(): void {
    // Confirm that expected output was written to our io fixture.
    $output = $fixtures->getOutput();
    $this->assertStringContainsString('Copy [web-root]/robots.txt from assets/robots.txt', $output);

    // Test when the target file is already present and not writable.
    $this->assertDirectoryIsWritable(dirname($destination->fullPath()));
    file_put_contents($destination->fullPath(), 'This is a test');
    chmod($destination->fullPath(), 0444);
    $this->assertFileIsNotWritable($destination->fullPath());
    chmod(dirname($destination->fullPath()), 0555);
    $this->assertDirectoryIsNotWritable(dirname($destination->fullPath()));
    $sut = new ReplaceOp($source, TRUE);
    $this->assertFileExists($destination->fullPath());
    // Test the system under test.
    $sut->process($destination, $fixtures->io(TRUE), $options);
    // Assert the target contained the contents from the correct scaffold file.
    $contents = trim(file_get_contents($destination->fullPath()));
    $this->assertEquals('# Test version of robots.txt from drupal/core.', $contents);
    // Assert the target is still not writable.
    $this->assertFileIsNotWritable($destination->fullPath());
    $this->assertDirectoryIsNotWritable(dirname($destination->fullPath()));
    // Confirm that expected output was written to our io fixture.
    $output = $fixtures->getOutput();
    $this->assertStringContainsString('Copy [web-root]/robots.txt from assets/robots.txt', $output);

    // Make it a symlink and ensure that the permissions code is not executed.
    $source_perms = fileperms($source->fullPath());
    $this->assertNotEquals($source_perms, fileperms($destination->fullPath()));
    $options = ScaffoldOptions::create(['drupal-scaffold' => ['symlink' => TRUE]]);
    $sut->process($destination, $fixtures->io(TRUE), $options);
    $this->assertTrue(is_link($destination->fullPath()));
    // Confirm that expected output was written to our io fixture.
    $output = $fixtures->getOutput();
    $this->assertStringContainsString('Link [web-root]/robots.txt', $output);
    $this->assertEquals($source_perms, fileperms($destination->fullPath()));
    $this->assertEquals($source_perms, fileperms($source->fullPath()));
  }

  /**