Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.x] Prevent some folders from listing in template fieldtype #10031

Merged
merged 9 commits into from
May 27, 2024
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
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',
]);
}
}
Loading