Unverified Commit 1a191d35 authored by Alex Pott's avatar Alex Pott
Browse files

fix: #3559132 FileSystem::deleteRecursive() leaves files/directories when realpath() returns FALSE

By: oli4
By: cilefen
By: cmlara
By: rory downes
By: jesseh
By: smustgrave
(cherry picked from commit 24d12af3)
parent a50bc847
Loading
Loading
Loading
Loading
Loading
+9 −2
Original line number Diff line number Diff line
@@ -352,9 +352,16 @@ public function delete($path) {
   * {@inheritdoc}
   */
  public function deleteRecursive($path, ?callable $callback = NULL) {
    // Ensure paths are local paths when a recursive delete is started.
    // Resolve stream wrapper URIs to local paths so that is_link() can
    // detect symlinks and prevent traversal outside the target directory.
    // Remote stream wrappers return FALSE from realpath(), in which case
    // the original URI is kept since is_link() does not work with stream
    // wrapper URIs.
    if ($this->streamWrapperManager->isValidUri($path)) {
      $path = $this->realpath($path);
      $realpath = $this->realpath($path);
      if ($realpath !== FALSE) {
        $path = $realpath;
      }
    }

    if ($callback) {
+99 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\KernelTests\Core\File;

use Drupal\file_test\StreamWrapper\DummyRemoteStreamWrapper;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;

/**
 * Tests deleteRecursive() with remote stream wrappers where realpath() is FALSE.
 *
 * @internal
 */
#[Group('File')]
#[RunTestsInSeparateProcesses]
class FileDeleteRecursiveRemoteTest extends FileTestBase {

  /**
   * A stream wrapper scheme to register for the test.
   *
   * @var string
   */
  protected $scheme = 'dummy-remote';

  /**
   * A fully-qualified stream wrapper class name to register for the test.
   *
   * @var string
   */
  protected $classname = DummyRemoteStreamWrapper::class;

  /**
   * Verifies the dummy-remote stream wrapper returns FALSE from realpath().
   */
  public function testRealpathReturnsFalse(): void {
    $uri = 'dummy-remote://test.txt';
    file_put_contents($uri, 'test');
    $this->assertFalse($this->container->get('file_system')->realpath($uri));
    unlink($uri);
  }

  /**
   * Tests deleting a single file via a remote stream wrapper.
   */
  public function testSingleFile(): void {
    $filepath = 'dummy-remote://' . $this->randomMachineName();
    file_put_contents($filepath, '');

    $this->assertTrue(\Drupal::service('file_system')->deleteRecursive($filepath));
    $this->assertFileDoesNotExist($filepath);
  }

  /**
   * Tests deleting an empty directory via a remote stream wrapper.
   */
  public function testEmptyDirectory(): void {
    $directory = $this->createDirectory('dummy-remote://' . $this->randomMachineName());

    $this->assertTrue(\Drupal::service('file_system')->deleteRecursive($directory));
    $this->assertDirectoryDoesNotExist($directory);
  }

  /**
   * Tests deleting a directory with files via a remote stream wrapper.
   */
  public function testDirectory(): void {
    $directory = $this->createDirectory('dummy-remote://' . $this->randomMachineName());
    $filepathA = $directory . '/A';
    $filepathB = $directory . '/B';
    file_put_contents($filepathA, '');
    file_put_contents($filepathB, '');

    $this->assertTrue(\Drupal::service('file_system')->deleteRecursive($directory));
    $this->assertFileDoesNotExist($filepathA);
    $this->assertFileDoesNotExist($filepathB);
    $this->assertDirectoryDoesNotExist($directory);
  }

  /**
   * Tests deleting subdirectories with files via a remote stream wrapper.
   */
  public function testSubDirectory(): void {
    $directory = $this->createDirectory('dummy-remote://' . $this->randomMachineName());
    $subdirectory = $this->createDirectory($directory . '/sub');
    $filepathA = $directory . '/A';
    $filepathB = $subdirectory . '/B';
    file_put_contents($filepathA, '');
    file_put_contents($filepathB, '');

    $this->assertTrue(\Drupal::service('file_system')->deleteRecursive($directory));
    $this->assertFileDoesNotExist($filepathA);
    $this->assertFileDoesNotExist($filepathB);
    $this->assertDirectoryDoesNotExist($subdirectory);
    $this->assertDirectoryDoesNotExist($directory);
  }

}