diff --git a/composer.json b/composer.json index f2aad5b..7a47ce4 100755 --- a/composer.json +++ b/composer.json @@ -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" diff --git a/composer.lock b/composer.lock index ee043ef..61c30ca 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/migrations/Version20250126120344.php b/migrations/Version20250126120344.php new file mode 100644 index 0000000..904687d --- /dev/null +++ b/migrations/Version20250126120344.php @@ -0,0 +1,32 @@ +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'); + } +} diff --git a/src/Controller/FilesController.php b/src/Controller/FilesController.php index f20b871..1d6a90a 100755 --- a/src/Controller/FilesController.php +++ b/src/Controller/FilesController.php @@ -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('/(? + */ + #[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 + */ + 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; + } } diff --git a/src/Entity/ParentDirectoryPermission.php b/src/Entity/ParentDirectoryPermission.php new file mode 100644 index 0000000..f3269bc --- /dev/null +++ b/src/Entity/ParentDirectoryPermission.php @@ -0,0 +1,84 @@ +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; + } +} diff --git a/src/Repository/ParentDirectoryPermissionRepository.php b/src/Repository/ParentDirectoryPermissionRepository.php new file mode 100644 index 0000000..f92ed37 --- /dev/null +++ b/src/Repository/ParentDirectoryPermissionRepository.php @@ -0,0 +1,45 @@ + + */ +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() + // ; + // } +} diff --git a/src/Security/Voter/FileVoter.php b/src/Security/Voter/FileVoter.php index 01d3b40..6501b39 100644 --- a/src/Security/Voter/FileVoter.php +++ b/src/Security/Voter/FileVoter.php @@ -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)); } } diff --git a/src/Twig/Extension/EntityExtension.php b/src/Twig/Extension/EntityExtension.php new file mode 100644 index 0000000..60c8b81 --- /dev/null +++ b/src/Twig/Extension/EntityExtension.php @@ -0,0 +1,20 @@ +parentDirectoryRepository->findOneBy(['name' => $value]); + } +} diff --git a/templates/files/index.html.twig b/templates/files/index.html.twig index ff24390..353d6da 100755 --- a/templates/files/index.html.twig +++ b/templates/files/index.html.twig @@ -8,6 +8,7 @@
{{ include('partials/alerts.html.twig') }}
+ {% if parentDir == null or (parentDir != null and is_granted('file_write', parentDir)) %}
Ajouter des fichiers {% endif %}
+ {% endif %}
{% include 'partials/breadbrumb.html.twig' %}
@@ -62,6 +64,7 @@ {% if file.type == 'file' %} + {% if is_granted('file_write', parentDir) %} @@ -70,13 +73,21 @@ filename: file.path }) }}" class="hover:text-red-700 duration-300"> + {% endif %} {% else %} + {% if parentDir == null %} + {% set parentDirectory = get_parent_dir(file.path) %} + {% else %} + {% set parentDirectory = parentDir %} + {% endif %} + {% if is_granted('file_write', parentDirectory) %} + {% endif %} {% endif %}