Skip to content

Commit

Permalink
[5.x] Prevent some folders from listing in template fieldtype (#10031)
Browse files Browse the repository at this point in the history
Co-authored-by: Jason Varga <[email protected]>
  • Loading branch information
peimn and jasonvarga authored May 27, 2024
1 parent a18562a commit 46b4d39
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 42 deletions.
30 changes: 15 additions & 15 deletions src/Fieldtypes/TemplateFolder.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Statamic\Fieldtypes;

use FilesystemIterator;
use RecursiveCallbackFilterIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use Statamic\Support\Str;
Expand All @@ -18,20 +20,18 @@ protected function toItemArray($id, $site = null)

public function getIndexItems($request)
{
return collect(config('view.paths'))
->flatMap(function ($path) {
$directories = collect();
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path), RecursiveIteratorIterator::SELF_FIRST);

foreach ($iterator as $file) {
if ($file->isDir() && ! $iterator->isDot() && ! $iterator->isLink()) {
$directories->push(Str::replaceFirst($path.DIRECTORY_SEPARATOR, '', $file->getPathname()));
}
}

return $directories->filter()->values();
})
->map(fn ($folder) => ['id' => $folder, 'title' => $folder])
->values();
return collect(config('view.paths'))->flatMap(function ($path) {
return collect(new RecursiveIteratorIterator(
new RecursiveCallbackFilterIterator(
new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS),
fn ($file) => $file->isDir() && ! str_starts_with($file->getFilename(), '.') && ! in_array($file->getBaseName(), ['node_modules'])
),
RecursiveIteratorIterator::SELF_FIRST
))->map(fn ($file) => Str::of($file->getPathname())
->after($path.DIRECTORY_SEPARATOR)
->replace('\\', '/')
->toString()
);
})->map(fn ($folder) => ['id' => $folder, 'title' => $folder])->sort()->values();
}
}
33 changes: 13 additions & 20 deletions src/Http/Controllers/CP/API/TemplatesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Statamic\Http\Controllers\CP\API;

use RecursiveCallbackFilterIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use Statamic\Http\Controllers\CP\CpController;
Expand All @@ -11,25 +12,17 @@ class TemplatesController extends CpController
{
public function index()
{
return collect(config('view.paths'))
->flatMap(function ($path) {
$views = collect();
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path));

foreach ($iterator as $file) {
if ($file->isFile()) {
$viewPath = Str::of($file->getPathname())
->after($path.DIRECTORY_SEPARATOR)
->before('.')
->replace('\\', '/')
->toString();

$views->push($viewPath);
}
}

return $views->filter()->sort()->values();
})
->values();
return collect(config('view.paths'))->flatMap(function ($path) {
return collect(new RecursiveIteratorIterator(
new RecursiveCallbackFilterIterator(
new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS | RecursiveDirectoryIterator::FOLLOW_SYMLINKS),
fn ($file) => ! str_starts_with($file->getFilename(), '.') && ! in_array($file->getBaseName(), ['node_modules'])
)
))->map(fn ($file) => Str::of($file->getPathname())
->after($path.DIRECTORY_SEPARATOR)
->before('.')
->replace('\\', '/')
)->sort()->values();
});
}
}
108 changes: 108 additions & 0 deletions tests/Fieldtypes/TemplateFolderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

namespace Tests\Fieldtypes;

use Statamic\Facades\File;
use Statamic\Fields\Field;
use Statamic\Fieldtypes\TemplateFolder;
use Tests\TestCase;

class TemplateFolderTest extends TestCase
{
private string $dir;

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

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

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

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

parent::tearDown();
}

/** @test */
public function it_returns_a_list_of_directories()
{
$this->createFiles();

$fieldtype = $this->fieldtype();

$items = $fieldtype->getIndexItems(request());

// A collection with identical id/title keys are returned but we're only really concerned about the content.
$actual = $items->map->id->all();

$this->assertEquals([
'empty',
'empty-symlink',
'empty-symlink/three',
'one',
'one/two',
'symlink-dir',
'symlink-dir/five',
'symlink-dir/four',
], $actual);
}

