Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Fieldtypes/TemplateFolder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

use FilesystemIterator;
use RecursiveCallbackFilterIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use Statamic\Filesystem\RecursiveDirectoryIterator;
use Statamic\Support\Str;

class TemplateFolder extends Relationship
Expand Down
25 changes: 25 additions & 0 deletions src/Filesystem/RecursiveDirectoryIterator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace Statamic\Filesystem;

class RecursiveDirectoryIterator extends \RecursiveDirectoryIterator
{
public function hasChildren(bool $allowLinks = false): bool
{
if (parent::hasChildren($allowLinks)) {
return true;
}

if (DIRECTORY_SEPARATOR !== '\\') {
return false;
}

// A Windows junction reports an lstat mode that is neither a link nor a
// directory, so the parent treats it as a leaf and FOLLOW_SYMLINKS never
// gets a look in. The parent has just lstat'd the path, and is_dir() would
// reuse that cached result, so the cache needs clearing before asking.
clearstatcache(true, $path = $this->getPathname());

return is_dir($path);
}
}
2 changes: 1 addition & 1 deletion src/Http/Controllers/CP/API/TemplatesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
namespace Statamic\Http\Controllers\CP\API;

use RecursiveCallbackFilterIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use Statamic\Filesystem\RecursiveDirectoryIterator;
use Statamic\Http\Controllers\CP\CpController;
use Statamic\Support\Str;

Expand Down
36 changes: 36 additions & 0 deletions tests/DeletesDirectories.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace Tests;

use FilesystemIterator;

trait DeletesDirectories
{
// Laravel's deleteDirectory() reaches for unlink(), which on Windows cannot remove
// anything carrying the directory attribute. Symlinked and junctioned directories
// survive it, along with every directory above them.
protected function deleteDirectory(string $directory): void
{
if (! is_dir($directory)) {
return;
}

foreach (new FilesystemIterator($directory, FilesystemIterator::SKIP_DOTS) as $item) {
$path = $item->getPathname();

// Deliberately no is_dir()/is_link() calls. A junction reports an lstat
// mode that is neither, and PHP caches an lstat result as the stat result
// when it decides the path isn't a link, so the two answers contradict
// each other. unlink() removes files and file symlinks, rmdir() removes
// empty directories, directory symlinks and junctions without touching
// what they point at, and anything surviving both has contents in it.
if (@unlink($path) || @rmdir($path)) {
continue;
}

$this->deleteDirectory($path);
}

@rmdir($directory);
}
}
51 changes: 51 additions & 0 deletions tests/DeletesDirectoriesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace Tests;

use PHPUnit\Framework\Attributes\Test;
use Statamic\Facades\File;

class DeletesDirectoriesTest extends TestCase
{
use DeletesDirectories;

private string $dir;

public function setUp(): void
{
parent::setUp();

$this->dir = __DIR__.'/deletes-directories-tmp';
}

public function tearDown(): void
{
$this->deleteDirectory($this->dir);

parent::tearDown();
}

#[Test]
public function it_deletes_a_directory_containing_links_without_touching_their_targets()
{
File::put($this->dir.'/target-dir/kept.html', '');
File::put($this->dir.'/target-file.html', '');

File::put($this->dir.'/subject/file.html', '');
File::put($this->dir.'/subject/nested/deep.html', '');
File::makeDirectory($this->dir.'/subject/empty');

// A directory link is a junction on Windows, where neither unlink() nor an
// is_dir()/is_link() check behaves the way it does everywhere else.
app('files')->link($this->dir.'/target-dir', $this->dir.'/subject/linked-dir');
app('files')->link($this->dir.'/target-file.html', $this->dir.'/subject/linked-file.html');

$this->deleteDirectory($this->dir.'/subject');

clearstatcache();

$this->assertFalse(is_dir($this->dir.'/subject'));
$this->assertTrue(is_file($this->dir.'/target-dir/kept.html'));
$this->assertTrue(is_file($this->dir.'/target-file.html'));
}
}
7 changes: 5 additions & 2 deletions tests/Fieldtypes/TemplateFolderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,27 @@
use Statamic\Facades\File;
use Statamic\Fields\Field;
use Statamic\Fieldtypes\TemplateFolder;
use Tests\DeletesDirectories;
use Tests\TestCase;

class TemplateFolderTest extends TestCase
{
use DeletesDirectories;

private string $dir;

public function setUp(): void
{
parent::setUp();

app('files')->makeDirectory($this->dir = __DIR__.'/templates-test-tmp', force: true);
app('files')->makeDirectory($this->dir = __DIR__.'/template-folder-test-tmp', force: true);

$this->app['config']->set('view.paths', [$this->dir.'/views']);
}

public function tearDown(): void
{
app('files')->deleteDirectory($this->dir);
$this->deleteDirectory($this->dir);

parent::tearDown();
}
Expand Down
5 changes: 3 additions & 2 deletions tests/Fieldtypes/TemplatesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
use PHPUnit\Framework\Attributes\Test;
use Statamic\Facades\File;
use Statamic\Facades\User;
use Tests\DeletesDirectories;
use Tests\PreventSavingStacheItemsToDisk;
use Tests\TestCase;

class TemplatesTest extends TestCase
{
use PreventSavingStacheItemsToDisk;
use DeletesDirectories, PreventSavingStacheItemsToDisk;

private string $dir;

Expand All @@ -25,7 +26,7 @@ public function setUp(): void

public function tearDown(): void
{
app('files')->deleteDirectory($this->dir);
$this->deleteDirectory($this->dir);

parent::tearDown();
}
Expand Down
Loading