Skip to content

Commit b1caa5e

Browse files
committed
DirectoryListingService
1 parent 7828c54 commit b1caa5e

5 files changed

Lines changed: 156 additions & 0 deletions

File tree

config/services/services.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,10 @@ services:
272272
autowire: true
273273
autoconfigure: true
274274

275+
PhpList\Core\Domain\Common\Upload\Service\DirectoryListingService:
276+
autowire: true
277+
autoconfigure: true
278+
275279
PhpList\Core\Domain\Common\Storage\UploadStorageInterface:
276280
alias: PhpList\Core\Domain\Common\Storage\LocalUploadStorage
277281

config/services/validators.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,7 @@ services:
1414
PhpList\Core\Domain\Common\Validator\UploadValidator:
1515
autowire: true
1616
autoconfigure: true
17+
18+
PhpList\Core\Domain\Common\Validator\UploadDirectoryValidator:
19+
autowire: true
20+
autoconfigure: true

public/uploads/.gitkeep

Whitespace-only changes.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Domain\Common\Service;
6+
7+
use RuntimeException;
8+
9+
class DirectoryListingService
10+
{
11+
/**
12+
* @return array<int, array{
13+
* name: string,
14+
* path: string,
15+
* size: int,
16+
* type: string,
17+
* modified: int
18+
* }>
19+
*/
20+
public function list(string $directory, string $realPath): array
21+
{
22+
$items = $this->readDirectory($realPath, $directory);
23+
24+
$files = [];
25+
26+
foreach ($items as $item) {
27+
$entry = $this->createEntry($directory, $realPath, $item);
28+
29+
if ($entry !== null) {
30+
$files[] = $entry;
31+
}
32+
}
33+
34+
$this->sortFiles($files);
35+
36+
return $files;
37+
}
38+
39+
private function readDirectory(string $realPath, string $directory): array
40+
{
41+
$items = scandir($realPath);
42+
43+
if ($items === false) {
44+
throw new RuntimeException(
45+
sprintf('Unable to read directory "%s".', $directory)
46+
);
47+
}
48+
49+
return $items;
50+
}
51+
52+
private function sortFiles(array &$files): void
53+
{
54+
usort(
55+
$files,
56+
static function (array $file1, array $file2): int {
57+
if ($file1['type'] !== $file2['type']) {
58+
return $file1['type'] === 'directory' ? -1 : 1;
59+
}
60+
61+
return strcmp($file1['name'], $file2['name']);
62+
}
63+
);
64+
}
65+
66+
/**
67+
* @return array{
68+
* name:string,
69+
* path:string,
70+
* size:int,
71+
* type:string,
72+
* modified:int
73+
* }|null
74+
*/
75+
private function createEntry(string $directory, string $realPath, string $item,): ?array
76+
{
77+
if ($item === '.' || $item === '..') {
78+
return null;
79+
}
80+
81+
$fullPath = $realPath . DIRECTORY_SEPARATOR . $item;
82+
83+
if (!is_file($fullPath) && !is_dir($fullPath)) {
84+
return null;
85+
}
86+
87+
$isDirectory = is_dir($fullPath);
88+
89+
return [
90+
'name' => $item,
91+
'path' => '/' . trim($directory, '/') . '/' . $item,
92+
'size' => $isDirectory ? 0 : (filesize($fullPath) ?: 0),
93+
'type' => $isDirectory ? 'directory' : 'file',
94+
'modified' => filemtime($fullPath) ?: 0,
95+
];
96+
}
97+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Domain\Common\Validator;
6+
7+
use Symfony\Component\DependencyInjection\Attribute\Autowire;
8+
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
9+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
10+
11+
class UploadDirectoryValidator
12+
{
13+
public function __construct(
14+
#[Autowire('%kernel.project_dir%/public')]
15+
private readonly string $publicPath,
16+
) {
17+
}
18+
19+
public function validate(string $directory): string
20+
{
21+
$directory = trim($directory);
22+
23+
if ($directory === '') {
24+
throw new BadRequestHttpException('Directory name cannot be empty.');
25+
}
26+
27+
if (!preg_match('#^[A-Za-z0-9/_-]+$#', $directory)) {
28+
throw new BadRequestHttpException('Invalid directory name.');
29+
}
30+
31+
$requestedPath = $this->publicPath . '/' . ltrim($directory, '/');
32+
33+
$realBasePath = realpath($this->publicPath);
34+
$realRequestedPath = realpath($requestedPath);
35+
36+
if ($realBasePath === false || $realRequestedPath === false) {
37+
throw new NotFoundHttpException(sprintf('Directory "%s" not found.', $directory));
38+
}
39+
40+
if (!str_starts_with($realRequestedPath, $realBasePath . DIRECTORY_SEPARATOR)
41+
&& $realRequestedPath !== $realBasePath) {
42+
throw new BadRequestHttpException('Invalid directory.');
43+
}
44+
45+
if (!is_dir($realRequestedPath)) {
46+
throw new NotFoundHttpException(sprintf('Directory "%s" not found.', $directory));
47+
}
48+
49+
return $realRequestedPath;
50+
}
51+
}

0 commit comments

Comments
 (0)