Skip to content
Open
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
18 changes: 10 additions & 8 deletions Controller/ManagerController.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,10 @@ public function indexAction(Request $request, FileTypeService $fileTypeService):
}
$fileManager = $this->newFileManager($queryParameters);

// Folder search
$directoriesArbo = $this->retrieveSubDirectories($fileManager, $fileManager->getDirName(), \DIRECTORY_SEPARATOR, $fileManager->getBaseName());
// Folder search, walks the whole directory tree, so only when the tree is displayed
$directoriesArbo = $fileManager->getTree()
? $this->retrieveSubDirectories($fileManager, $fileManager->getDirName(), \DIRECTORY_SEPARATOR, $fileManager->getBaseName())
: null;

// File search
$finderFiles = new Finder();
Expand Down Expand Up @@ -116,7 +118,7 @@ public function indexAction(Request $request, FileTypeService $fileTypeService):
}

if ('dimension' === $orderBy) {
usort($fileArray, function (File $a, File $b) {
usort($fileArray, static function (File $a, File $b) {
$aDimension = $a->getDimension();
$bDimension = $b->getDimension();
if ($aDimension && !$bDimension) {
Expand Down Expand Up @@ -147,7 +149,7 @@ public function indexAction(Request $request, FileTypeService $fileTypeService):
if ($isJson) {
$fileList = $this->renderView('@ArtgrisFileManager/views/_manager_view.html.twig', $parameters);

return new JsonResponse(['data' => $fileList, 'badge' => $finderFiles->count(), 'treeData' => $directoriesArbo]);
return new JsonResponse(['data' => $fileList, 'badge' => \count($fileArray), 'treeData' => $directoriesArbo]);
}
$parameters['treeData'] = json_encode($directoriesArbo);

Expand Down Expand Up @@ -213,7 +215,7 @@ public function renameFileAction(Request $request, string $fileName): RedirectRe
$fileManager = $this->newFileManager($queryParameters);
$newfilePath = $fileManager->getCurrentPath().\DIRECTORY_SEPARATOR.$newfileName;
$oldfilePath = realpath($fileManager->getCurrentPath().\DIRECTORY_SEPARATOR.$fileName);
if (0 !== mb_strpos($newfilePath, $fileManager->getCurrentPath())) {
if (!str_starts_with($newfilePath, $fileManager->getCurrentPath())) {
$this->addFlash('danger', $this->translator->trans('file.renamed.unauthorized'));
} else {
$fs = new Filesystem();
Expand Down Expand Up @@ -305,7 +307,7 @@ public function deleteAction(Request $request): RedirectResponse {
$is_delete = false;
foreach ($queryParameters['delete'] as $fileName) {
$filePath = realpath($fileManager->getCurrentPath().\DIRECTORY_SEPARATOR.$fileName);
if (0 !== mb_strpos($filePath, $fileManager->getCurrentPath())) {
if (!str_starts_with($filePath, $fileManager->getCurrentPath())) {
$this->addFlash('danger', 'file.deleted.danger');
} else {
try {
Expand Down Expand Up @@ -338,7 +340,7 @@ public function deleteAction(Request $request): RedirectResponse {

$this->dispatch(FileManagerEvents::POST_DELETE_FOLDER);
$queryParameters['route'] = \dirname($fileManager->getRoute());
if ($queryParameters['route'] == '/') {
if ($queryParameters['route'] === '/') {
unset($queryParameters['route']);
}

Expand Down Expand Up @@ -449,7 +451,7 @@ private function newFileManager(array $queryParameters): FileManager {
return $this->fileManager;
}

protected function dispatch(string $eventName, array $arguments = []) {
protected function dispatch(string $eventName, array $arguments = []): void {
$arguments = array_replace([
'filemanager' => $this->fileManager,
], $arguments);
Expand Down
31 changes: 25 additions & 6 deletions Helpers/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,45 @@
namespace Artgris\Bundle\FileManagerBundle\Helpers;

use Artgris\Bundle\FileManagerBundle\Service\FileTypeService;
use Symfony\Component\ErrorHandler\ErrorHandler;
use Symfony\Component\Finder\SplFileInfo;
use Symfony\Contracts\Translation\TranslatorInterface;

class File
{
private $preview;
public const IMAGE_EXTENSIONS = ['gif', 'png', 'jpg', 'jpeg', 'svg', 'webp'];

private ?array $preview = null;

private array|false|string|null $dimension = null;

/**
* File constructor.
*/
public function __construct(private SplFileInfo $file,private TranslatorInterface $translator,private FileTypeService $fileTypeService,private FileManager $fileManager)
{
$this->preview = $this->fileTypeService->preview($this->fileManager, $this->file);
}

/**
* Memoized: sorting by dimension calls this for every comparison.
*
* @return array|false|string
*/
public function getDimension(): bool|array|string {
return preg_match('/(gif|png|jpe?g|svg|webp)$/i', $this->file->getExtension()) ?
@getimagesize($this->file->getPathname()) : '';
return $this->dimension ??= $this->readDimension();
}

private function readDimension(): array|false|string {
if (!\in_array(strtolower($this->file->getExtension()), self::IMAGE_EXTENSIONS, true)) {
return '';
}

try {
// Turns the warning emitted for unreadable or corrupt images into an exception
return ErrorHandler::call('getimagesize', $this->file->getPathname());
} catch (\ErrorException) {
return false;
}
}

public function getHTMLDimension(): ?string {
Expand Down Expand Up @@ -68,7 +86,7 @@ public function getAttribut(): ?string {
}

public function isImage(): bool {
return \array_key_exists('image', $this->preview);
return \array_key_exists('image', $this->getPreview());
}

public function getFile(): SplFileInfo {
Expand All @@ -81,7 +99,8 @@ public function setFile(SplFileInfo $file) :void
}

public function getPreview(): array {
return $this->preview;
// Lazy: the preview renders a Twig template, so build it only for files that are displayed
return $this->preview ??= $this->fileTypeService->preview($this->fileManager, $this->file);
}

public function setPreview(array $preview) :void
Expand Down
2 changes: 1 addition & 1 deletion Helpers/FileManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public function getRegex(): string

return match ($this->getType()) {
'media' => '/\.(mp4|ogg|webm)$/i',
'image' => '/\.(gif|png|jpe?g|svg|webp)$/i',
'image' => '/\.('.implode('|', File::IMAGE_EXTENSIONS).')$/i',
default => '/.+$/i',
};
}
Expand Down
25 changes: 12 additions & 13 deletions Service/FileTypeService.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

namespace Artgris\Bundle\FileManagerBundle\Service;

use Artgris\Bundle\FileManagerBundle\Helpers\File;
use Artgris\Bundle\FileManagerBundle\Helpers\FileManager;
use SplFileInfo;
use Symfony\Component\Routing\RouterInterface;
use Twig\Environment;

class FileTypeService {
const IMAGE_SIZE = [
public const IMAGE_SIZE = [
FileManager::VIEW_LIST => '22',
FileManager::VIEW_THUMBNAIL => '100',
];
Expand Down Expand Up @@ -72,19 +73,17 @@ public function fileIcon(string $filePath,?string $extension = null, ?int $size

$imageTemplate = null;

if (null === $extension) {
$filePathTmp = strtok($filePath, '?');
$extension = pathinfo($filePathTmp, PATHINFO_EXTENSION);
}
// Strip the query string before reading the extension, then match it case-insensitively
$extension = strtolower($extension ?? (new SplFileInfo(explode('?', $filePath)[0]))->getExtension());
switch (true) {
case $this->isYoutubeVideo($filePath):
case preg_match('/(mp4|ogg|webm|avi|wmv|mov)$/i', $extension):
case \in_array($extension, ['mp4', 'ogg', 'webm', 'avi', 'wmv', 'mov'], true):
$fa = 'far fa-file-video';
break;
case preg_match('/(mp3|wav)$/i', $extension):
case \in_array($extension, ['mp3', 'wav'], true):
$fa = 'far fa-file-audio';
break;
case preg_match('/(gif|png|jpe?g|svg|webp)$/i', $extension):
case \in_array($extension, File::IMAGE_EXTENSIONS, true):

$fileName = $filePath;
if ($cachebreaker) {
Expand Down Expand Up @@ -112,19 +111,19 @@ public function fileIcon(string $filePath,?string $extension = null, ?int $size
'html' => $html,
'image' => true,
];
case preg_match('/(pdf)$/i', $extension):
case 'pdf' === $extension:
$fa = 'far fa-file-pdf';
break;
case preg_match('/(docx?)$/i', $extension):
case \in_array($extension, ['doc', 'docx'], true):
$fa = 'far fa-file-word';
break;
case preg_match('/(xlsx?|csv)$/i', $extension):
case \in_array($extension, ['xls', 'xlsx', 'csv'], true):
$fa = 'far fa-file-excel';
break;
case preg_match('/(pptx?)$/i', $extension):
case \in_array($extension, ['ppt', 'pptx'], true):
$fa = 'far fa-file-powerpoint';
break;
case preg_match('/(zip|rar|gz)$/i', $extension):
case \in_array($extension, ['zip', 'rar', 'gz', 'tgz'], true):
$fa = 'far fa-file-archive';
break;
case filter_var($filePath, FILTER_VALIDATE_URL):
Expand Down
26 changes: 26 additions & 0 deletions Tests/Controller/ManagerControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,30 @@ public function testTableTr()
$this->assertSame($file, $crawler->filter('.list-blk tbody tr')->eq($i)->attr('class'));
}
}

public function testJsonWithoutTreeSkipsTreeData()
{
$this->getBackendPage(['conf' => 'default', 'tree' => 0, 'json' => 1]);
$data = json_decode($this->client->getResponse()->getContent(), true);

$this->assertNull($data['treeData']);
// SubDir and text.txt
$this->assertSame(2, $data['badge']);
}

public function testJsonWithTreeHasTreeData()
{
$this->getBackendPage(['conf' => 'default', 'json' => 1]);
$data = json_decode($this->client->getResponse()->getContent(), true);

$this->assertNotEmpty($data['treeData']);
// tree mode lists files only
$this->assertSame(1, $data['badge']);
}

public function testOrderByDimension()
{
$this->getBackendPage(['conf' => 'default', 'orderby' => 'dimension']);
$this->assertSame(200, $this->client->getResponse()->getStatusCode());
}
}
81 changes: 81 additions & 0 deletions Tests/Helpers/FileTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

namespace Artgris\Bundle\FileManagerBundle\Tests\Helpers;

use Artgris\Bundle\FileManagerBundle\Helpers\File;
use Artgris\Bundle\FileManagerBundle\Helpers\FileManager;
use Artgris\Bundle\FileManagerBundle\Service\FileTypeService;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Finder\SplFileInfo;
use Symfony\Contracts\Translation\TranslatorInterface;

class FileTest extends TestCase
{
private string $dir;

protected function setUp(): void
{
$this->dir = sys_get_temp_dir().'/artgris-file-test-'.uniqid();
mkdir($this->dir);
}

protected function tearDown(): void
{
array_map('unlink', glob($this->dir.'/*'));
rmdir($this->dir);
}

public function testDimensionOfImage()
{
// 1x1 transparent PNG
$file = $this->createFile('pixel.PNG', base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='));

$this->assertSame([1, 1], \array_slice($file->getDimension(), 0, 2));
}

public function testDimensionOfCorruptImageIsFalse()
{
$file = $this->createFile('broken.png', 'not an image');

$this->assertFalse($file->getDimension());
}

public function testDimensionOfNonImageIsEmpty()
{
$file = $this->createFile('text.txt', 'text');

$this->assertSame('', $file->getDimension());
}

public function testPreviewIsNotBuiltUntilNeeded()
{
$fileTypeService = $this->createMock(FileTypeService::class);
$fileTypeService->expects($this->never())->method('preview');

$this->createFile('text.txt', 'text', $fileTypeService)->getDimension();
}

public function testPreviewIsBuiltOnce()
{
$fileTypeService = $this->createMock(FileTypeService::class);
$fileTypeService->expects($this->once())->method('preview')->willReturn(['image' => true]);

$file = $this->createFile('text.txt', 'text', $fileTypeService);

$this->assertTrue($file->isImage());
$this->assertSame(['image' => true], $file->getPreview());
$this->assertSame(['image' => true], $file->getPreview());
}

private function createFile(string $name, string $content, ?FileTypeService $fileTypeService = null): File
{
file_put_contents($this->dir.'/'.$name, $content);

return new File(
new SplFileInfo($this->dir.'/'.$name, '', $name),
$this->createMock(TranslatorInterface::class),
$fileTypeService ?? $this->createMock(FileTypeService::class),
$this->createMock(FileManager::class),
);
}
}
Loading