From 5311271ae590460b986e3e09edcdc665b73710ca Mon Sep 17 00:00:00 2001 From: polo Date: Fri, 24 Oct 2025 02:27:19 +0200 Subject: =?UTF-8?q?entit=C3=A9=20pour=20table=20interm=C3=A9diaire=20?= =?UTF-8?q?=C3=A0=203=20champs=20entre=20NodeData=20et=20Asset,=20table=20?= =?UTF-8?q?Asset=20repens=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + public/css/foot.css | 5 ++ public/js/InputFile.js | 15 +++++- public/js/InputText.js | 4 +- public/user_data/assets/favicon48x48.png | Bin 3067 -> 0 bytes public/user_data/assets/fond-piscine.jpg | Bin 24757 -> 0 bytes public/user_data/assets/logo-120x75.jpg | Bin 12240 -> 0 bytes public/user_data/assets/logo-150x94.jpg | Bin 13506 -> 0 bytes public/user_data/assets/logo-nb-et-ffn.png | Bin 61236 -> 0 bytes public/user_data/assets/logo2.jpg | Bin 36899 -> 0 bytes src/controller/HeadFootController.php | 74 +++++++++++++++++++++-------- src/model/Model.php | 11 +++++ src/model/entities/Asset.php | 63 ++++++++++-------------- src/model/entities/Image.php | 3 +- src/model/entities/NodeData.php | 69 +++++++++++++++++++-------- src/model/entities/NodeDataAsset.php | 55 +++++++++++++++++++++ src/view/FooterBuilder.php | 10 ++-- src/view/HeadBuilder.php | 15 ++---- src/view/HeaderBuilder.php | 23 ++++----- src/view/templates/footer.php | 2 +- src/view/templates/head.php | 4 +- src/view/templates/header.php | 2 +- 22 files changed, 241 insertions(+), 115 deletions(-) delete mode 100644 public/user_data/assets/favicon48x48.png delete mode 100644 public/user_data/assets/fond-piscine.jpg delete mode 100644 public/user_data/assets/logo-120x75.jpg delete mode 100644 public/user_data/assets/logo-150x94.jpg delete mode 100644 public/user_data/assets/logo-nb-et-ffn.png delete mode 100644 public/user_data/assets/logo2.jpg create mode 100644 src/model/entities/NodeDataAsset.php diff --git a/.gitignore b/.gitignore index f2c2c53..0d17bbc 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ node_modules/ public/js/tinymce/ public/js/tinymce-langs/ config/config.ini +public/user_data/ public/images/ public/images-mini/ composer.lock diff --git a/public/css/foot.css b/public/css/foot.css index 8a0d08d..38284e3 100644 --- a/public/css/foot.css +++ b/public/css/foot.css @@ -9,6 +9,11 @@ footer /*padding: 0 20px;*/ } +footer a +{ + color: unset; /* ne plus hériter */ + text-decoration: none; +} footer > div { max-width: 1200px; diff --git a/public/js/InputFile.js b/public/js/InputFile.js index f5e450c..e676037 100644 --- a/public/js/InputFile.js +++ b/public/js/InputFile.js @@ -35,7 +35,18 @@ class InputFile{ .then(data => { if(data.success){ this.parent.querySelector('#' + this.name + '_img').src = data.location; - this.close(this.name); + + // cas particulier + if(this.name === 'head_favicon'){ + const link = document.querySelector('link[rel="icon"]'); + link.type = data.mime_type; + link.href = data.location; + } + else if(this.name === 'header_background'){ + document.querySelector('header').style.backgroundImage = "url('" + data.location + "')"; + } + + this.close(); } else{ console.error("Erreur: le serveur n'a pas enregistré l'image'."); @@ -46,6 +57,6 @@ class InputFile{ }); } cancel(){ - this.close(this.name); + this.close(); } } \ No newline at end of file diff --git a/public/js/InputText.js b/public/js/InputText.js index 33dcf8d..79f0398 100644 --- a/public/js/InputText.js +++ b/public/js/InputText.js @@ -31,7 +31,7 @@ class InputText{ .then(data => { if(data.success){ this.parent.querySelector('#' + this.name + '_span').innerHTML = new_text; - this.close(this.name); + this.close(); } else{ console.error("Erreur: le serveur n'a pas enregistré le nouveau texte."); @@ -43,6 +43,6 @@ class InputText{ } cancel(){ this.parent.querySelector('#' + this.name + '_input').value = this.parent.querySelector('#' + this.name + '_span').innerHTML; - this.close(this.name); + this.close(); } } \ No newline at end of file diff --git a/public/user_data/assets/favicon48x48.png b/public/user_data/assets/favicon48x48.png deleted file mode 100644 index 9825db1..0000000 Binary files a/public/user_data/assets/favicon48x48.png and /dev/null differ diff --git a/public/user_data/assets/fond-piscine.jpg b/public/user_data/assets/fond-piscine.jpg deleted file mode 100644 index 239d95d..0000000 Binary files a/public/user_data/assets/fond-piscine.jpg and /dev/null differ diff --git a/public/user_data/assets/logo-120x75.jpg b/public/user_data/assets/logo-120x75.jpg deleted file mode 100644 index b58a7a6..0000000 Binary files a/public/user_data/assets/logo-120x75.jpg and /dev/null differ diff --git a/public/user_data/assets/logo-150x94.jpg b/public/user_data/assets/logo-150x94.jpg deleted file mode 100644 index 67ec6cc..0000000 Binary files a/public/user_data/assets/logo-150x94.jpg and /dev/null differ diff --git a/public/user_data/assets/logo-nb-et-ffn.png b/public/user_data/assets/logo-nb-et-ffn.png deleted file mode 100644 index f51ac9c..0000000 Binary files a/public/user_data/assets/logo-nb-et-ffn.png and /dev/null differ diff --git a/public/user_data/assets/logo2.jpg b/public/user_data/assets/logo2.jpg deleted file mode 100644 index 39c03bd..0000000 Binary files a/public/user_data/assets/logo2.jpg and /dev/null differ diff --git a/src/controller/HeadFootController.php b/src/controller/HeadFootController.php index 0429aac..cf3aed0 100644 --- a/src/controller/HeadFootController.php +++ b/src/controller/HeadFootController.php @@ -3,8 +3,7 @@ declare(strict_types=1); -//use App\Entity\Node; -//use App\Entity\NodeData; +use App\Entity\NodeDataAsset; use App\Entity\Asset; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\EntityManager; @@ -38,41 +37,76 @@ class HeadFootController echo json_encode(['success' => false]); } else{ - $file = $_FILES['file']; - if(!is_dir(Asset::USER_PATH)){ mkdir(Asset::USER_PATH, 0700, true); } + /* -- téléchargement -- */ + $file = $_FILES['file']; $allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'tiff', 'tif', 'ico', 'bmp']; // pas de SVG - $name = Security::secureFileName(pathinfo($file['name'], PATHINFO_FILENAME)); $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); if(!in_array($extension, $allowed_extensions) || $extension === 'jpg'){ $extension = 'jpeg'; } - $file_path = uniqid($name . '_') . '.' . $extension; + $mime_type = mime_content_type($file['tmp_name']); + $hash = hash_file('sha256', $file['tmp_name']); - if(ImageUploadController::imagickCleanImage(file_get_contents($file['tmp_name']), Asset::USER_PATH . $file_path, $extension)){ // recréer l’image pour la nettoyer - $params_array = explode('_', $request_params); // favicon, header_logo, header_background, footer_logo + /* -- instance d'Asset -- */ + $model = new Model($entityManager); + $result = $model->getWhatever('App\Entity\Asset', 'hash', $hash); - $model = new Model($entityManager); - if($model->findWhateverNode('name_node', $params_array[0])){ - $node_data = $model->getNode()->getNodeData(); - $image = new Asset($name, $file_path, mime_content_type($file['tmp_name']), $request_params); - $node_data->addAsset($image); + if(count($result) > 0){ // asset existant trouvé + $asset = $result[0]; - $entityManager->persist($image); - $entityManager->flush(); - echo json_encode(['success' => true, 'location' => Asset::USER_PATH . $file_path]); - } - else{ - echo json_encode(['success' => false, 'message' => 'Erreur noeud non trouvé.']); - } + // correction des informations + $name = $asset->getFileName(); // permet à priori de réécrire par dessus le précédent fichier + //$asset->setFileName($name); + $asset->setMimeType($mime_type); } else{ + $name = Security::secureFileName(pathinfo($file['name'], PATHINFO_FILENAME)); + $name = uniqid($name . '_') . '.' . $extension; + $asset = new Asset($name, $mime_type, $hash); + } + + /* -- écriture du fichier sur le disque -- */ + if(!ImageUploadController::imagickCleanImage(file_get_contents($file['tmp_name']), Asset::USER_PATH . $name, $extension)){ // recréer l’image pour la nettoyer http_response_code(500); echo json_encode(['success' => false, 'message' => 'Erreur image non valide.']); } + else{ + $params_array = explode('_', $request_params); // head_favicon, header_logo, header_background, footer_logo + + /* -- table intermédiaire node_data/asset-- */ + if($model->findWhateverNode('name_node', $params_array[0])){ // noeud (head, header ou footer) + $node_data = $model->getNode()->getNodeData(); + + // recherche à l'aide du rôle + $old_nda = null; + foreach($node_data->getNodeDataAssets() as $nda){ + if($nda->getRole() === $request_params){ + $old_nda = $nda; + $old_nda->setAsset($asset); + break; + } + } + // entrée pas trouvée + if($old_nda === null){ + $new_nda = new NodeDataAsset($node_data, $asset, $request_params); // $request_params sera le rôle de l'asset + $entityManager->persist($new_nda); + } + + if(count($result) === 0){ + $entityManager->persist($asset); + } + $entityManager->flush(); + echo json_encode(['success' => true, 'location' => Asset::USER_PATH . $name, 'mime_type' => $mime_type]); + } + else{ + http_response_code(500); + echo json_encode(['success' => false, 'message' => "Erreur noeud non trouvé, c'est pas du tout normal!"]); + } + } } die; } diff --git a/src/model/Model.php b/src/model/Model.php index 16061e7..68c4c08 100644 --- a/src/model/Model.php +++ b/src/model/Model.php @@ -203,6 +203,17 @@ class Model return true; } } + public function getWhatever(string $class, string $field, string $value): array + { + // penser au entityManager "repository" + $queryBuilder = $this->entityManager->createQueryBuilder(); + $queryBuilder + ->select('n') + ->from($class, 'n') + ->where("n.$field = :value") + ->setParameter('value', $value); + return $queryBuilder->getQuery()->getResult(); + } // récupération d'un article pour modification public function makeArticleNode(string $id = '', bool $get_section = false): bool diff --git a/src/model/entities/Asset.php b/src/model/entities/Asset.php index e359e21..5400db4 100644 --- a/src/model/entities/Asset.php +++ b/src/model/entities/Asset.php @@ -6,6 +6,7 @@ declare(strict_types=1); namespace App\Entity; use Doctrine\ORM\Mapping as ORM; +use Doctrine\Common\Collections\Collection; #[ORM\Entity] #[ORM\Table(name: TABLE_PREFIX . "asset")] @@ -13,71 +14,55 @@ class Asset { const PATH = 'assets/'; const USER_PATH = 'user_data/assets/'; + // choisir un répertoire du genre /var/www/html/uploads/? ou au moins hors de /src? j'en sais rien #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(type: "integer")] private int $id_asset; - #[ORM\Column(type: "string", length: 255, unique: true)] // nom d'image UNIQUE + #[ORM\Column(type: "string", length: 255)] // nom de fichier modifié avec uniqid (fichiers différents de même nom) private string $file_name; - // choisir un répertoire du genre /var/www/html/uploads/, au moins hors de /src - #[ORM\Column(type: "string", length: 255, unique: true, nullable: true)] - private ?string $file_path; - #[ORM\Column(type: "string", length: 255, nullable: true)] private string $mime_type; // image/jpeg, image/png, etc - #[ORM\Column(type: "string", length: 255, nullable: true)] - private string $alt; // texte alternatif - - // autre champs optionnels: file_size, date (default current timestamp) - - /* étapes au téléchargement: - => Validation du type de fichier : On vérifie que le fichier est bien une image en utilisant le type MIME. On peut aussi vérifier la taille du fichier. - => Création d'un répertoire structuré : On génère un chemin dynamique basé sur la date (uploads/2024/12/24/) pour organiser les images. - => Génération d'un nom de fichier unique : On utilise uniqid() pour générer un nom unique et éviter les conflits de nom. - => Déplacement du fichier sur le serveur : Le fichier est déplacé depuis son emplacement temporaire vers le répertoire uploads/. - => Enregistrement dans la base de données : On enregistre les informations de l'image dans la base de données. */ + #[ORM\Column(type: "string", length: 64, unique: true)] // doctrine n'a pas d'équivalent au type CHAR des BDD (on voulait CHAR(64)), c'est pas grave! + private string $hash; // pour détecter deux fichiers identiques, même si leurs noms et les métadonnées changent - #[ORM\ManyToMany(targetEntity: NodeData::class, mappedBy: "assets")] - private $node_data; + #[ORM\OneToMany(mappedBy: 'asset', targetEntity: NodeDataAsset::class)] + private Collection $nda_collection; - public function __construct(string $name, ?string $path, string $mime_type, string $alt) + public function __construct(string $name, string $mime_type, string $hash) { $this->file_name = $name; - $this->file_path = $path; $this->mime_type = $mime_type; - $this->alt = $alt; + $this->hash = $hash; } public function getFileName(): string { return $this->file_name; } - public function getFilePath(): string + public function setFileName(string $name): void { - return $this->file_path; + $this->file_name = $name; } - public function getAlt(): string + public function getMimeType(): string { - return $this->alt; + return $this->mime_type; } - - - // pour ViewBuilderController? - /*public function displayImage($imageId): void + public function setMimeType(string $mime_type): void { - //$imageId = 1; // Exemple d'ID d'image - $stmt = $pdo->prepare("SELECT file_path FROM images WHERE id = ?"); - $stmt->execute([$imageId]); - $image = $stmt->fetch(); + $this->mime_type = $mime_type; + } + public function getHash(): string + { + return $this->hash; + } - if ($image) { - echo "Image"; - } else { - echo "Image non trouvée."; - } - }*/ + public function getNodeDataAssets(): Collection + { + return $this->nda_collection; + } } diff --git a/src/model/entities/Image.php b/src/model/entities/Image.php index e867a5f..2ee379a 100644 --- a/src/model/entities/Image.php +++ b/src/model/entities/Image.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Entity; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] @@ -42,7 +43,7 @@ class Image => Enregistrement dans la base de données : On enregistre les informations de l'image dans la base de données. */ #[ORM\ManyToMany(targetEntity: Article::class, mappedBy: "images")] - private $article; + private Collection $article; public function __construct(string $name, ?string $path, ?string $path_mini, string $mime_type, string $alt) { diff --git a/src/model/entities/NodeData.php b/src/model/entities/NodeData.php index 9db866e..47e7208 100644 --- a/src/model/entities/NodeData.php +++ b/src/model/entities/NodeData.php @@ -39,21 +39,16 @@ class NodeData #[ORM\Column(type: "integer", nullable: true)] private ?int $pagination_limit = null; // pour les post_block et news_block - #[ORM\ManyToMany(targetEntity: Asset::class, inversedBy: "node_data")] // cascade: ['remove'] = très dangereux! - #[ORM\JoinTable( - name: TABLE_PREFIX . "nodedata_asset", - joinColumns: [new ORM\JoinColumn(name: "node_data_id", referencedColumnName: "id_node_data", onDelete: "CASCADE")], // onDelete: "CASCADE": très utile - inverseJoinColumns: [new ORM\JoinColumn(name: "asset_id", referencedColumnName: "id_asset", onDelete: "CASCADE")] - )] - private Collection $assets; + #[ORM\OneToMany(mappedBy: 'node_data', targetEntity: NodeDataAsset::class, cascade: ['persist', 'remove'])] + private Collection $nda_collection; private int $nb_pages = 1; - public function __construct(array $data, Node $node, Collection $assets = new ArrayCollection, ?string $presentation = null, ?bool $chrono_order = null) + public function __construct(array $data, Node $node, Collection $nda_collection = new ArrayCollection, ?string $presentation = null, ?bool $chrono_order = null) { $this->data = $data; $this->node = $node; - $this->assets = $assets; + $this->nda_collection = $nda_collection; if(!empty($presentation) && $presentation === 'grid'){ $this->grid_cols_min_width = 250; } @@ -132,22 +127,54 @@ class NodeData { $this->node = $node; }*/ - public function getAssets(): Collection + + + public function getNodeDataAssets(): Collection { - return $this->assets; + return $this->nda_collection; } - public function addAsset(Asset $asset): void + public function getNodeDataAssetByRole(string $role): ?NodeDataAsset { - if(!$this->assets->contains($asset)){ - $this->assets->add($asset); - //$asset->addNodeData($this); // autre sens + foreach($this->nda_collection as $nda){ + if($nda->getRole() === $role){ + return $nda; + } } + return null; } - public function removeAsset(Asset $asset): void + public function getAssetByRole(string $role): ?Asset { - $this->assets->removeElement($asset); - /*if($this->assets->removeElement($asset)){ // autre sens - $asset->removeNodeData($this); - }*/ + $nda = $this->getNodeDataAssetByRole($role); + if($nda === null){ + return null; + } + return $nda->getAsset() ?? null; } -} + /*public function addNodeDataAsset(NodeDataAsset $nda): self + { + if(!$this->nda_collection->contains($nda)){ // sécurité contrainte UNIQUE + $this->nda_collection->add($nda); + } + return $this; + }*/ + /*public function removeNodeDataAsset(NodeDataAsset $nda): self // inutile on peut faire: $node_data->getNodeDataAssets()->removeElement($nda); + { + $this->nda_collection->removeElement($nda); + // pas de synchro dans NodeDataAsset, les champs de cette table ne sont pas nullables + return $this; + }*/ + + // LE setter, sélectionne l'asset à utiliser en remplaçant l'entrée dans NodeDataAsset en fonction du rôle + // à mettre théoriquement dans une classe metier dans "service" + /*public function replaceAssetForRole(string $role, Asset $asset): void + { + foreach($this->nda_collection as $nda){ + if($nda->getRole() === $role){ + $this->removeNodeDataAsset($nda); + break; + } + } + $this->new_nda = new NodeDataAsset($this, $asset, $role); + $this->addNodeDataAsset($this->new_nda); + }*/ +} \ No newline at end of file diff --git a/src/model/entities/NodeDataAsset.php b/src/model/entities/NodeDataAsset.php new file mode 100644 index 0000000..7f7008e --- /dev/null +++ b/src/model/entities/NodeDataAsset.php @@ -0,0 +1,55 @@ +node_data = $node_data; + $this->asset = $asset; + $this->role = $role; + } + + /*public function getNodeData(): NodeData + { + return $this->node_data; + }*/ + public function getAsset(): Asset + { + return $this->asset; + } + public function setAsset(Asset $asset): self + { + $this->asset = $asset; + return $this; + } + public function getRole(): string + { + return $this->role; + } +} diff --git a/src/view/FooterBuilder.php b/src/view/FooterBuilder.php index 35df010..fcb78e0 100644 --- a/src/view/FooterBuilder.php +++ b/src/view/FooterBuilder.php @@ -4,6 +4,7 @@ declare(strict_types=1); use App\Entity\Node; +use App\Entity\Asset; class FooterBuilder extends AbstractBuilder { @@ -13,12 +14,15 @@ class FooterBuilder extends AbstractBuilder if(file_exists($viewFile)) { - // $adresses postale et e-mail - if(!empty($node->getNodeData()->getData())) + $node_data = $node->getNodeData(); + // nom du contact, adresse et e-mail + if(!empty($node_data->getData())) { - extract($node->getNodeData()->getData()); + extract($node_data->getData()); } + $footer_logo = Asset::USER_PATH . $node_data->getAssetByRole('footer_logo')?->getFileName() ?? ''; + $this->useChildrenBuilder($node); $breadcrumb = $this->html; diff --git a/src/view/HeadBuilder.php b/src/view/HeadBuilder.php index b3d78aa..fd7f751 100644 --- a/src/view/HeadBuilder.php +++ b/src/view/HeadBuilder.php @@ -49,18 +49,9 @@ class HeadBuilder extends AbstractBuilder $description = Model::$page_path->getLast()->getDescription(); // favicon - /*foreach($node->getNodeData()->getImages() as $image) - { - if(str_contains($image->getFileName(), 'favicon')) - { - $favicon = rtrim($image->getFilePathMini(), '/'); - $alt = $image->getAlt(); - } - }*/ - - // en dur temporairement - $favicon = Asset::USER_PATH . 'favicon48x48.png'; - $alt = 'favicon'; + // ?-> est l'opérateur de navigation sécurisée => LOVE! + $favicon = Asset::USER_PATH . ($favicon_object = $node->getNodeData()->getAssetByRole('head_favicon'))?->getFileName() ?? ''; + $favicon_type = $favicon_object?->getMimeType() ?? ''; ob_start(); require $viewFile; diff --git a/src/view/HeaderBuilder.php b/src/view/HeaderBuilder.php index 3b45a11..44e244d 100644 --- a/src/view/HeaderBuilder.php +++ b/src/view/HeaderBuilder.php @@ -3,8 +3,8 @@ declare(strict_types=1); -use App\Entity\Asset; use App\Entity\Node; +use App\Entity\Asset; class HeaderBuilder extends AbstractBuilder { @@ -35,18 +35,18 @@ class HeaderBuilder extends AbstractBuilder if(file_exists($viewFile)) { + $node_data = $node->getNodeData(); // titre et description - // => retourne $titre, $description et le tableau associatif: $social - if(!empty($node->getNodeData()->getData())) + if(!empty($node_data->getData())) { - extract($node->getNodeData()->getData()); + extract($node_data->getData()); } // réseaux sociaux + logo dans l'entête + $header_logo = Asset::USER_PATH . $node_data->getAssetByRole('header_logo')?->getFileName() ?? ''; + $header_background = Asset::USER_PATH . $node_data->getAssetByRole('header_background')?->getFileName() ?? ''; $keys = array_keys($social); $social_networks = ''; - //$header_logo; - //$header_background; // nécéssite des entrées dans la table node_asset /*foreach($node->getNodeData()->getAssets() as $asset) @@ -80,20 +80,21 @@ class HeaderBuilder extends AbstractBuilder // boutons mode admin if($_SESSION['admin']){ + // assets dans classe editing_zone $editing_zone_margin = '5px'; - $favicon = Asset::USER_PATH . 'favicon48x48.png'; // double le code dans HeadBuilder - $buttons_favicon = ' + $buttons_favicon = ' + '; - $background = Asset::USER_PATH . 'fond-piscine.jpg'; - $buttons_background = ' + $buttons_background = ' '; + // asset dans classe header_content $buttons_header_logo = ' '; - + // texte dans classe header_content $buttons_header_title = ' '; diff --git a/src/view/templates/footer.php b/src/view/templates/footer.php index 2bb5a9e..4db38ae 100644 --- a/src/view/templates/footer.php +++ b/src/view/templates/footer.php @@ -20,7 +20,7 @@ diff --git a/src/view/templates/head.php b/src/view/templates/head.php index 83a0e7c..bce0a19 100644 --- a/src/view/templates/head.php +++ b/src/view/templates/head.php @@ -2,12 +2,12 @@ + <?= $title ?> - + - \ No newline at end of file diff --git a/src/view/templates/header.php b/src/view/templates/header.php index b63aa84..ac8af7f 100644 --- a/src/view/templates/header.php +++ b/src/view/templates/header.php @@ -18,7 +18,7 @@
-- cgit v1.2.3