ajout parent dir
All checks were successful
Apply PHP CS Fixer / php-cs-fixer (push) Successful in 29s
CI / build-test (push) Successful in 1m39s
rector / Rector (push) Successful in 1m17s

This commit is contained in:
Melaine Gérard 2025-01-26 14:10:11 +01:00
parent 3a8fc71eb2
commit 2b52d18da5
11 changed files with 312 additions and 49 deletions

View File

@ -51,7 +51,7 @@
"symfony/web-link": "7.2.*",
"symfony/yaml": "7.2.*",
"symfonycasts/reset-password-bundle": "^1.23.1",
"symfonycasts/tailwind-bundle": "^0.7.0",
"symfonycasts/tailwind-bundle": "^0.7.1",
"tales-from-a-dev/flowbite-bundle": "^0.7.1",
"twig/extra-bundle": "^2.12|^3.18",
"twig/twig": "^2.12|^3.18"

14
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "13818ed85d08a0fb5be7425a3277b4c7",
"content-hash": "d8ceef27639b2bdc9c0081bebf3a7320",
"packages": [
{
"name": "composer/semver",
@ -8217,16 +8217,16 @@
},
{
"name": "symfonycasts/tailwind-bundle",
"version": "v0.7.0",
"version": "v0.7.1",
"source": {
"type": "git",
"url": "https://github.com/SymfonyCasts/tailwind-bundle.git",
"reference": "5c0e36694a49f017a06d0331e45ca384fc5f7677"
"reference": "81c9e6ff2bb1a95e67fc6af04ca87fccdcf55aa4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SymfonyCasts/tailwind-bundle/zipball/5c0e36694a49f017a06d0331e45ca384fc5f7677",
"reference": "5c0e36694a49f017a06d0331e45ca384fc5f7677",
"url": "https://api.github.com/repos/SymfonyCasts/tailwind-bundle/zipball/81c9e6ff2bb1a95e67fc6af04ca87fccdcf55aa4",
"reference": "81c9e6ff2bb1a95e67fc6af04ca87fccdcf55aa4",
"shasum": ""
},
"require": {
@ -8266,9 +8266,9 @@
],
"support": {
"issues": "https://github.com/SymfonyCasts/tailwind-bundle/issues",
"source": "https://github.com/SymfonyCasts/tailwind-bundle/tree/v0.7.0"
"source": "https://github.com/SymfonyCasts/tailwind-bundle/tree/v0.7.1"
},
"time": "2025-01-22T16:31:51+00:00"
"time": "2025-01-23T14:54:07+00:00"
},
{
"name": "tales-from-a-dev/flowbite-bundle",

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250126120344 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE parent_directory_permission (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, role VARCHAR(255) NOT NULL, read BOOLEAN NOT NULL, write BOOLEAN NOT NULL, parent_directory_id INTEGER NOT NULL, CONSTRAINT FK_F93986627CFA5BB1 FOREIGN KEY (parent_directory_id) REFERENCES parent_directory (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('CREATE INDEX IDX_F93986627CFA5BB1 ON parent_directory_permission (parent_directory_id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE parent_directory_permission');
}
}

View File

@ -43,11 +43,7 @@ class FilesController extends AbstractController
public function index(Filesystem $defaultAdapter, UrlGeneratorInterface $urlGenerator, #[MapQueryParameter('path')] string $path = ''): Response
{
$path = $this->normalizePath($path);
/**
* @var User $user
*/
$user = $this->getUser();
$this->getUser();
if ('' !== $path) {
$parentDir = $this->parentDirectoryRepository->findOneBy(['name' => $path]);
@ -55,9 +51,7 @@ class FilesController extends AbstractController
throw $this->createNotFoundException("Ce dossier n'existe pas !");
}
if (!$this->isGranted('file', $parentDir)) {
if (!$this->isGranted('file_read', $parentDir)) {
throw $this->createNotFoundException("Vous n'avez pas le droit d'accéder à ce dossier !");
}
}
@ -72,15 +66,15 @@ class FilesController extends AbstractController
// On vérifie si l'utilisateur a le droit d'accéder au fichier (vérifier que owner_role du parentDirectory correspondant est bien le folderRole de l'utilisateur)
$pathFile = explode('/', (string) $file['path']);
if ('' !== $path) {
$parentDir = $this->parentDirectoryRepository->findOneBy(['name' => $pathFile[0]]);
$parentDirectory = $this->parentDirectoryRepository->findOneBy(['name' => $pathFile[0]]);
if (null === $parentDir || !$this->isGranted('file', $parentDir)) {
if (null === $parentDirectory || !$this->isGranted('file_read', $parentDirectory)) {
continue;
}
} elseif ('file' !== $file['type']) {
$parentDir = $this->parentDirectoryRepository->findOneBy(['name' => $filename]);
$parentDirectory = $this->parentDirectoryRepository->findOneBy(['name' => $filename]);
if (null === $parentDir || !$this->isGranted('file', $parentDir)) {
if (null === $parentDirectory || !$this->isGranted('file_read', $parentDirectory)) {
continue;
}
}
@ -108,6 +102,7 @@ class FilesController extends AbstractController
return $this->render('files/index.html.twig', [
'files' => $realFiles,
'path' => $path,
'parentDir' => $parentDir ?? null,
]);
}
@ -134,7 +129,7 @@ class FilesController extends AbstractController
*/
$user = $this->getUser();
if (!$this->isGranted('file', $parentDir)) {
if (!$this->isGranted('file_read', $parentDir)) {
throw $this->createNotFoundException("Vous n'avez pas le droit d'accéder à ce fichier !");
}
}
@ -167,18 +162,16 @@ class FilesController extends AbstractController
#[IsGranted('ROLE_USER')]
public function fileDelete(Filesystem $defaultAdapter, #[MapQueryParameter('filename')] string $filename): RedirectResponse
{
/**
* @var User $user
*/
$user = $this->getUser();
$this->getUser();
$file = $this->normalizePath($filename);
$realPath = explode('/', $file);
$parentDir = null;
if (count($realPath) > 1) {
$parentDir = $this->parentDirectoryRepository->findOneBy(['name' => $realPath[0]]);
if (null === $parentDir || !$this->isGranted('file', $parentDir)) {
if (null === $parentDir || !$this->isGranted('file_write', $parentDir)) {
throw $this->createNotFoundException("Vous n'avez pas le droit de supprimer ce fichier !");
}
}
@ -204,28 +197,35 @@ class FilesController extends AbstractController
public function directoryDelete(Filesystem $defaultAdapter, #[MapQueryParameter('path')] string $path): RedirectResponse
{
$path = $this->normalizePath($path);
/**
* @var User $user
*/
$user = $this->getUser();
$this->getUser();
$realPath = explode('/', $path);
$parentDir = $this->parentDirectoryRepository->findOneBy(['name' => $realPath[0]]);
if (null === $parentDir || !$this->isGranted('file', $parentDir)) {
if (null === $parentDir || !$this->isGranted('file_write', $parentDir)) {
throw $this->createNotFoundException("Vous n'avez pas le droit de supprimer ce dossier !");
}
if ('' !== $path && !str_starts_with($path, '.') && $defaultAdapter->directoryExists($path)) {
$defaultAdapter->deleteDirectory($path);
if ($parentDir->getName() === $path) {
$this->entityManager->remove($parentDir);
$this->entityManager->flush();
}
$this->addFlash('success', 'Le dossier a bien été supprimé.');
} else {
$this->addFlash('error', 'Le dossier n\'existe pas.');
}
$newPath = dirname($path);
if ('.' === $newPath) {
$newPath = '';
}
return $this->redirectToRoute('app_files_index', [
'path' => dirname($path),
'path' => $newPath,
]);
}
@ -237,10 +237,7 @@ class FilesController extends AbstractController
public function rename(#[MapQueryParameter('path')] string $filepath, Request $request, Filesystem $defaultAdapter): Response
{
$filepath = $this->normalizePath($filepath);
/**
* @var User $user
*/
$user = $this->getUser();
$this->getUser();
if ('' === $filepath || str_starts_with($filepath, '.') || !$defaultAdapter->fileExists($filepath)) {
throw $this->createNotFoundException("Ce fichier n'existe pas !");
@ -251,7 +248,7 @@ class FilesController extends AbstractController
if (count($realPath) > 1) {
$parentDir = $this->parentDirectoryRepository->findOneBy(['name' => $realPath[0]]);
if (null === $parentDir || !$this->isGranted('file', $parentDir)) {
if (null === $parentDir || !$this->isGranted('file_write', $parentDir)) {
throw $this->createNotFoundException("Vous n'avez pas le droit de renommer ce fichier !");
}
}
@ -302,7 +299,7 @@ class FilesController extends AbstractController
if (count($realPath) > 1) {
$parentDir = $this->parentDirectoryRepository->findOneBy(['name' => $realPath[0]]);
if (null === $parentDir || !$this->isGranted('file', $parentDir)) {
if (null === $parentDir || !$this->isGranted('file_write', $parentDir)) {
throw $this->createNotFoundException("Vous n'avez pas le droit de créer de sous-dossier dans ce dossier !");
}
}
@ -368,15 +365,12 @@ class FilesController extends AbstractController
public function renameDirectory(#[MapQueryParameter('path')] string $filepath, Request $request, Filesystem $defaultAdapter): Response
{
$filepath = $this->normalizePath($filepath);
/**
* @var User $user
*/
$user = $this->getUser();
$this->getUser();
$realPath = explode('/', $filepath);
$parentDir = $this->parentDirectoryRepository->findOneBy(['name' => $realPath[0]]);
if (null === $parentDir || !$this->isGranted('file', $parentDir)) {
if (null === $parentDir || !$this->isGranted('file_write', $parentDir)) {
throw $this->createNotFoundException("Vous n'avez pas le droit de renommer ce dossier !");
}
@ -423,10 +417,7 @@ class FilesController extends AbstractController
{
$path = $this->normalizePath($path);
/**
* @var User $user
*/
$user = $this->getUser();
$this->getUser();
if ('' === $path) {
throw $this->createNotFoundException("Vous ne pouvez pas uploader de fichier à la racine !");
@ -435,7 +426,7 @@ class FilesController extends AbstractController
$realPath = explode('/', $path);
$parentDir = $this->parentDirectoryRepository->findOneBy(['name' => $realPath[0]]);
if (null === $parentDir || !$this->isGranted('file', $parentDir)) {
if (null === $parentDir || !$this->isGranted('file_write', $parentDir)) {
throw $this->createNotFoundException("Vous n'avez pas le droit d'uploader des fichiers dans ce dossier !");
}
@ -478,6 +469,8 @@ class FilesController extends AbstractController
$path = trim($path, '/');
// On retire les chemins relatifs
$path = str_replace('..', '', $path);
// On retire les . qui sont seul dans la chaîne, en vérifiant qu'il n'y a pas de lettre avant ou après
$path = preg_replace('/(?<!\w)\.(?!\w)/', '', $path);
return str_replace('//', '/', $path);
}

View File

@ -6,6 +6,8 @@ namespace App\Entity;
use App\Enum\RoleEnum;
use App\Repository\ParentDirectoryRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ParentDirectoryRepository::class)]
@ -29,6 +31,17 @@ class ParentDirectory
#[ORM\Column]
private ?bool $isPublic = null;
/**
* @var Collection<int, ParentDirectoryPermission>
*/
#[ORM\OneToMany(targetEntity: ParentDirectoryPermission::class, mappedBy: 'parentDirectory', orphanRemoval: true)]
private Collection $parentDirectoryPermissions;
public function __construct()
{
$this->parentDirectoryPermissions = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
@ -81,4 +94,32 @@ class ParentDirectory
return $this;
}
/**
* @return Collection<int, ParentDirectoryPermission>
*/
public function getParentDirectoryPermissions(): Collection
{
return $this->parentDirectoryPermissions;
}
public function addParentDirectoryPermission(ParentDirectoryPermission $parentDirectoryPermission): static
{
if (!$this->parentDirectoryPermissions->contains($parentDirectoryPermission)) {
$this->parentDirectoryPermissions->add($parentDirectoryPermission);
$parentDirectoryPermission->setParentDirectory($this);
}
return $this;
}
public function removeParentDirectoryPermission(ParentDirectoryPermission $parentDirectoryPermission): static
{
// set the owning side to null (unless already changed)
if ($this->parentDirectoryPermissions->removeElement($parentDirectoryPermission) && $parentDirectoryPermission->getParentDirectory() === $this) {
$parentDirectoryPermission->setParentDirectory(null);
}
return $this;
}
}

View File

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Enum\RoleEnum;
use App\Repository\ParentDirectoryPermissionRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ParentDirectoryPermissionRepository::class)]
class ParentDirectoryPermission
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(enumType: RoleEnum::class)]
private ?RoleEnum $role = null;
#[ORM\Column]
private ?bool $read = null;
#[ORM\Column]
private ?bool $write = null;
#[ORM\ManyToOne(inversedBy: 'parentDirectoryPermissions')]
#[ORM\JoinColumn(nullable: false)]
private ?ParentDirectory $parentDirectory = null;
public function getId(): ?int
{
return $this->id;
}
public function getRole(): ?RoleEnum
{
return $this->role;
}
public function setRole(RoleEnum $role): static
{
$this->role = $role;
return $this;
}
public function isRead(): ?bool
{
return $this->read;
}
public function setRead(bool $read): static
{
$this->read = $read;
return $this;
}
public function isWrite(): ?bool
{
return $this->write;
}
public function setWrite(bool $write): static
{
$this->write = $write;
return $this;
}
public function getParentDirectory(): ?ParentDirectory
{
return $this->parentDirectory;
}
public function setParentDirectory(?ParentDirectory $parentDirectory): static
{
$this->parentDirectory = $parentDirectory;
return $this;
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\ParentDirectoryPermission;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<ParentDirectoryPermission>
*/
class ParentDirectoryPermissionRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, ParentDirectoryPermission::class);
}
// /**
// * @return ParentDirectoryPermission[] Returns an array of ParentDirectoryPermission objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('p')
// ->andWhere('p.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('p.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?ParentDirectoryPermission
// {
// return $this->createQueryBuilder('p')
// ->andWhere('p.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Security\Voter;
use App\Entity\ParentDirectory;
use App\Entity\ParentDirectoryPermission;
use App\Entity\User;
use App\Enum\RoleEnum;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
@ -29,9 +30,23 @@ final class FileVoter extends Voter
return false;
}
$parentDirectoryPermissions = array_filter($realSubject->getParentDirectoryPermissions()->toArray(), static fn (ParentDirectoryPermission $parentDirectoryPermission) => $parentDirectoryPermission->getRole() === $user->getFolderRole());
$parentDirectoryPermission = null;
if ([] !== $parentDirectoryPermissions) {
$parentDirectoryPermission = array_values($parentDirectoryPermissions)[0];
}
$checkNeeded = false;
if (null !== $parentDirectoryPermission) {
$checkNeeded = 'file_read' === $attribute ? $parentDirectoryPermission->isRead() : $parentDirectoryPermission->isWrite();
}
if ($realSubject->getUserCreated() === $user) {
return true;
}
return $realSubject->isPublic() && ($realSubject->getOwnerRole() === $user->getFolderRole() || in_array($user->getFolderRole(), $realSubject->getOwnerRole()->getHigherRoles(), true));
return $realSubject->isPublic() && ($checkNeeded || $realSubject->getOwnerRole() === $user->getFolderRole() || in_array($user->getFolderRole(), $realSubject->getOwnerRole()->getHigherRoles(), true));
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Twig\Extension;
use App\Twig\Runtime\EntityExtensionRuntime;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
class EntityExtension extends AbstractExtension
{
public function getFunctions(): array
{
return [
new TwigFunction('get_parent_dir', [EntityExtensionRuntime::class, 'getParentDir']),
];
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Twig\Runtime;
use App\Entity\ParentDirectory;
use App\Repository\ParentDirectoryRepository;
use Twig\Extension\RuntimeExtensionInterface;
class EntityExtensionRuntime implements RuntimeExtensionInterface
{
public function __construct(
private readonly ParentDirectoryRepository $parentDirectoryRepository
) {
}
public function getParentDir(string $value): ParentDirectory
{
return $this->parentDirectoryRepository->findOneBy(['name' => $value]);
}
}

View File

@ -8,6 +8,7 @@
<div class="mt-4">
{{ include('partials/alerts.html.twig') }}
</div>
{% if parentDir == null or (parentDir != null and is_granted('file_write', parentDir)) %}
<div class="flex justify-end">
<a href="{{ path('app_files_create_directory', {
base: path
@ -18,6 +19,7 @@
}) }}" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">Ajouter des fichiers</a>
{% endif %}
</div>
{% endif %}
<div class="mt-4">
{% include 'partials/breadbrumb.html.twig' %}
</div>
@ -62,6 +64,7 @@
<td class="px-6 py-4 flex gap-2 light:text-black">
{% if file.type == 'file' %}
<a title="Permet de télécharger le fichier" href="{{ file.url }}" class="hover:text-blue-700 duration-300"><twig:ux:icon class="w-6 h-6" name="fa6-solid:download" /></a>
{% if is_granted('file_write', parentDir) %}
<a title="Permet de renommer le fichier" href="{{ path('app_files_rename', {
path: file.path
}) }}" class="hover:text-blue-700 duration-300"><twig:ux:icon class="w-6 h-6" name="fa6-solid:pencil" /></a>
@ -70,13 +73,21 @@
filename: file.path
})
}}" class="hover:text-red-700 duration-300"><twig:ux:icon class="w-6 h-6" name="fa6-solid:trash-can" /></a>
{% endif %}
{% else %}
{% if parentDir == null %}
{% set parentDirectory = get_parent_dir(file.path) %}
{% else %}
{% set parentDirectory = parentDir %}
{% endif %}
{% if is_granted('file_write', parentDirectory) %}
<a href="{{ path('app_files_rename-directory', {
path: file.path
}) }}" class="hover:text-blue-700 duration-300" title="Permet de renommer le dossier"><twig:ux:icon class="w-6 h-6" name="fa6-solid:pencil" /></a>
<a href="{{ path('app_files_delete_directory', {
path: file.path
}) }}" class="hover:text-red-700 duration-300" title="Permet de supprimer le dossier"><twig:ux:icon class="w-6 h-6" name="fa6-solid:trash-can" /></a>
{% endif %}
{% endif %}
</td>
</tr>