private function createFiles()
{
$files = [
// Regular files, these should all be shown.
'alfa.html',
'one/bravo.html',
'one/two/charlie.html',
'one/two/delta.html',

// .git directories at any level should get filtered out
'.git/echo.html',
'one/.git/foxtrot.html',
'one/two/.git/golf.html',

// node_modules at any level should get filtered out
'node_modules/hotel.html',
'one/node_modules/india.html',
'one/two/node_modules/juliett.html',

// dotfiles at any level should get filtered out
'.kilo.html',
'one/.lima.html',
'one/two/.mike.html',
];

foreach ($files as $path) {
File::put($this->dir.'/views/'.$path, '');
}

// Empty directories should also be shown.
File::makeDirectory($this->dir.'/views/empty');

// Symlinked directories (even empties) should be shown.
File::makeDirectory($this->dir.'/empty-symlink-target');
File::makeDirectory($this->dir.'/empty-symlink-target/three');
File::put($this->dir.'/symlink-target-dir/tango.html', '');
File::put($this->dir.'/symlink-target-dir/four/uniform.html', '');
File::makeDirectory($this->dir.'/symlink-target-dir/five');
symlink($this->dir.'/empty-symlink-target', $this->dir.'/views/empty-symlink');
symlink($this->dir.'/symlink-target-dir', $this->dir.'/views/symlink-dir');

// Symlinked files should not.
File::put($this->dir.'/foo.html', '');
symlink($this->dir.'/foo.html', $this->dir.'/views/victor.html');
}

private function fieldtype()
{
$field = new Field('test', array_merge([
'type' => 'template_folder',
]));

return (new TemplateFolder)->setField($field);
}
}
75 changes: 68 additions & 7 deletions tests/Fieldtypes/TemplatesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Tests\Fieldtypes;

use Statamic\Facades\File;
use Statamic\Facades\User;
use Tests\PreventSavingStacheItemsToDisk;
use Tests\TestCase;
Expand All @@ -10,26 +11,86 @@ class TemplatesTest extends TestCase
{
use PreventSavingStacheItemsToDisk;

private string $dir;

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

$this->app['config']->set('view.paths', [
__DIR__.'/../__fixtures__/templates',
]);
app('files')->makeDirectory($this->dir = __DIR__.'/templates-test-tmp', force: true);

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

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

parent::tearDown();
}

/** @test */
public function it_returns_a_list_of_templates()
{
$files = [
// Regular files, these should all be shown.
'alfa.html',
'one/bravo.html',
'one/two/charlie.html',
'one/two/delta.html',

// .git directories at any level should get filtered out
'.git/echo.html',
'one/.git/foxtrot.html',
'one/two/.git/golf.html',

// node_modules at any level should get filtered out
'node_modules/hotel.html',
'one/node_modules/india.html',
'one/two/node_modules/juliett.html',

// dot directories at any level should get filtered out
'.kilo/lima.html',
'one/.mike/november.html',
'one/two/.oscar/papa.html',

// dotfiles at any level should get filtered out
'.quebec.html',
'one/.rome.html',
'one/two/.sierra.html',
];

foreach ($files as $path) {
File::put($this->dir.'/views/'.$path, '');
}

// Empty directories should be ignored.
File::makeDirectory($this->dir.'/views/empty');

// Empty symlinked directories should be ignored.
File::makeDirectory($this->dir.'/empty-symlink-target');
app('files')->link($this->dir.'/empty-symlink-target', $this->dir.'/views/empty-symlink');

// Files in symlinked directories should be shown.
File::put($this->dir.'/symlink-target-dir/tango.html', '');
File::put($this->dir.'/symlink-target-dir/three/uniform.html', '');
app('files')->link($this->dir.'/symlink-target-dir', $this->dir.'/views/symlink-dir');

// Symlinked files should be shown.
File::put($this->dir.'/foo.html', '');
app('files')->link($this->dir.'/foo.html', $this->dir.'/views/victor.html');

$this
->actingAs(User::make()->makeSuper()->save())
->get(cp_route('api.templates.index'))
->assertJson([
'blog/index',
'conditions-literals',
'five_hundred_nested_ifs',
'nested-conditionals',
'alfa',
'one/bravo',
'one/two/charlie',
'one/two/delta',
'symlink-dir/tango',
'symlink-dir/three/uniform',
'victor',
]);
}
}

0 comments on commit 46b4d39

Please sign in to comment.