summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorpolo-pc-greta <ordipolo@gmx.fr>2025-03-27 10:13:03 +0100
committerpolo-pc-greta <ordipolo@gmx.fr>2025-03-27 10:13:03 +0100
commitdf3612ed7e6691530503f79483d2fdbc032d01b8 (patch)
tree56d1c68fdc8625f5dad1937a654299d45142c79a /src
downloadcms-df3612ed7e6691530503f79483d2fdbc032d01b8.zip
mise en ligne github
Diffstat (limited to 'src')
-rw-r--r--src/Config.php76
-rw-r--r--src/controller/Director.php101
-rw-r--r--src/controller/Security.php111
-rw-r--r--src/controller/URL.php88
-rw-r--r--src/controller/ajax.php104
-rw-r--r--src/controller/installation.php144
-rw-r--r--src/controller/password.php357
-rw-r--r--src/controller/post.php17
-rw-r--r--src/model/Menu.php53
-rw-r--r--src/model/Path.php84
-rw-r--r--src/model/doctrine-bootstrap.php31
-rw-r--r--src/model/entities/Article.php77
-rw-r--r--src/model/entities/Image.php91
-rw-r--r--src/model/entities/Node.php168
-rw-r--r--src/model/entities/NodeData.php62
-rw-r--r--src/model/entities/Page.php97
-rw-r--r--src/model/entities/User.php47
-rw-r--r--src/view/AbstractBuilder.php52
-rw-r--r--src/view/ArticleBuilder.php60
-rw-r--r--src/view/BlogBuilder.php49
-rw-r--r--src/view/BreadcrumbBuilder.php53
-rw-r--r--src/view/FooterBuilder.php64
-rw-r--r--src/view/GaleryBuilder.php49
-rw-r--r--src/view/GridBuilder.php55
-rw-r--r--src/view/HeadBuilder.php68
-rw-r--r--src/view/HeaderBuilder.php64
-rw-r--r--src/view/LoginBuilder.php15
-rw-r--r--src/view/MainBuilder.php30
-rw-r--r--src/view/NavBuilder.php61
-rw-r--r--src/view/NewBuilder.php93
-rw-r--r--src/view/ViewBuilder.php16
-rw-r--r--src/view/password.php152
-rw-r--r--src/view/templates/article.php15
-rw-r--r--src/view/templates/blog.php6
-rw-r--r--src/view/templates/footer.php15
-rw-r--r--src/view/templates/galery.php10
-rw-r--r--src/view/templates/grid.php8
-rw-r--r--src/view/templates/head.php11
-rw-r--r--src/view/templates/header.php23
-rw-r--r--src/view/templates/new.php21
40 files changed, 2698 insertions, 0 deletions
diff --git a/src/Config.php b/src/Config.php
new file mode 100644
index 0000000..cfec876
--- /dev/null
+++ b/src/Config.php
@@ -0,0 +1,76 @@
1<?php
2// src/controller/Config.php
3
4declare(strict_types=1);
5
6class Config
7{
8 // BDD
9 static public string $db_host = 'localhost';
10 static public string $database = 'nageurs';
11 static public string $db_driver = 'pdo_mysql';
12 static public string $user = 'root';
13 static public string $password = '';
14 static public string $table_prefix = '';
15
16 // classe URL
17 static public string $protocol = 'http';
18 static public string $host = 'nageurs.localhost';
19 static public string $port = '80';
20
21 // copier dans ce tableau les variables contenant des chemins
22 static private array $path_vars = [];
23
24 static public function load(string $file_path): void
25 {
26 if(file_exists($file_path))
27 {
28 // ce serait bien de gérer aussi les fichiers corrompus?
29 $raw_data = parse_ini_file($file_path);
30 self::hydrate($raw_data);
31 }
32 else
33 {
34 echo "le fichier config.ini n'existe pas ou n'est pas lisible";
35 }
36 define('TABLE_PREFIX', self::$table_prefix);
37 }
38
39 // renseigner les variables internes de Config
40 static private function hydrate(array $raw_data): void
41 {
42 foreach($raw_data as $field => $value)
43 {
44 if($value != '') // valeur par défaut
45 {
46 if(isset(self::$$field)) // le champ existe dans Config
47 {
48 // problème du slash à la fin du nom d'un dossier
49 $value = self::slashAtEndOfPath($field, $value);
50 self::$$field = $value;
51 }
52 else
53 {
54 echo "debug: le fichier config.ini comporte une erreur, le champ: " . $field . " est incorrect,\nl'information contenue sur cette ligne ne sera pas utilisée\n";
55 }
56 }
57 /*else
58 {
59 echo "debug: le champ " . $field . " est vide, la valeur par défaut " . self::$$field . " sera utilisée.\n";
60 }*/
61 }
62 }
63
64
65 // pour que les chemins finissent toujours par un /
66 static private function slashAtEndOfPath(string $field, string $value): string
67 {
68 foreach(self::$path_vars as $item)
69 {
70 if($field === $item){
71 return !str_ends_with($value, '/') ? $value . '/' : $value;
72 }
73 }
74 return $value;
75 }
76}
diff --git a/src/controller/Director.php b/src/controller/Director.php
new file mode 100644
index 0000000..896cde1
--- /dev/null
+++ b/src/controller/Director.php
@@ -0,0 +1,101 @@
1<?php
2// src/controller/Director.php
3
4declare(strict_types=1);
5
6use Doctrine\ORM\EntityManager;
7use App\Entity\Page;
8use App\Entity\Node;
9
10class Director
11{
12 private EntityManager $entityManager;
13 static public Menu $menu_data; // pour NavBuilder
14 static public Path $page_path; // pour BreadcrumbBuilder
15 private Page $page;
16 private Node $root_node;
17
18 public function __construct(EntityManager $entityManager)
19 {
20 $this->entityManager = $entityManager;
21 self::$menu_data = new Menu($entityManager); // Menu est un modèle mais pas une entité
22 self::$page_path = new Path();
23 $this->page = self::$page_path->getLast();
24 $this->root_node = new Node; // instance mère "vide" ne possédant rien d'autre que des enfants
25 }
26
27 public function makeRootNode(string $id = ''): void
28 {
29 // on récupère toutes les entrées
30 $dql = 'SELECT n FROM App\Entity\Node n WHERE n.page = :page OR n.page IS null';
31 if($id == '')
32 {
33 $bulk_data = $this->entityManager
34 ->createQuery($dql)
35 ->setParameter('page', $this->page)
36 ->getResult();
37 }
38 else // avec $_GET['id'] dans l'URL
39 {
40 $dql .= ' OR n.article_timestamp = :id';
41 $bulk_data = $this->entityManager
42 ->createQuery($dql)
43 ->setParameter('page', $this->page)
44 ->setParameter('id', $id)
45 ->getResult();
46 }
47 $this->feedObjects($bulk_data);
48 }
49
50 public function makeArticleNode(string $id = ''): bool
51 {
52 $bulk_data = $this->entityManager
53 ->createQuery('SELECT n FROM App\Entity\Node n WHERE n.article_timestamp = :id')
54 ->setParameter('id', $id)
55 ->getResult();
56
57 if(count($bulk_data) === 0){
58 return false;
59 }
60
61 $this->root_node = $bulk_data[0];
62 return true;
63 }
64
65 public function feedObjects(array $bulk_data): void // $bulk_data = tableau de Node
66 {
67 // puis on les range
68 // (attention, risque de disfonctionnement si les noeuds de 1er niveau ne sont pas récupérés en 1er dans la BDD)
69 foreach($bulk_data as $node)
70 {
71 // premier niveau
72 if($node->getParent() == null)
73 {
74 $this->root_node->addChild($node);
75
76 // spécifique page article
77 if($node->getName() === 'main' && $this->page->getEndOfPath() == 'article'){
78 $main = $node;
79 }
80 }
81 // autres niveaux
82 else
83 {
84 $node->getParent()->addChild($node);
85
86 // spécifique page article
87 if($node->getName() === 'new' && $this->page->getEndOfPath() == 'article'){
88 $new = $node;
89 }
90 }
91 }
92 if(isset($new)){
93 $main->setTempChild($new);
94 }
95 }
96
97 public function getRootNode(): Node
98 {
99 return $this->root_node;
100 }
101}
diff --git a/src/controller/Security.php b/src/controller/Security.php
new file mode 100644
index 0000000..ab59d07
--- /dev/null
+++ b/src/controller/Security.php
@@ -0,0 +1,111 @@
1<?php
2// src/controller/Security.php
3//
4// utilise htmlawed contre les faille XSS
5
6class Security
7{
8 private static $configHtmLawed = array(
9 'safe'=>1, // protection contre les élements et attributs dangereux
10
11 // liste blanche d'éléments HTML
12 'elements'=> 'h1, h2, h3, h4, h5, h6, p, s, em, span, strong, a, ul, ol, li, sup, sub, code, blockquote, div, pre, table, caption, colgroup, col, tbody, tr, th, td, figure, img, figcaption',
13
14 // liste noire d'attributs HTML
15 'deny_attribute'=> 'id, class' // on garde 'style'
16 );
17
18 // faire qu'un certain élément puisse n'avoir que certains attributs, regarder la doc
19 private static $specHtmLawed = '';
20
21 public static function secureString(string $chaine): string
22 {
23 return trim(htmLawed($chaine, self::$configHtmLawed, self::$specHtmLawed));;
24 }
25
26 public static function secureFileName(string $chaine): string
27 {
28 // sécuriser un nom avec chemin avec basename?
29 //$chaine = basename($chaine);
30
31 /*
32 - caractères interdits sous windows / \ : * ? " < > |
33 - mac autorise les /
34 - mac interdit :
35 - linux autorise tout sauf les /
36 - imagemagick ne supporte pas les :
37
38 - 'espace' fonctionne
39 - / remplacé par firefox en :
40 - \ retire ce qui est devant le \
41 - * fonctionne
42 - ? permet le téléchargement mais pas l'affichage
43 - " ne fonctionne pas, remplacé par %22, filtrer %22
44 - < > fonctionnent
45 - | fonctionne
46 - = fonctionne, mais je filtre parce qu'on en trouve dans une URL
47 - ' ` fonctionnent
48 - % fonctionne
49 - (){}[] fonctionnent
50 - ^ fonctionne
51 - # ne fonctionne pas
52 - ~ fonctionne
53 - & fonctionne
54 - ^ pas encore testé
55 */
56
57 // => on remplace tout par des _
58 // filtrer / et \ semble inutile
59
60 $cibles = [' ', '/', '\\', ':', '*', '?', '<', '>', '|', '=', "'", '`', '"', '%22', '#'];
61 $chaine = str_replace($cibles, '_', $chaine); // nécéssite l'extension mbstring
62 $chaine = mb_strtolower($chaine);
63 return($chaine);
64
65 // les problèmes avec \ persistent !!
66 // => javascript
67 // malheureusement document.getElementById('upload').files[0].name = chaine; ne marche pas! interdit!
68 // javascript ne doit pas pouvoir accéder au système de fichiers
69 // solutions:
70 // - au lieu de fournir une chaine (le chemin du fichier), donner un objet à files[0].name
71 // - créer une copie du fichier et l'envoyer à la place
72 // - envoyer le fichier en AJAX
73 // - envoyer le nom du fichier à part puis renommer en PHP
74 }
75}
76
77// erreurs à la création des mots de passe
78function removeSpacesTabsCRLF(string $chaine): string
79{
80 $cibles = [' ', "\t", "\n", "\r"]; // doubles quotes !!
81 return(str_replace($cibles, '', $chaine));
82}
83
84// lien sans http://
85function fixLinks($data)
86{
87 // 1/
88 // si une adresse est de type "domaine.fr" sans le http:// devant, le comportement des navigateurs est de rechercher un fichier comme si mon adresse commençait par file://
89 // tomber ainsi sur une page d'erreur est parfaitement déroutant
90
91 // regex pour détecter les balises <a> et ajouter http:// au début des liens si nécessaire
92 $pattern = '#(<a[^>]+href=")((?!https?://)[^>]+>)#';
93 //$data = preg_replace($pattern, '$1http://$2', $data);
94
95 // 2/
96 // cas où la regex fait mal son boulot:
97 // l'erreur 404 est gérée par le .htaccess
98 // et le visiteur est redirigé à la page "menu"
99 // (ça ne règle pas le problème mais c'est mieux)
100
101 // 3/
102 // quand l'éditeur est ouvert (avant de valider l'article),
103 // le lien qu'on vient de créer apparaît dans l'infobulle,
104 // cliquer dessus ouvre un onglet sur une erreur 404
105 // solution partielle avec le .htaccess
106 //
107 // solution? fermer ce nouvel onglet avec echo '<SCRIPT>javascript:window.close()</SCRIPT>';
108 // comment déclencher le JS? en faisant qu'une erreur 404 causée pour cette raison soit particulière?
109
110 return($data);
111}
diff --git a/src/controller/URL.php b/src/controller/URL.php
new file mode 100644
index 0000000..956d85d
--- /dev/null
+++ b/src/controller/URL.php
@@ -0,0 +1,88 @@
1<?php
2// src/controller/URL.php
3
4declare(strict_types=1);
5
6class URL implements Stringable
7{
8 static private string $protocol = 'http://';
9 static private string $host = 'localhost';
10 static private string $port;
11 static private string $path = '/index.php';
12 private array $params;
13 private string $anchor = '';
14
15 // setters statiques
16 static public function setProtocol(string $protocol = 'http'): void
17 {
18 self::$protocol = $protocol === 'https' ? 'https://' : 'http://';
19 }
20 static public function setPort(int|string $port = 80): void
21 {
22 if((int)$port === 443){
23 self::$protocol = 'https://';
24 self::$port = '';
25 }
26 elseif((int)$port === 80){
27 self::$protocol = 'http://';
28 self::$port = '';
29 }
30 else{
31 self::$port = ':' . (string)$port;
32 }
33 }
34 static public function setHost(string $host): void
35 {
36 self::$host = $host;
37 }
38 static public function setPath(string $path): void
39 {
40 self::$path = '/' . ltrim($path, '/');
41 }
42
43 public function __construct(array $gets = [], string $anchor = ''){
44 $this->params = $gets;
45 if($anchor != ''){
46 $this->setAnchor($anchor);
47 }
48 }
49
50 //setters normaux
51 public function addParams(array $gets): void
52 {
53 // array_merge est préféré à l'opérateur d'union +, si une clé existe déjà la valeur est écrasée
54 $this->params = array_merge($this->params, $gets);
55 }
56 public function setAnchor(string $anchor = ''): void
57 {
58 if($anchor != ''){
59 $this->anchor = '#' . ltrim($anchor, '#');
60 }
61 else{
62 $this->anchor = '';
63 }
64 }
65
66 private function makeParams(): string
67 {
68 $output = '';
69 $first = true;
70
71 foreach($this->params as $key => $value) {
72 if($first){
73 $output .= '?';
74 $first = false;
75 }
76 else{
77 $output .= '&';
78 }
79 $output .= $key . '=' . $value;
80 }
81 return $output;
82 }
83
84 public function __toString(): string
85 {
86 return self::$protocol . self::$host . self::$port . self::$path . $this->makeParams() . $this->anchor;
87 }
88} \ No newline at end of file
diff --git a/src/controller/ajax.php b/src/controller/ajax.php
new file mode 100644
index 0000000..130c4c6
--- /dev/null
+++ b/src/controller/ajax.php
@@ -0,0 +1,104 @@
1<?php
2// src/controller/ajax.php
3
4declare(strict_types=1);
5
6// détection des requêtes de tinymce
7if($_SERVER['CONTENT_TYPE'] === 'application/json' && isset($_GET['action']))
8{
9 // récupération des données
10 $data = file_get_contents('php://input');
11 $json = json_decode($data, true);
12
13 if($_GET['action'] === 'editor_submit' && isset($json['id']) && isset($json['content']))
14 {
15 if(json_last_error() === JSON_ERROR_NONE)
16 {
17 $articleId = $json['id'];
18 $content = Security::secureString($json['content']);
19
20 $director = new Director($entityManager);
21 if($director->makeArticleNode($articleId)) // une entrée est trouvée
22 {
23 $node = $director->getRootNode();
24 $node->getArticle()->setContent($content);
25 $entityManager->flush();
26
27 echo json_encode(['success' => true]);
28 }
29 else{
30 echo json_encode(['success' => false, 'message' => 'Aucune entrée trouvée en BDD']);
31 }
32 }
33 else{
34 echo json_encode(['success' => false, 'message' => 'Erreur de décodage JSON']);
35 }
36 die;
37 }
38 elseif($_GET['action'] === 'delete_article' && isset($json['id']))
39 {
40 $articleId = $json['id'];
41
42 $director = new Director($entityManager);
43 $director->makeArticleNode($articleId);
44 $node = $director->getRootNode();
45 $entityManager->remove($node);
46 $entityManager->flush();
47
48 // test avec une nouvelle requête qui ne devrait rien trouver
49 if(!$director->makeArticleNode($articleId))
50 {
51 echo json_encode(['success' => true]);
52
53 // on pourrait afficher une notification "toast"
54 }
55 else{
56 http_response_code(500);
57 echo json_encode(['success' => false, 'message' => 'Erreur lors de la suppression de l\'article.']);
58 }
59 die;
60 }
61}
62
63// détection des requêtes d'upload d'image de tinymce
64if(strpos($_SERVER['CONTENT_TYPE'], 'multipart/form-data') !== false && isset($_GET['action']) && $_GET['action'] === 'upload_image'){
65 if (isset($_FILES['file'])) {
66 $file = $_FILES['file'];
67 $dest = 'images/';
68 $dest_mini = 'images-mini/';
69
70 // Vérifier si les répertoires existent, sinon les créer
71 if(!is_dir($dest)) {
72 mkdir($dest, 0700, true);
73 }
74 if(!is_dir($dest_mini)) {
75 mkdir($dest_mini, 0700, true);
76 }
77
78 $filePath = $dest . basename($file['name']);
79
80 // créer une miniature de l'image
81
82 if(move_uploaded_file($file['tmp_name'], $filePath)) {
83 $image_url = str_replace(basename($_SERVER['SCRIPT_NAME']), '', $_SERVER['SCRIPT_NAME']);
84 echo json_encode(['location' => $image_url . $filePath]); // renvoyer l'URL de l'image téléchargée
85 }
86 else{
87 http_response_code(500);
88 echo json_encode(['message' => 'Erreur 500: Internal Server Error']);
89 }
90 }
91 else{
92 http_response_code(400);
93 echo json_encode(['message' => 'Erreur 400: Bad Request']);
94 }
95 die;
96}
97
98// détection des requêtes de type XHR, pas d'utilité pour l'instant
99/*elseif(isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'){
100 echo "requête XHR reçue par le serveur";
101 die;
102}*/
103
104
diff --git a/src/controller/installation.php b/src/controller/installation.php
new file mode 100644
index 0000000..a692618
--- /dev/null
+++ b/src/controller/installation.php
@@ -0,0 +1,144 @@
1<?php
2// src/controller/installation.php
3
4declare(strict_types=1);
5
6use App\Entity\Page;
7use App\Entity\Node;
8use App\Entity\NodeData;
9use App\Entity\Image;
10use Doctrine\Common\Collections\ArrayCollection;
11use Doctrine\ORM\EntityManager;
12
13function installation(): void
14{
15 /* -- extensions PHP -- */
16 $extensions = [];
17 foreach($extensions as $extension){
18 if(!extension_loaded($extension))
19 {
20 echo("l'extension " . $extension . ' est manquante<br>');
21 }
22 }
23 if(!extension_loaded('imagick') && !extension_loaded('gd')){
24 echo("il manque une de ces extensions au choix: imagick (de préférence) ou gd<br>");
25 }
26
27 /* -- droits des fichiers et dossiers -- */
28 $droits_dossiers = 0700;
29 $droits_fichiers = 0600;
30
31 // accès interdit en HTTP
32 if(!file_exists('../config/.htaccess')){
33 $contenu = <<< HTACCESS
34<Files "config.ini">
35 Order Allow,Deny
36 Deny from all
37</Files>
38HTACCESS;
39
40 $fichier = fopen('../config/.htaccess', 'w');
41 fputs($fichier, $contenu);
42 fclose($fichier);
43 chmod('../config/.htaccess', $droits_fichiers);
44 //echo("danger<br>pas de .htaccess dans config<br>prévenez le respondable du site");
45 //die;
46 }
47
48 // accès limité en local (600) pour config.ini
49 if(substr(sprintf('%o', fileperms('../config/config.ini')), -4) != 600){
50 chmod('../config/config.ini', $droits_fichiers);
51 }
52
53 // création de data et sous-dossiers
54 if(!file_exists('../data')){
55 mkdir('../data/');
56 chmod('../data/', $droits_dossiers);
57 }
58 if(!touch('../data')){
59 echo("dossier data non autorisé en écriture");
60 die;
61 }
62 $sous_dossiers = array('images', 'images-mini', 'videos');
63 foreach ($sous_dossiers as $sous_dossier){
64 if(!file_exists('../data/' . $sous_dossier)){
65 mkdir('../data/' . $sous_dossier);
66 chmod('../data/' . $sous_dossier, $droits_dossiers);
67 }
68 if(!touch('../data/' . $sous_dossier)){
69 echo("dossier data non autorisé en écriture");
70 die;
71 }
72 }
73}
74
75// création de la page d'accueil à la toute 1ère visite du site
76// les informations ici ne sont pas demandées à l'utilisateur pour l'instant (on verra ça plus tard)
77function makeStartPage(EntityManager $entityManager){
78 /* -- table page -- */
79 // paramètres: name_page, end_of_path, reachable, in_menu, parent
80 $accueil = new Page('Accueil', 'accueil', true, true, NULL);
81 $connection = new Page('Connexion', 'connexion', true, false, NULL);
82 $article = new Page('Article', 'article', true, false, NULL);
83 $edit_page = new Page("Modification d'une page", 'modif_page', true, false, NULL);
84 $new_page = new Page('Nouvelle page', 'nouvelle_page', true, false, NULL);
85 $edit_paths = new Page("Menu et chemins", 'menu_chemins', true, false, NULL);
86
87 /* -- table node -- */
88 // paramètres: name_node, article_timestamp, attributes, position, parent, page, article
89 $head_accueil = new Node('head', NULL, ['css_array' => ['body', 'head', 'nav', 'main', 'foot'], 'js_array' => ['main']], 1, NULL, $accueil, NULL);
90 $header = new Node('header', NULL, [], 2, NULL, NULL, NULL);
91 $nav = new Node('nav', NULL, [], 1, $header, NULL, NULL);
92 $main = new Node('main', NULL, [], 3, NULL, NULL, NULL);
93 $footer = new Node('footer', NULL, [], 4, NULL, NULL, NULL);
94 $breadcrumb = new Node('breadcrumb', NULL, [], 1, $footer, NULL, NULL);
95 $head_login = new Node('head', NULL, ["stop" => true, 'css_array' => ['body', 'head', 'nav', 'main'], 'js_array' => ['main']], 1, NULL, $connection, NULL);
96 $login = new Node('login', NULL, [], 1, $main, $connection, NULL);
97 $head_article = new Node('head', NULL, ['css_array' => ['body', 'head', 'nav', 'main', 'foot'], 'js_array' => ['main']], 1, NULL, $article, NULL);
98
99 /* -- table image -- */
100 // paramètres: file_name, file_path, file_path_mini, mime_type, alt
101 $favicon = new Image("favicon48x48.png", NULL, "assets/favicon48x48.png", "image/png", "favicon");
102 $logo = new Image("logo-120x75.jpg", NULL, "assets/logo-120x75.jpg", "image/png", "head_logo");
103 $facebook = new Image("facebook.svg", NULL, "assets/facebook.svg", "image/svg+xml", "facebook");
104 $instagram = new Image("instagram.svg", NULL, "assets/instagram.svg", "image/svg+xml", "instagram");
105 $fond_piscine = new Image("fond-piscine.jpg", "assets/fond-piscine.jpg", NULL, "images/jpg", "fond-piscine");
106
107 /* -- table node_data -- */
108 // paramètres: data, node
109 $head_accueil_data = new NodeData(["description" => "Club, École de natation et Perfectionnement", "title" => "Les Nageurs Bigoudens"], $head_accueil, new ArrayCollection([$favicon]));
110 $header_data = new NodeData(["description" => "Club, École de natation et Perfectionnement", "title" => "Les Nageurs Bigoudens", "facebook_link" => "https://www.facebook.com/nageursbigoudens29120", "instagram_link" => "https://www.instagram.com/nageursbigoudens/"], $header, new ArrayCollection([$logo, $facebook, $instagram, $fond_piscine]));
111 $footer_data = new NodeData(["adresse" => "17, rue Raymonde Folgoas Guillou, 29120 Pont-l’Abbé", "contact_nom" => "Les Nageurs Bigoudens", "e_mail" => "nb.secretariat@orange.fr"], $footer);
112 $head_login_data = new NodeData(["description" => "Connexion", "title" => "Connexion"], $head_login, new ArrayCollection([$favicon]));
113 $head_article_data = new NodeData(["description" => "", "title" => ""], $head_article, new ArrayCollection([$favicon]));
114
115 $entityManager->persist($accueil);
116 $entityManager->persist($connection);
117 $entityManager->persist($article);
118 $entityManager->persist($edit_page);
119 $entityManager->persist($new_page);
120 $entityManager->persist($edit_paths);
121 $entityManager->persist($head_accueil);
122 $entityManager->persist($header);
123 $entityManager->persist($nav);
124 $entityManager->persist($main);
125 $entityManager->persist($footer);
126 $entityManager->persist($breadcrumb);
127 $entityManager->persist($head_login);
128 $entityManager->persist($login);
129 $entityManager->persist($head_article);
130 $entityManager->persist($favicon);
131 $entityManager->persist($logo);
132 $entityManager->persist($facebook);
133 $entityManager->persist($instagram);
134 $entityManager->persist($fond_piscine);
135 $entityManager->persist($head_accueil_data);
136 $entityManager->persist($header_data);
137 $entityManager->persist($footer_data);
138 $entityManager->persist($head_login_data);
139 $entityManager->persist($head_article_data);
140 $entityManager->flush();
141
142 header('Location: ' . new URL);
143 die;
144} \ No newline at end of file
diff --git a/src/controller/password.php b/src/controller/password.php
new file mode 100644
index 0000000..d5e66ff
--- /dev/null
+++ b/src/controller/password.php
@@ -0,0 +1,357 @@
1<?php
2// src/controller/password.php
3//
4// test mot de passe et captcha
5
6declare(strict_types=1);
7
8use Doctrine\ORM\EntityManager;
9use App\Entity\User;
10
11// exécutée dans installation.php à l'ouverture de chaque page
12function existUsers(EntityManager $entityManager)
13{
14 // lecture
15 $users = $entityManager->getRepository(User::class)->findAll();
16
17 // cas particulier table vide
18 if(count($users) === 0)
19 {
20 $_GET = [];
21 $_SESSION['user'] = '';
22 $_SESSION['admin'] = false;
23
24 // création d'un utilisateur, puis rechargement de la page
25 createPassword($entityManager);
26 }
27}
28
29
30function createPassword(EntityManager $entityManager)
31{
32 // fonction exécutée à priori deux fois d'affilée: affichage puis traitement de la saisie
33
34 // II - traitement
35 unset($_SESSION['user']);
36
37 $captcha_solution = (isset($_SESSION['captcha']) && is_int($_SESSION['captcha'])) ? $_SESSION['captcha'] : 0;
38 $captcha = isset($_POST['captcha']) ? controlCaptchaInput($_POST['captcha']) : 0;
39
40 $error = '';
41 if(!isset($_POST['captcha'])) // page rechargée
42 {
43 //$error = '';
44 }
45 elseif($captcha == 0)
46 {
47 $error = 'error_non_valid_captcha';
48 }
49 elseif($captcha_solution == 0)
50 {
51 //$error = '';
52 }
53 elseif($captcha != $captcha_solution) // le test!
54 {
55 $error = 'bad_solution_captcha';
56 }
57 elseif(!isset($_POST['password']) || empty($_POST['password'])
58 || (!isset($_POST['login']) || empty($_POST['login'])))
59 {
60 $error = 'bad_login_or_password';
61 }
62 else
63 {
64 // -> caractères HTML dangereux supprimés
65 $login = Security::secureString($_POST['login']);
66 $password = Security::secureString($_POST['password']);
67
68 // -> prévenir la validation par erreur d'une chaine "vide"
69 $login = removeSpacesTabsCRLF($login);
70 $password = removeSpacesTabsCRLF($password);
71
72 // conformité
73 if(isset($password) && isset($login)
74 && $password == $_POST['password'] && $login == $_POST['login'])
75 {
76 // enregistrement et redirection
77 $password = password_hash($password, PASSWORD_DEFAULT);
78 $user = new App\Entity\User($login, $password);
79 $entityManager->persist($user);
80 $entityManager->flush();
81
82 header('Location: ' . new URL);
83 exit();
84 }
85 else
86 {
87 $error = 'bad_password';
88 }
89 }
90
91 // inséré dans $captchaHtml puis dans $formulaireNouveauMDP
92 $captcha = createCaptcha();
93 // enregistrement de la réponse du captcha pour vérification
94 $_SESSION['captcha'] = $captcha[2]; // int
95
96
97 // I - affichage
98 $title = 'Bienvenue nageur bigouden';
99 $subHeading = 'Veuillez choisir les codes que vous utiliserez pour gérer le site.';
100
101 // même vue que la fonction changerMotDePasse()
102 require('../src/view/password.php');
103
104 echo($header);
105 if($error != '')
106 {
107 sleep(1);
108 echo($error_messages[$error]);
109 }
110 echo($formulaireNouveauMDP);
111 echo($error_messages['forbidden_characters']);
112 echo($footer);
113 die;
114}
115
116
117function connect(LoginBuilder $builder, EntityManager $entityManager)
118{
119 // déjà connecté
120 if($_SESSION['admin'])
121 {
122 header('Location: ' . new URL);
123 die;
124 }
125
126 // II - traitement
127 $_SESSION['user'] = '';
128 $_SESSION['admin'] = false;
129
130 $captcha_solution = (isset($_SESSION['captcha']) && is_int($_SESSION['captcha'])) ? $_SESSION['captcha'] : 0;
131 $captcha = isset($_POST['captcha']) ? controlCaptchaInput($_POST['captcha']) : 0;
132
133 $error = '';
134 if(!isset($_POST['captcha'])) // page rechargée
135 {
136 //$error = '';
137 }
138 elseif($captcha == 0)
139 {
140 $error = 'error_non_valid_captcha';
141 }
142 elseif($captcha_solution == 0)
143 {
144 //$error = '';
145 }
146 elseif($captcha != $captcha_solution) // le test!
147 {
148 $error = 'bad_solution_captcha';
149 }
150 elseif(!isset($_POST['login']) || empty($_POST['login'])
151 || !isset($_POST['password']) || empty($_POST['password']))
152 {
153 $error = 'bad_password';
154 }
155 else // c'est OK
156 {
157 $login = $_POST['login'];
158 $password = $_POST['password'];
159 $user = getUser($login, $entityManager);
160
161 // enregistrement et redirection
162 if(password_verify($password, $user->getPassword()))
163 {
164 session_start();
165 $_SESSION['user'] = $login;
166 $_SESSION['admin'] = true;
167 $link = new URL(isset($_GET['from']) ? ['page' => $_GET['from']] : []);
168 isset($_GET['id']) ? $link->addParams(['id' => $_GET['id']]) : '';
169 header('Location: ' . $link);
170 die;
171 }
172 else
173 {
174 $error = 'bad_password';
175 }
176 }
177
178 // inséré dans $captchaHtml puis dans $formulaireNouveauMDP
179 $captcha = createCaptcha();
180 // enregistrement de la réponse du captcha pour vérification
181 $_SESSION['captcha'] = $captcha[2]; // int
182
183 // I - affichage
184 $title = "Connexion";
185 $subHeading = "Veuillez saisir votre identifiant (e-mail) et votre mot de passe.";
186
187 require('../src/view/password.php');
188
189 //$builder->addHTML($header);
190 if($error != '')
191 {
192 sleep(1);
193 $builder->addHTML($error_messages[$error]);
194 }
195 $builder->addHTML($formulaireConnexion);
196 //$builder->addHTML($warning_messages['message_cookie']);
197 $builder->addHTML($warning_messages['private_browsing']);
198 $builder->addHTML($footer);
199
200 //die;
201}
202
203
204function changePassword(EntityManager $entityManager)
205{
206 // fonction exécutée à priori deux fois d'affilée: affichage puis traitement de la saisie
207
208 // OUT !!
209 if(empty($_SESSION['user']) || !$_SESSION['admin'])
210 {
211 $_SESSION['user'] = '';
212 $_SESSION['admin'] = false;
213 header('Location: index.php');
214 die;
215 }
216
217 // II - traitement
218 $error = '';
219 $success = false;
220 if(empty($_POST)) // première fois ou page rechargée
221 {
222 //
223 }
224 elseif(!isset($_POST['login']) || empty($_POST['login'])
225 || !isset($_POST['old_password']) || empty($_POST['old_password'])
226 || !isset($_POST['new_password']) || empty($_POST['new_password']))
227 {
228 $error = 'bad_login_or_password';
229 }
230 else
231 {
232 // sécurisation de la saisie
233 $new_password = Security::secureString($_POST['new_password']);
234 $login = Security::secureString($_POST['login']);
235 $old_password = Security::secureString($_POST['old_password']);
236
237 // éviter d'enregistrer une chaîne vide
238 $new_password = removeSpacesTabsCRLF($new_password);
239
240 // tests de conformité
241 if($login != $_POST['login'] || $old_password != $_POST['old_password'] || $new_password != $_POST['new_password'])
242 {
243 $error = 'forbidden_characters';
244 }
245 else
246 {
247 $user = getUser($login, $entityManager);
248
249 if(password_verify($old_password, $user->getPassword()))
250 {
251 // enregistrement et redirection
252 $new_password = password_hash($new_password, PASSWORD_DEFAULT);
253 $user->setPassword($new_password);
254 $entityManager->flush();
255 $success = true;
256 }
257 else
258 {
259 $error = 'bad_password';
260 }
261 }
262 }
263
264
265 // I - affichage
266 $title = "Nouveau mot de passe";
267 $subHeading = "Veuillez vous identifier à nouveau puis saisir votre nouveau mot de passe.";
268
269 require('../src/view/password.php');
270
271 echo($header);
272 if($error != '')
273 {
274 sleep(1); // sécurité TRÈS insuffisante à la force brute
275 echo($error_messages[$error]);
276 }
277 elseif($success)
278 {
279 $success = false;
280 echo($alertJSNewPassword);
281 die;
282 }
283 echo($formulaireModifMDP);
284 echo($footer);
285 die;
286}
287
288
289function getUser(string $login, EntityManager $entityManager): User
290{
291 $users = $entityManager->getRepository('App\Entity\User')->findBy(['login' => $login]);
292
293 // détection d'un abus
294 if(count($users) === 0)
295 {
296 $_SESSION['user'] = '';
297 $_SESSION['admin'] = false;
298
299 header('Location: index.php'); // page création d'un mot de passe à l'attérissage
300 die;
301 }
302
303 foreach($users as $user)
304 {
305 if($user->getLogin() === $login)
306 {
307 return $user;
308 }
309 }
310 header('Location: ' . new URL);
311 die;
312}
313
314
315function disconnect(EntityManager $entityManager)
316{
317 // nettoyage complet
318 $_SESSION = []; // mémoire vive
319 session_destroy(); // fichier côté serveur
320 setcookie('PHPSESSID', '', time() - 4200, '/'); // cookie de session
321 $link = new URL(['page' => $_GET['page']]);
322 isset($_GET['id']) ? $link->addParams(['id' => $_GET['id']]) : '';
323 header('Location: ' . $link);
324 die;
325}
326
327
328function createCaptcha(): array
329{
330 $a = rand(2, 9);
331 $b = rand(2, 9);
332 return array(toLettersFrench($a), toLettersFrench($b), $a * $b);
333}
334
335function toLettersFrench(int $number): string
336{
337 return match($number)
338 {
339 2 => 'deux',
340 3 => 'trois',
341 4 => 'quatre',
342 5 => 'cinq',
343 6 => 'six',
344 7 => 'sept',
345 8 => 'huit',
346 9 => 'neuf',
347 default => '', // erreur
348 };
349}
350
351// on veut des chiffres
352function controlCaptchaInput(string $captcha = '0'): int
353{
354 // $captcha est un POST donc une chaîne, '2.3' est acceptés
355 // (int) supprime les décimales
356 return (is_numeric($captcha) && $captcha == (int) $captcha) ? (int) $captcha : 0;
357} \ No newline at end of file
diff --git a/src/controller/post.php b/src/controller/post.php
new file mode 100644
index 0000000..926a5ae
--- /dev/null
+++ b/src/controller/post.php
@@ -0,0 +1,17 @@
1<?php
2// src/controller/post.php
3
4declare(strict_types=1);
5
6if($_SERVER['REQUEST_METHOD'] === 'POST' && $_SESSION['admin'] === true)
7{
8 // requêtes AJAX
9 require '../src/controller/ajax.php';
10
11 // formulaires HTML
12 if(isset($_POST['from']) // page d'où vient la requête
13 && isset($_POST)) // données
14 {
15 echo "requête envoyée en validant un formulaire";
16 }
17}
diff --git a/src/model/Menu.php b/src/model/Menu.php
new file mode 100644
index 0000000..624a0fc
--- /dev/null
+++ b/src/model/Menu.php
@@ -0,0 +1,53 @@
1<?php
2// src/controller/Menu.php
3
4declare(strict_types=1);
5
6use Doctrine\ORM\EntityManager;
7use App\Entity\Page;
8use Doctrine\Common\Collections\ArrayCollection;
9
10class Menu extends Page
11{
12 private EntityManager $entityManager;
13 private array $other_pages = []; // pages n'apparaissant pas dans le menu
14
15 public function __construct(EntityManager $entityManager){
16 $this->children = new ArrayCollection();
17
18 $bulk_data = $entityManager
19 ->createQuery('SELECT n FROM App\Entity\Page n WHERE n.parent IS null') // :Doctrine\ORM\Query
20 ->getResult(); // :array de Page
21
22 if(count($bulk_data) === 0){
23 makeStartPage($entityManager);
24 }
25
26 foreach($bulk_data as $first_level_entries){
27 // génération du menu
28 if($first_level_entries->getInMenu()){
29 $this->addChild($first_level_entries);
30 }
31 // autres pages
32 else{
33 // attention, seul le premier élément du chemin est pris en compte
34 $this->other_pages[] = $first_level_entries;
35 }
36 }
37
38 foreach($this->getChildren() as $page){
39 $page->fillChildrenPagePath();
40 }
41
42 /*for($i = 0; $i < count($this->getChildren()[1]->getChildren()); $i++){
43 echo $this->getChildren()[1]->getChildren()[$i]->getEndOfPath() . ' - ';
44 echo $this->getChildren()[1]->getChildren()[$i]->getPageName() . '<br>';
45 }*/
46 //die;
47 }
48
49 public function getOtherPages(): array
50 {
51 return $this->other_pages;
52 }
53} \ No newline at end of file
diff --git a/src/model/Path.php b/src/model/Path.php
new file mode 100644
index 0000000..6faadfd
--- /dev/null
+++ b/src/model/Path.php
@@ -0,0 +1,84 @@
1<?php
2// src/controller/Path.php
3
4declare(strict_types=1);
5
6use Doctrine\ORM\EntityManager;
7use App\Entity\Page;
8
9class Path extends Page
10{
11 private array $current_page = []; // tableau d'objets Page
12
13 public function __construct()
14 {
15 $path_array = explode('/', CURRENT_PAGE);
16 try{
17 $this->findPage(Director::$menu_data, $path_array); // remplit $this->current_page
18 }
19 catch(Exception $e){}
20 /*echo "nb d'autres pages: " . count(Director::$menu_data->getOtherPages()) . '<br>';
21 echo 'longueur du chemin: ' . count($this->current_page) . '<br>';
22 foreach($this->current_page as $current){
23 echo $current->getEndOfPath() . ' ';
24 }
25 die;*/
26 }
27
28 // produit un tableau de Page en comparant le chemin demandé avec les données dans Menu
29 // succès => une exception est lancée pour sortir des fonctions imbriquées
30 // echec => redirection vers la page erreur 404
31 private function findPage(Page|Menu $menu, array $path_array)
32 {
33 // recherche dans les autres pages
34 if($menu instanceof Menu){
35 foreach($menu->getOtherPages() as $page)
36 {
37 if($path_array[0] === $page->getEndOfPath())
38 {
39 $this->current_page[] = $page;
40 throw new Exception();
41 }
42 }
43 }
44 // recherche dans le menu
45 foreach($menu->getChildren() as $page)
46 {
47 if($path_array[0] === $page->getEndOfPath())
48 {
49 $this->current_page[] = $page;
50 if(count($path_array) > 1)
51 {
52 array_shift($path_array); // $this->path_array n'est pas modifié, un tableau PHP est passé à une fonction par copie
53 $this->findPage($page, $path_array);
54 }
55 else{
56 throw new Exception(); // sortir de tous les findPage() en même temps
57 }
58 }
59 }
60 // rien trouvé
61 URL::setPath('erreur404.html');
62 header('Location: '. new URL);
63 die;
64 }
65
66 public function getString(): string
67 {
68 $path_string = "";
69 foreach($this->current_page as $one_page){
70 $path_string .= $one_page->getEndOfPath() . '/';
71 }
72 return rtrim($path_string, '/');
73 }
74 public function getArray(): array
75 {
76 return $this->current_page;
77 }
78
79 // c'est là qu'on est quoi
80 public function getLast(): Page
81 {
82 return $this->current_page[count($this->current_page) - 1];
83 }
84} \ No newline at end of file
diff --git a/src/model/doctrine-bootstrap.php b/src/model/doctrine-bootstrap.php
new file mode 100644
index 0000000..139f410
--- /dev/null
+++ b/src/model/doctrine-bootstrap.php
@@ -0,0 +1,31 @@
1<?php
2// src/model/doctrine-bootstrap.php
3
4use Doctrine\ORM\ORMSetup;
5use Doctrine\DBAL\DriverManager;
6use Doctrine\ORM\EntityManager;
7
8require_once "../vendor/autoload.php";
9
10// Create a simple "default" Doctrine ORM configuration for Attributes
11$config = ORMSetup::createAttributeMetadataConfiguration(
12 //paths: array(__DIR__.'/entities'),
13 paths: array('../src/model/entities'),
14 isDevMode: true,
15 // true: cache en mémoire vive
16 // false: utilisation de APCu ou redis ou memcache
17);
18
19// configuring the database connection
20$connection = DriverManager::getConnection([
21 'driver' => Config::$db_driver,
22 'user' => Config::$user,
23 'password' => Config::$password,
24 'host' => Config::$db_host,
25 'dbname' => Config::$database,
26], $config);
27
28// obtaining the entity manager
29$entityManager = new EntityManager($connection, $config);
30
31foreach($entityManager->getMetadataFactory()->getAllMetadata() as $class){} \ No newline at end of file
diff --git a/src/model/entities/Article.php b/src/model/entities/Article.php
new file mode 100644
index 0000000..3b846da
--- /dev/null
+++ b/src/model/entities/Article.php
@@ -0,0 +1,77 @@
1<?php
2// src/model/entities/Article.php
3
4declare(strict_types=1);
5
6namespace App\Entity;
7
8use Doctrine\ORM\Mapping as ORM;
9use Doctrine\Common\Collections\ArrayCollection;
10use Doctrine\Common\Collections\Collection;
11
12#[ORM\Entity]
13#[ORM\Table(name: TABLE_PREFIX . "article")]
14class Article
15{
16 #[ORM\Id]
17 #[ORM\GeneratedValue]
18 #[ORM\Column(type: "integer")]
19 private int $id_article;
20
21 // datetime_immutable permet à la base de toujours gérer cette clé primaire correctement
22 #[ORM\Column(type: 'datetime', options: ['default' => 'CURRENT_TIMESTAMP'], unique: true)]
23 private \DateTime $date_time; // le type datetime de doctrine convertit en type \DateTime de PHP
24
25 #[ORM\Column(type: "string")]
26 private string $title;
27
28 #[ORM\Column(type: "text")]
29 private string $preview; // une simple textarea
30
31 #[ORM\Column(type: "text")]
32 private string $content; // de l'éditeur html
33
34 // liaison avec table intermédiaire
35 #[ORM\ManyToMany(targetEntity: Image::class, inversedBy: "article")]
36 #[ORM\JoinTable(
37 name: "nb_article_image",
38 joinColumns: [new ORM\JoinColumn(name: "article_id", referencedColumnName: "id_article", onDelete: "CASCADE")],
39 inverseJoinColumns: [new ORM\JoinColumn(name: "image_id", referencedColumnName: "id_image", onDelete: "CASCADE")]
40 )]
41 private Collection $images;
42
43 public function __construct()
44 {
45 $this->images = new ArrayCollection(); // initialisation nécessaire
46 }
47
48 public function getDateTime(): \DateTime
49 {
50 return $this->date_time;
51 }
52 public function getTimestamp(): int
53 {
54 return $this->date_time->getTimestamp();
55 }
56 public function getTitle(): string
57 {
58 return $this->title;
59 }
60 public function getPreview(): string
61 {
62 return $this->preview;
63 }
64 public function getContent(): string
65 {
66 return $this->content;
67 }
68 public function setContent(string $data): void
69 {
70 $this->content = $data;
71 }
72
73 public function getImages(): Collection
74 {
75 return $this->images;
76 }
77}
diff --git a/src/model/entities/Image.php b/src/model/entities/Image.php
new file mode 100644
index 0000000..181c137
--- /dev/null
+++ b/src/model/entities/Image.php
@@ -0,0 +1,91 @@
1<?php
2// src/model/entities/Image.php
3
4declare(strict_types=1);
5
6namespace App\Entity;
7
8use Doctrine\ORM\Mapping as ORM;
9
10#[ORM\Entity]
11#[ORM\Table(name: TABLE_PREFIX . "image")]
12class Image
13{
14 #[ORM\Id]
15 #[ORM\GeneratedValue]
16 #[ORM\Column(type: "integer")]
17 private int $id_image;
18
19 #[ORM\Column(type: "string", length: 255, unique: true)] // nom d'image UNIQUE
20 private string $file_name;
21
22 // choisir un répertoire du genre /var/www/html/uploads/ de préférence hors de /src
23 #[ORM\Column(type: "string", length: 255, unique: true, nullable: true)]
24 private ?string $file_path;
25
26 #[ORM\Column(type: "string", length: 255, unique: true, nullable: true)]
27 private ?string $file_path_mini;
28
29 #[ORM\Column(type: "string", length: 255, nullable: true)]
30 private string $mime_type; // image/jpeg, image/png, etc
31
32 #[ORM\Column(type: "string", length: 255, nullable: true)]
33 private string $alt; // texte alternatif
34
35 // autre champs optionnels: file_size, date (default current timestamp)
36
37 /* étapes au téléchargement:
38 => 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.
39 => 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.
40 => 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.
41 => Déplacement du fichier sur le serveur : Le fichier est déplacé depuis son emplacement temporaire vers le répertoire uploads/.
42 => Enregistrement dans la base de données : On enregistre les informations de l'image dans la base de données. */
43
44 #[ORM\ManyToMany(targetEntity: NodeData::class, mappedBy: "images")]
45 private $node_data;
46
47 #[ORM\ManyToMany(targetEntity: Article::class, mappedBy: "images")]
48 private $article;
49
50 public function __construct(string $name, ?string $path, ?string $path_mini, string $mime_type, string $alt)
51 {
52 $this->file_name = $name;
53 $this->file_path = $path;
54 $this->file_path_mini = $path_mini;
55 $this->mime_type = $mime_type;
56 $this->alt = $alt;
57 }
58
59 public function getFileName(): string
60 {
61 return $this->file_name;
62 }
63 public function getFilePath(): string
64 {
65 return $this->file_path;
66 }
67 public function getFilePathMini(): string
68 {
69 return $this->file_path_mini;
70 }
71 public function getAlt(): string
72 {
73 return $this->alt;
74 }
75
76
77 // pour ViewBuilder?
78 /*public function displayImage($imageId): void
79 {
80 //$imageId = 1; // Exemple d'ID d'image
81 $stmt = $pdo->prepare("SELECT file_path FROM images WHERE id = ?");
82 $stmt->execute([$imageId]);
83 $image = $stmt->fetch();
84
85 if ($image) {
86 echo "<img src='" . $image['file_path'] . "' alt='Image'>";
87 } else {
88 echo "Image non trouvée.";
89 }
90 }*/
91}
diff --git a/src/model/entities/Node.php b/src/model/entities/Node.php
new file mode 100644
index 0000000..49e16ba
--- /dev/null
+++ b/src/model/entities/Node.php
@@ -0,0 +1,168 @@
1<?php
2// src/model/entities/Node.php
3
4declare(strict_types=1);
5
6namespace App\Entity;
7
8use Config;
9use Doctrine\ORM\Mapping as ORM;
10
11#[ORM\Entity]
12#[ORM\Table(name: TABLE_PREFIX . "node")]
13class Node
14{
15 #[ORM\Id]
16 #[ORM\GeneratedValue]
17 #[ORM\Column(type: "integer")]
18 private int $id_node;
19
20 #[ORM\Column(type: "string", length: 255)]
21 private string $name_node;
22
23 #[ORM\Column(type: "string", length: 255, unique: true, nullable: true)]
24 private ?string $article_timestamp;
25
26 #[ORM\Column(type: "json", nullable: true)] // type: "json" crée un longtext avec mariadb
27 private ?array $attributes = null;
28
29 #[ORM\Column(type: "integer")]
30 private int $position;
31
32 #[ORM\ManyToOne(targetEntity: self::class)]
33 //#[ORM\ManyToOne(targetEntity: self::class, fetch: 'EAGER')] // À TESTER
34 #[ORM\JoinColumn(name: "parent_id", referencedColumnName: "id_node", onDelete: "SET NULL", nullable: true)]
35 private ?self $parent = null;
36
37 #[ORM\ManyToOne(targetEntity: Page::class)]
38 #[ORM\JoinColumn(name: "page_id", referencedColumnName: "id_page", onDelete: "SET DEFAULT", nullable: true)]
39 private ?Page $page;
40
41 #[ORM\ManyToOne(targetEntity: Article::class, cascade: ['persist'])]
42 #[ORM\JoinColumn(name: "article_id", referencedColumnName: "id_article", onDelete: "SET NULL", nullable: true)]
43 private ?Article $article = null;
44
45 // propriété non mappée dans la table "node", la jointure est décrite dans NodeData
46 // elle sert à persister ou supprimer des données par cascade
47 // "mappedBy" permet de cibler $node dans l'autre classe, qui elle possède un "inversedBy"
48 #[ORM\OneToOne(targetEntity: NodeData::class, mappedBy: "node", cascade: ['persist', 'remove'])]
49 private ?NodeData $node_data = null;
50
51
52 // -- fin des attributs destinés à doctrine, début du code utilisateur --
53
54 private array $children = []; // tableau de Node
55 private ?self $temp_child = null; // = "new" est l'enfant de "main" lorsque la page est "article"
56
57 public function __construct(string $name = '', ?string $article_timestamp = null, array $attributes = [], int $position = 0, ?self $parent = null, ?Page $page = null, ?Article $article = null)
58 {
59 $this->name_node = $name;
60 $this->article_timestamp = $article_timestamp;
61 $this->attributes = $attributes;
62 $this->position = $position;
63 $this->parent = $parent;
64 $this->page = $page;
65 $this->article = $article;
66 }
67
68 public function addChild(self $child): void
69 {
70 $this->children[] = $child;
71 $this->sortChildren();
72 }
73
74 // utiliser $position pour afficher les éléments dans l'ordre
75 private function sortChildren(): void
76 {
77 $iteration = count($this->children);
78 while($iteration > 1)
79 {
80 for($i = 0; $i < $iteration - 1; $i++)
81 {
82 //echo '<br>' . $this->children[$i]->getPosition() . ' - ' . $this->children[$i + 1]->getPosition();
83 if($this->children[$i]->getPosition() > $this->children[$i + 1]->getPosition())
84 {
85 $tmp = $this->children[$i];
86 $this->children[$i] = $this->children[$i + 1];
87 $this->children[$i + 1] = $tmp;
88 }
89 }
90 $iteration--;
91 }
92 }
93
94 // pfff...
95 public function getId(): int
96 {
97 return $this->id_node;
98 }
99 public function getName(): string
100 {
101 return $this->name_node;
102 }
103 /*public function setName(string $name): void
104 {
105 $this->name_node = $name;
106 }*/
107 public function getArticleTimestamp(): string
108 {
109 return $this->article_timestamp;
110 }
111 public function getAttributes(): array
112 {
113 return $this->attributes;
114 }
115 /*public function setAttributes(array $attributes): void
116 {
117 $this->attributes = $attributes;
118 }*/
119 public function getParent(): ?self
120 {
121 return $this->parent;
122 }
123 /*public function setParent(?self $parent): void
124 {
125 $this->parent = $parent;
126 }*/
127 public function getPosition(): int
128 {
129 return $this->position;
130 }
131 /*public function setPosition(int $position): void
132 {
133 $this->position = $position;
134 }*/
135 public function getPage(): Page
136 {
137 return $this->page;
138 }
139 /*public function setPage(Page $page): void
140 {
141 $this->page = $page;
142 }*/
143 public function getArticle(): Article
144 {
145 return $this->article;
146 }
147 /*public function setArticle(Article $article): void
148 {
149 $this->article = $article;
150 }*/
151 public function getNodeData(): ?NodeData
152 {
153 return $this->node_data;
154 }
155 public function getChildren(): array
156 {
157 return $this->children;
158 }
159
160 public function getTempChild(): ?self // peut renvoyer null
161 {
162 return $this->temp_child;
163 }
164 public function setTempChild(self $child): void
165 {
166 $this->temp_child = $child;
167 }
168}
diff --git a/src/model/entities/NodeData.php b/src/model/entities/NodeData.php
new file mode 100644
index 0000000..ddf6083
--- /dev/null
+++ b/src/model/entities/NodeData.php
@@ -0,0 +1,62 @@
1<?php
2// src/model/entities/NodeData.php
3
4declare(strict_types=1);
5
6namespace App\Entity;
7
8use Doctrine\ORM\Mapping as ORM;
9use Doctrine\Common\Collections\ArrayCollection; // classe
10use Doctrine\Common\Collections\Collection; // interface
11
12#[ORM\Entity]
13#[ORM\Table(name: TABLE_PREFIX . "node_data")]
14class NodeData
15{
16 #[ORM\Id]
17 #[ORM\GeneratedValue]
18 #[ORM\Column(type: "integer")]
19 private int $id_node_data;
20
21 // onDelete: "CASCADE" supprime les données si le noeud est supprimé
22 // inverseBy fait le lien avec $node_data dans Node (qui a "mappedBy")
23 #[ORM\OneToOne(targetEntity: Node::class, inversedBy: "node_data")]
24 #[ORM\JoinColumn(name: "node_id", referencedColumnName: "id_node", onDelete: "CASCADE")]
25 private Node $node;
26
27 #[ORM\Column(type: "json")]
28 private array $data;
29
30 // liaison avec table intermédiaire
31 #[ORM\ManyToMany(targetEntity: Image::class, inversedBy: "id_image")]
32 #[ORM\JoinTable(
33 name: "nb_node_image",
34 joinColumns: [new ORM\JoinColumn(name: "node_data_id", referencedColumnName: "id_node_data", onDelete: "CASCADE")],
35 inverseJoinColumns: [new ORM\JoinColumn(name: "image_id", referencedColumnName: "id_image", onDelete: "CASCADE")]
36 )]
37 private Collection $images;
38
39 public function __construct(array $data, Node $node, Collection $images = new ArrayCollection)
40 {
41 $this->data = $data;
42 $this->node = $node;
43 $this->images = $images;
44 }
45
46 public function getData(): array
47 {
48 return $this->data;
49 }
50 /*public function setData(array $data): void
51 {
52 $this->data = $data;
53 }
54 public function setNode(Node $node): void
55 {
56 $this->node = $node;
57 }*/
58 public function getImages(): Collection
59 {
60 return $this->images;
61 }
62}
diff --git a/src/model/entities/Page.php b/src/model/entities/Page.php
new file mode 100644
index 0000000..d7d8098
--- /dev/null
+++ b/src/model/entities/Page.php
@@ -0,0 +1,97 @@
1<?php
2// src/model/entities/Page.php
3
4declare(strict_types=1);
5
6namespace App\Entity;
7
8use Doctrine\ORM\Mapping as ORM;
9use Doctrine\Common\Collections\Collection;
10use Doctrine\Common\Collections\ArrayCollection;
11
12#[ORM\Entity]
13#[ORM\Table(name: TABLE_PREFIX . "page")]
14class Page
15{
16 #[ORM\Id]
17 #[ORM\GeneratedValue]
18 #[ORM\Column(type: "integer")]
19 private int $id_page;
20
21 #[ORM\Column(type: "string", length: 255)]
22 private string $name_page;
23
24 #[ORM\Column(type: "string", length: 255)]
25 private string $end_of_path; // morceau d'URL plus exactement
26
27 private string $page_path;
28
29 #[ORM\Column(type: "boolean")]
30 private bool $reachable;
31
32 #[ORM\Column(type: "boolean")]
33 private bool $in_menu;
34
35 #[ORM\ManyToOne(targetEntity: self::class)]
36 #[ORM\JoinColumn(name: "parent_id", referencedColumnName: "id_page", onDelete: "SET NULL", nullable: true)]
37 private ?self $parent = null;
38
39 #[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')]
40 protected Collection $children;
41
42 /*#[ORM\Column(type: "json", nullable: true)]
43 private ?array $metadata = null;*/
44
45 public function __construct(string $name, string $eop, bool $reachable, bool $in_menu, ?Page $parent)
46 {
47 $this->name_page = $name;
48 $this->end_of_path = $eop;
49 $this->reachable = $reachable;
50 $this->in_menu = $in_menu;
51 $this->parent = $parent;
52 $this->children = new ArrayCollection();
53 }
54
55 // getters
56 /*public function getId(): int
57 {
58 return $this->id_page;
59 }*/
60 public function getPageName(): string
61 {
62 return $this->name_page;
63 }
64 public function getPagePath(): string
65 {
66 return $this->page_path;
67 }
68 public function getEndOfPath(): string
69 {
70 return $this->end_of_path;
71 }
72 public function getInMenu(): bool
73 {
74 return $this->in_menu;
75 }
76 public function getParent(): ?Page
77 {
78 return $this->parent;
79 }
80 public function getChildren(): Collection
81 {
82 return $this->children;
83 }
84
85 public function fillChildrenPagePath(string $parent_path = ''): void
86 {
87 $this->page_path = $parent_path != '' ? $parent_path . '/' . $this->end_of_path : $this->end_of_path;
88 foreach($this->getChildren() as $page){
89 $page->fillChildrenPagePath($this->page_path);
90 }
91 }
92
93 public function addChild(self $child): void
94 {
95 $this->children[] = $child;
96 }
97}
diff --git a/src/model/entities/User.php b/src/model/entities/User.php
new file mode 100644
index 0000000..4b1dcb8
--- /dev/null
+++ b/src/model/entities/User.php
@@ -0,0 +1,47 @@
1<?php
2// src/model/entities/User.php
3
4declare(strict_types=1);
5
6namespace App\Entity;
7
8use Doctrine\ORM\Mapping as ORM;
9
10#[ORM\Entity]
11#[ORM\Table(name: TABLE_PREFIX . "user")]
12class User
13{
14 #[ORM\Id]
15 #[ORM\GeneratedValue]
16 #[ORM\Column(type: "integer")]
17 private int $id_user;
18
19 /*#[ORM\Column(type: "string", length: 255)]
20 private string $name;*/
21
22 #[ORM\Column(type: "string", length: 255, unique: true)] // risque de modifier son mot de passe sans s'apercevoir qu'il fonctionne encore sur un autre compte
23 private string $login;
24
25 #[ORM\Column(type: "string", length: 255)]
26 private string $password;
27
28 public function __construct(string $login, string $password)
29 {
30 $this->login = $login;
31 $this->password = $password;
32 }
33
34 public function getLogin(): string
35 {
36 return $this->login;
37 }
38 public function getPassword(): string
39 {
40 return $this->password;
41 }
42
43 public function setPassword(string $password): void
44 {
45 $this->password = $password;
46 }
47} \ No newline at end of file
diff --git a/src/view/AbstractBuilder.php b/src/view/AbstractBuilder.php
new file mode 100644
index 0000000..cd2b361
--- /dev/null
+++ b/src/view/AbstractBuilder.php
@@ -0,0 +1,52 @@
1<?php
2// src/view/AbstractBuilder.php
3
4use App\Entity\Node;
5
6abstract class AbstractBuilder
7{
8 protected const VIEWS_PATH = '../src/view/templates/';
9 protected string $html = '';
10
11 protected function useChildrenBuilder(Node $node): void
12 {
13 foreach($node->getChildren() as $child_node)
14 {
15 $builder_name = $this->snakeToPascalCase($child_node->getName()) . 'Builder';
16 $builder = new $builder_name($child_node);
17 $this->html .= $builder->render();
18
19 // pages spéciales où on n'assemble pas tout
20 if($builder_name === 'HeadBuilder' && $builder->getStop())
21 {
22 foreach($node->getChildren() as $target_node){
23 if($target_node->getName() === 'main'){
24 $main_node = $target_node;
25 break;
26 }
27 }
28 // on construit <main> et on s'arrête! les autres noeuds sont ignorés
29 $builder_name = $this->snakeToPascalCase($main_node->getName()) . 'Builder';
30 $builder = new $builder_name($main_node);
31 $this->html .= "<body>\n";
32 $this->html .= $builder->render() . "\n";
33 $this->html .= "</body>\n</html>";
34 break;
35 }
36 }
37 }
38
39 protected function snakeToPascalCase(string $input): string
40 {
41 return str_replace('_', '', ucwords($input, '_'));
42 }
43
44 public function render(): string // = getHTML()
45 {
46 return $this->html;
47 }
48 public function addHTML(string $html): void
49 {
50 $this->html .= $html;
51 }
52} \ No newline at end of file
diff --git a/src/view/ArticleBuilder.php b/src/view/ArticleBuilder.php
new file mode 100644
index 0000000..989da0d
--- /dev/null
+++ b/src/view/ArticleBuilder.php
@@ -0,0 +1,60 @@
1<?php
2// src/view/ArticleBuilder.php
3
4use App\Entity\Node;
5
6class ArticleBuilder extends AbstractBuilder
7{
8 public function __construct(Node $node)
9 {
10 $viewFile = self::VIEWS_PATH . $node->getName() . '.php';
11
12 if(file_exists($viewFile))
13 {
14 // id (timestamp)
15 if(!empty($node->getAttributes()))
16 {
17 extract($node->getAttributes());
18 }
19
20 // html
21 $title = $node->getArticle()->getTitle();
22 $html = $node->getArticle()->getContent();
23 $id = $node->getArticleTimestamp();
24
25 // partage
26 $share_link = new URL(['page' => CURRENT_PAGE], $id);
27 $share_js = 'onclick="copyInClipBoard(\'' . $share_link . '\')"';
28 $share_button = '<p><a href="' . $share_link . '" ' . $share_js . '><img class="action_icon" src="assets/share.svg"></a></p>' . "\n";
29
30 // modifier un article
31 $admin_buttons = '';
32 if($_SESSION['admin'])
33 {
34 $modify_js = 'onclick="openEditor(\'' . $id . '\')"';
35 $modify_article = '<p id="edit-' . $id . '"><a href="#"><img class="action_icon" src="assets/edit.svg" ' . $modify_js . '></a></p>' . "\n";
36
37 $up_link = new URL(['page' => CURRENT_PAGE, 'id' => $id, 'action' => 'position_up']);
38 $up_button = '<p id="position_up-' . $id . '"><a href="' . $up_link . '"><img class="action_icon" src="assets/arrow-up.svg"></a></p>' . "\n";
39
40 $down_link = new URL(['page' => CURRENT_PAGE, 'id' => $id, 'action' => 'position_down']);
41 $down_button = '<p id="position_down-' . $id . '"><a href="' . $down_link . '"><img class="action_icon" src="assets/arrow-down.svg"></a></p>' . "\n";
42
43 $delete_js = 'onclick="deleteArticle(\'' . $id . '\')"';
44 $delete_article = '<p id="delete-' . $id . '"><a href="#"><img class="action_icon" src="assets/delete-bin.svg" ' . $delete_js . '></a></p>' . "\n";
45
46 $close_js = 'onclick="closeEditor(\'' . $id . '\')"';
47 $close_editor = '<p id="cancel-' . $id . '" class="hidden"><a href="#"><button ' . $close_js . '>Annuler</button></a></p>';
48
49 $submit_js = 'onclick="submitArticle(\'' . $id . '\')"';
50 $submit_article = '<p id="submit-' . $id . '" class="hidden"><a href="#"><button ' . $submit_js . '>Valider</button></a></p>';
51
52 $admin_buttons = $modify_article . $up_button . $down_button . $delete_article . $close_editor . $submit_article;
53 }
54
55 ob_start();
56 require($viewFile);
57 $this->html .= ob_get_clean();
58 }
59 }
60}
diff --git a/src/view/BlogBuilder.php b/src/view/BlogBuilder.php
new file mode 100644
index 0000000..8c2125f
--- /dev/null
+++ b/src/view/BlogBuilder.php
@@ -0,0 +1,49 @@
1<?php
2// src/view/BlogBuilder.php
3
4use App\Entity\Node;
5
6class BlogBuilder extends AbstractBuilder
7{
8 public function __construct(Node $node)
9 {
10 $viewFile = self::VIEWS_PATH . $node->getName() . '.php';
11
12 if(file_exists($viewFile))
13 {
14 if(!empty($node->getNodeData()->getData()))
15 {
16 extract($node->getNodeData()->getData());
17 }
18
19 // ajouter un article
20 $new_article = '';
21 $new_article_admin_buttons = '';
22 if($_SESSION['admin'])
23 {
24 $id = 'new';
25
26 //$link = new URL(['page' => CURRENT_PAGE, 'action' => 'open_editor']);
27 $js = 'onclick="openEditor(\'' . $id . '\')"';
28 //$new_article = '<article><a href="' . $link . '"><button>Nouvel article</button></a></article>';
29 $new_article = '<article><p id="new"></p>' . "\n" .
30 '<p id="new-' . $id . '"><a href="#"><button ' . $js . '><img class="action_icon" src="assets/edit.svg">Nouvel article</button></a></p>';
31
32 $close_js = 'onclick="closeEditor(\'' . $id . '\')"';
33 $close_editor = '<div class="article_admin_zone"><p id="cancel-' . $id . '" class="hidden"><a href="#"><button ' . $close_js . '>Annuler</button></a></p>';
34
35 $submit_js = 'onclick="submitArticle(\'' . $id . '\')"';
36 $submit_article = '<p id="submit-' . $id . '" class="hidden"><a href="#"><button ' . $submit_js . '>Valider</button></a></p></div></article>';
37
38 $new_article_admin_buttons = $close_editor . $submit_article;
39 }
40
41 $this->useChildrenBuilder($node);
42 $content = $this->html;
43
44 ob_start();
45 require $viewFile;
46 $this->html = ob_get_clean(); // pas de concaténation ici, on écrase
47 }
48 }
49} \ No newline at end of file
diff --git a/src/view/BreadcrumbBuilder.php b/src/view/BreadcrumbBuilder.php
new file mode 100644
index 0000000..f1fdddf
--- /dev/null
+++ b/src/view/BreadcrumbBuilder.php
@@ -0,0 +1,53 @@
1<?php
2// src/view/BreadcrumbBuilder.php
3
4use App\Entity\Node;
5
6class BreadcrumbBuilder extends AbstractBuilder
7{
8 public function __construct(Node $node)
9 {
10 $this->html = $this->breadcrumbHTML(false);
11 }
12
13 private function breadcrumbHTML(bool $links = false): string
14 {
15 $asset = 'assets/home.svg'; // => BDD?
16 $breadcrumb_array = Director::$page_path->getArray(); // tableau de Page
17 $html = '';
18 $nb_of_entries = count($breadcrumb_array);
19
20 if($nb_of_entries > 1)
21 {
22 // petite maison et flèche
23 $html .= '<nav class="breadcrumb" aria-label="Breadcrumb">' . "\n";
24 $html .= '<a href="' . new URL . '"><img src="' . $asset . '"></a><span class="arrow"> →</span>' . "\n";
25
26 // partie intermédiaire
27 for($i = 0; $i < ($nb_of_entries - 1); $i++)
28 {
29 // liens optionnels
30 if($links)
31 {
32 $html .= '<a href="';
33 for($j = 1; $j < $i; $j++) // chemin sans la fin
34 {
35 $html .= new URL(['page' => $breadcrumb_array[$i]->getPagePath()]);
36 }
37 $html .= '">';
38 }
39 $html .= '<span>' . $breadcrumb_array[$i]->getPageName() . '</span>';
40 if($links)
41 {
42 $html .= '</a>';
43 }
44 $html .= '<span class="arrow"> →</span>' . "\n";
45 }
46
47 // fin du chemin
48 $html .= '<span aria-current="page">' . $breadcrumb_array[$nb_of_entries - 1]->getPageName() . "</span>\n";
49 $html .= "</nav>\n";
50 }
51 return $html;
52 }
53} \ No newline at end of file
diff --git a/src/view/FooterBuilder.php b/src/view/FooterBuilder.php
new file mode 100644
index 0000000..49da71c
--- /dev/null
+++ b/src/view/FooterBuilder.php
@@ -0,0 +1,64 @@
1<?php
2// src/view/FooterBuilder.php
3
4use App\Entity\Node;
5
6class FooterBuilder extends AbstractBuilder
7{
8 public function __construct(Node $node)
9 {
10 $viewFile = self::VIEWS_PATH . $node->getName() . '.php';
11
12 if(file_exists($viewFile))
13 {
14 // $adresses postale et e-mail
15 if(!empty($node->getNodeData()->getData()))
16 {
17 extract($node->getNodeData()->getData());
18 }
19
20 $this->useChildrenBuilder($node);
21 $breadcrumb = $this->html;
22
23 // zone admin
24 $empty_admin_zone = '';
25 //$zone_admin = '';
26 if($_SESSION['admin'])
27 {
28 $div_admin = 'logged_in';
29 $empty_admin_zone = 'empty_admin_zone';
30 $link_edit_page = new URL(['page' => CURRENT_PAGE, 'action' => 'modif_page']);
31 $link_new_page = new URL(['from' => CURRENT_PAGE, 'page' => 'nouvelle_page']);
32 $link_change_paths = new URL(['from' => CURRENT_PAGE, 'page' => 'menu_chemins']);
33
34 $link_change_password = new URL(['from' => CURRENT_PAGE, 'action' => 'modif_mdp']);
35 isset($_GET['id']) ? $link_change_password->addParams(['id' => $_GET['id']]) : '';
36
37 $link_logout = new URL(['page' => CURRENT_PAGE, 'action' => 'deconnexion']);
38 isset($_GET['id']) ? $link_logout->addParams(['id' => $_GET['id']]) : '';
39
40 $zone_admin = '<p>Vous êtes en mode administrateur.' . "\n" .
41 '<a href="' . $link_edit_page . '"><button>Modifier la page</button></a>' . "\n" .
42 '<a href="' . $link_new_page . '"><button>Nouvelle page</button></a>' . "\n" .
43 '<a href="' . $link_change_paths . '"><button>Menu et chemins</button></a>' . "\n" .
44 '<a href="' . $link_change_password . '"><button>Changer de mot de passe</button></a>' . "\n" .
45 '<a href="' . $link_logout . '"><button>Déconnexion</button></a></p>' . "\n";
46 }
47 else
48 {
49 $div_admin = 'logged_out';
50 $zone_admin = '';
51 if(Director::$page_path->getLast()->getEndOfPath() === 'article' && isset($_GET['id'])){
52 $zone_admin = '<button><a href="' . new URL(['page' => 'connexion', 'from' => CURRENT_PAGE, 'id' => $_GET['id']]) . '">Mode admin</a></button>';
53 }
54 else{
55 $zone_admin = '<button><a href="' . new URL(['page' => 'connexion', 'from' => CURRENT_PAGE]) . '">Mode admin</a></button>';
56 }
57 }
58
59 ob_start();
60 require $viewFile;
61 $this->html = ob_get_clean();
62 }
63 }
64} \ No newline at end of file
diff --git a/src/view/GaleryBuilder.php b/src/view/GaleryBuilder.php
new file mode 100644
index 0000000..89be2b1
--- /dev/null
+++ b/src/view/GaleryBuilder.php
@@ -0,0 +1,49 @@
1<?php
2// src/view/GaleryBuilder.php
3
4use App\Entity\Node;
5
6class GaleryBuilder extends AbstractBuilder
7{
8 public function __construct(Node $node)
9 {
10 $viewFile = self::VIEWS_PATH . $node->getName() . '.php';
11
12 if(file_exists($viewFile))
13 {
14 if(!empty($node->getNodeData()->getData()))
15 {
16 extract($node->getNodeData()->getData());
17 }
18
19 // ajouter un article
20 $new_article = '';
21 $new_article_admin_buttons = '';
22 if($_SESSION['admin'])
23 {
24 $id = 'new';
25
26 //$link = new URL(['page' => CURRENT_PAGE, 'action' => 'open_editor']);
27 $js = 'onclick="openEditor(\'' . $id . '\')"';
28 //$new_article = '<article><a href="' . $link . '"><button>Nouvel article</button></a></article>';
29 $new_article = '<article><p id="new"></p>' . "\n" .
30 '<p id="new-' . $id . '"><a href="#"><button ' . $js . '><img class="action_icon" src="assets/edit.svg">Nouvel article</button></a></p>';
31
32 $close_js = 'onclick="closeEditor(\'' . $id . '\')"';
33 $close_editor = '<div class="article_admin_zone"><p id="cancel-' . $id . '" class="hidden"><a href="#"><button ' . $close_js . '>Annuler</button></a></p>';
34
35 $submit_js = 'onclick="submitArticle(\'' . $id . '\')"';
36 $submit_article = '<p id="submit-' . $id . '" class="hidden"><a href="#"><button ' . $submit_js . '>Valider</button></a></p></div></article>';
37
38 $new_article_admin_buttons = $close_editor . $submit_article;
39 }
40
41 $this->useChildrenBuilder($node);
42 $content = $this->html;
43
44 ob_start();
45 require $viewFile;
46 $this->html = ob_get_clean(); // pas de concaténation ici, on écrase
47 }
48 }
49} \ No newline at end of file
diff --git a/src/view/GridBuilder.php b/src/view/GridBuilder.php
new file mode 100644
index 0000000..6e16d46
--- /dev/null
+++ b/src/view/GridBuilder.php
@@ -0,0 +1,55 @@
1<?php
2// src/view/GridBuilder.php
3
4use App\Entity\Node;
5
6class GridBuilder extends AbstractBuilder
7{
8 public function __construct(Node $node)
9 {
10 $viewFile = self::VIEWS_PATH . $node->getName() . '.php';
11
12 if(file_exists($viewFile))
13 {
14 if(!empty($node->getNodeData()->getData()))
15 {
16 extract($node->getNodeData()->getData());
17 }
18
19 // ajouter un article
20 $new_article = '';
21 $new_article_admin_buttons = '';
22 if($_SESSION['admin'])
23 {
24 $id = 'new';
25 $js = 'onclick="openEditor(\'' . $id . '\')"';
26
27 if(Director::$page_path->getLast()->getEndOfPath() === 'accueil')
28 {
29 $new_article = '<article><p id="new"></p>' . "\n" .
30 '<p><a href="' . new URL(['page' => 'article']) . '"><button><img class="action_icon" src="assets/edit.svg">Nouvel article</button></a></p>';
31 }
32 else
33 {
34 $new_article = '<article><p id="new"></p>' . "\n" .
35 '<p id="new-' . $id . '"><a href="#"><button ' . $js . '><img class="action_icon" src="assets/edit.svg">Nouvel article</button></a></p>';
36 }
37
38 $close_js = 'onclick="closeEditor(\'' . $id . '\')"';
39 $close_editor = '<div class="article_admin_zone"><p id="cancel-' . $id . '" class="hidden"><a href="#"><button ' . $close_js . '>Annuler</button></a></p>';
40
41 $submit_js = 'onclick="submitArticle(\'' . $id . '\')"';
42 $submit_article = '<p id="submit-' . $id . '" class="hidden"><a href="#"><button ' . $submit_js . '>Valider</button></a></p></div></article>';
43
44 $new_article_admin_buttons = $close_editor . $submit_article;
45 }
46
47 $this->useChildrenBuilder($node);
48 $content = $this->html;
49
50 ob_start();
51 require $viewFile;
52 $this->html = ob_get_clean(); // pas de concaténation ici, on écrase
53 }
54 }
55} \ No newline at end of file
diff --git a/src/view/HeadBuilder.php b/src/view/HeadBuilder.php
new file mode 100644
index 0000000..c31c930
--- /dev/null
+++ b/src/view/HeadBuilder.php
@@ -0,0 +1,68 @@
1<?php
2// src/view/HeadBuilder.php
3
4use App\Entity\Node;
5
6class HeadBuilder extends AbstractBuilder
7{
8 private bool $stop = false;
9
10 public function __construct(Node $node)
11 {
12 $viewFile = self::VIEWS_PATH . $node->getName() . '.php';
13
14 if(file_exists($viewFile))
15 {
16 // css et js
17 if(!empty($node->getAttributes()))
18 {
19 extract($node->getAttributes());
20 }
21
22 // pages spéciales où on n'assemble pas tout
23 $this->stop = isset($stop) ? $stop : false;
24 $css = '';
25 foreach($css_array as $name)
26 {
27 $css .= '<link rel="stylesheet" href="css/' . $name . '.css">' . "\n";
28 }
29 $js = '';
30 foreach($js_array as $name)
31 {
32 $js .= '<script src="js/' . $name . '.js"></script>' . "\n";
33 }
34
35 // tinymce, nécéssite un script de copie dans composer.json
36 if($_SESSION['admin']){
37 $css .= '<link rel="stylesheet" href="css/tinymce.css">' . "\n";
38 $js .= '<script src="js/tinymce/tinymce.min.js"></script>' . "\n";
39 $js .= '<script src="js/tinymce.js"></script>' . "\n";
40 }
41
42 // titre et description
43 if(!empty($node->getNodeData()->getData()))
44 {
45 extract($node->getNodeData()->getData());
46 }
47
48 // favicon
49 foreach($node->getNodeData()->getImages() as $image)
50 {
51 if(str_contains($image->getFileName(), 'favicon'))
52 {
53 $favicon = rtrim($image->getFilePathMini(), '/');
54 $alt = $image->getAlt();
55 }
56 }
57
58 ob_start();
59 require $viewFile;
60 $this->html .= ob_get_clean();
61 }
62 }
63
64 public function getStop(): bool
65 {
66 return $this->stop;
67 }
68}
diff --git a/src/view/HeaderBuilder.php b/src/view/HeaderBuilder.php
new file mode 100644
index 0000000..252958a
--- /dev/null
+++ b/src/view/HeaderBuilder.php
@@ -0,0 +1,64 @@
1<?php
2// src/view/HeaderBuilder.php
3
4use App\Entity\Node;
5
6class HeaderBuilder extends AbstractBuilder
7{
8 private ?Node $nav = null;
9
10 public function __construct(Node $node)
11 {
12 // nav
13 // n'utilise pas useChildrenBuilder, il faudrait peut-être
14 $children = $node->getChildren();
15 foreach($children as $child)
16 {
17 if($child->getName() === 'nav')
18 {
19 $this->nav = $child;
20 $nav_builder = new NavBuilder($this->nav);
21 $nav = $nav_builder->render();
22 }
23 }
24
25 $viewFile = self::VIEWS_PATH . $node->getName() . '.php';
26
27 if(file_exists($viewFile))
28 {
29 // titre et description
30 if(!empty($node->getNodeData()->getData()))
31 {
32 extract($node->getNodeData()->getData());
33 }
34
35 // attributs, aucun pour l'instant
36 if(!empty($node->getAttributes()))
37 {
38 extract($node->getAttributes());
39 }
40
41 // header logo + réseaux sociaux
42 $targets = ['logo', 'facebook', 'instagram', 'fond_piscine'];
43 $i = 0;
44 foreach($node->getNodeData()->getImages() as $image)
45 {
46 if(str_contains($image->getFileName(), $targets[$i]))
47 {
48 $var = $targets[$i];
49 $$var = rtrim($image->getFilePathMini(), '/');
50 $var .= '_alt'; // ex: logo_alt
51 $$var = $image->getAlt();
52 }
53 $i++;
54 }
55
56 // générer HTML réseaux sociaux
57 //
58
59 ob_start();
60 require $viewFile;
61 $this->html .= ob_get_clean();
62 }
63 }
64} \ No newline at end of file
diff --git a/src/view/LoginBuilder.php b/src/view/LoginBuilder.php
new file mode 100644
index 0000000..ac9910f
--- /dev/null
+++ b/src/view/LoginBuilder.php
@@ -0,0 +1,15 @@
1<?php
2// src/view/LoginBuilder.php
3
4use App\Entity\Node;
5
6class LoginBuilder extends AbstractBuilder
7{
8 public function __construct(Node $node)
9 {
10 global $entityManager;
11
12 // une classe Password ce serait pas mal!!
13 connect($this, $entityManager);
14 }
15}
diff --git a/src/view/MainBuilder.php b/src/view/MainBuilder.php
new file mode 100644
index 0000000..a44570b
--- /dev/null
+++ b/src/view/MainBuilder.php
@@ -0,0 +1,30 @@
1<?php
2// src/view/MainBuilder.php
3
4use App\Entity\Node;
5
6class MainBuilder extends AbstractBuilder
7{
8 public function __construct(Node $node)
9 {
10 $this->html .= "<main>\n";
11
12 if(Director::$page_path->getLast()->getEndOfPath() === 'article'){
13 if($node->getTempChild() == null){
14 $new = new Node;
15 }
16 else{
17 $new = $node->getTempChild();
18 }
19 //$builder_name = $this->snakeToPascalCase($new->getName()) . 'Builder';
20 $builder_name = 'NewBuilder';
21 $builder = new $builder_name($new);
22 $this->html .= $builder->render();
23 }
24 else{
25 $this->useChildrenBuilder($node);
26 }
27
28 $this->html .= "</main>\n";
29 }
30}
diff --git a/src/view/NavBuilder.php b/src/view/NavBuilder.php
new file mode 100644
index 0000000..e7254b1
--- /dev/null
+++ b/src/view/NavBuilder.php
@@ -0,0 +1,61 @@
1<?php
2// src/view/NavBuilder.php
3
4declare(strict_types=1);
5
6use App\Entity\Node;
7use App\Entity\Page;
8
9class NavBuilder extends AbstractBuilder
10{
11 public function __construct(Node $node)
12 {
13 $this->html .= '<nav class="nav_main"><ul>';
14 $this->html .= $this->navMainHTML(Director::$menu_data, Director::$page_path->getArray());
15 $this->html .= '</ul></nav>';
16 }
17
18 private function navMainHTML(Page $nav_data, array $current): string
19 {
20 $nav_html = '';
21 static $level = 0;
22
23 foreach($nav_data->getChildren() as $data)
24 {
25 $class = '';
26 if(isset($current[$level]) && $data->getEndOfPath() === $current[$level]->getEndOfPath()){
27 $class = ' current';
28 }
29
30 if(count($data->getChildren()) > 0) // titre de catégorie
31 {
32 $nav_html .= '<li class="drop-down'. $class . '"><p>' . $data->getPageName() . '</p><ul class="sub-menu">' . "\n";
33 $level++;
34 $nav_html .= $this->navMainHTML($data, $current);
35 $level--;
36 $nav_html .= '</ul></li>' . "\n";
37 }
38 else
39 {
40 $target = '';
41 if(str_starts_with($data->getEndOfPath(), 'http')) // lien vers autre site
42 {
43 $link = $data->getEndOfPath(); // $link = chaine
44 $target = ' target="_blank"';
45 }
46 elseif($data->getEndOfPath() != '') // lien relatif
47 {
48 $link = new URL(['page' => $data->getPagePath()]); // $link = objet
49 }
50 /*else
51 {
52 echo "else page d'accueil" . '<br>';
53 $link = new URL; // page d'accueil
54 }*/
55
56 $nav_html .= '<a href="' . $link . '"' . $target . '><li class="'. $class . '"><p>' . $data->getPageName() . '</p></li></a>' . "\n";
57 }
58 }
59 return $nav_html;
60 }
61} \ No newline at end of file
diff --git a/src/view/NewBuilder.php b/src/view/NewBuilder.php
new file mode 100644
index 0000000..605c174
--- /dev/null
+++ b/src/view/NewBuilder.php
@@ -0,0 +1,93 @@
1<?php
2// src/view/NewBuilder.php
3
4use App\Entity\Node;
5
6class NewBuilder extends AbstractBuilder
7{
8 public function __construct(Node $node)
9 {
10 $viewFile = self::VIEWS_PATH . $node->getName() . '.php';
11
12 if(file_exists($viewFile))
13 {
14 // id (timestamp)
15 if(!empty($node->getAttributes()))
16 {
17 extract($node->getAttributes());
18 }
19
20 // html, date
21 $title = $node->getArticle()->getTitle();
22 $preview = $node->getArticle()->getPreview();
23 $id = $node->getArticleTimestamp();
24 $content = '';
25
26 // page article unique
27 if(Director::$page_path->getLast()->getEndOfPath() === 'article')
28 {
29 $content = $node->getArticle()->getContent();
30 $from_to_button = '<p><a class="link_to_article" href="' . new URL(['page' => 'accueil']) . '"><button>Retour page d\'accueil</button></a></p>';
31 }
32 // page d'accueil (avec des news)
33 else
34 {
35 $from_to_button = '<p><a class="link_to_article" href="' . new URL(['page' => 'article', 'id' => $id]) . '"><button><img class="action_icon" src="assets/book-open.svg">Lire la suite</button></a></p>';
36 }
37
38
39 $date_object = $node->getArticle()->getDateTime(); // class DateTime
40 $date = 'le ' . str_replace(':', 'h', $date_object->format('d-m-Y à H:i'));
41
42 // partage
43 $share_link = new URL(['page' => CURRENT_PAGE], $id);
44 isset($_GET['id']) ? $share_link->addParams(['id' => $_GET['id']]) : '';
45 $share_js = 'onclick="copyInClipBoard(\'' . $share_link . '\')"';
46 $share_button = '<p><a href="' . $share_link . '" ' . $share_js . '><img class="action_icon" src="assets/share.svg"></a></p>' . "\n";
47
48 // modifier un article
49 $admin_buttons = '';
50 if($_SESSION['admin'])
51 {
52 if(Director::$page_path->getLast()->getEndOfPath() === 'article'){
53 $modify_js = 'onclick="openEditor(\'' . $id . '\')"';
54 $modify_article = '<p id="edit-' . $id . '"><a href="#"><img class="action_icon" src="assets/edit.svg" ' . $modify_js . '></a></p>' . "\n";
55
56 $up_button = '<p id="position_up-' . $id . '"></p>' . "\n";
57 $down_button = '<p id="position_down-' . $id . '"></p>' . "\n";
58
59 $delete_js = 'onclick="deleteArticle(\'' . $id . '\', \'' . CURRENT_PAGE . '\')"';
60 $delete_article = '<p id="delete-' . $id . '"><a href="#"><img class="action_icon" src="assets/delete-bin.svg" ' . $delete_js . '></a></p>' . "\n";
61
62 $close_js = 'onclick="closeEditor(\'' . $id . '\')"';
63 $close_editor = '<p id="cancel-' . $id . '" class="hidden"><a href="#"><button ' . $close_js . '>Annuler</button></a></p>';
64
65 $submit_js = 'onclick="submitArticle(\'' . $id . '\')"';
66 $submit_article = '<p id="submit-' . $id . '" class="hidden"><a href="#"><button ' . $submit_js . '>Valider</button></a></p>';
67 }
68 else{
69 $modify_article = '<p id="edit-' . $id . '"></p>' . "\n";
70
71 $up_link = new URL(['page' => CURRENT_PAGE, 'id' => $id, 'action' => 'position_up']);
72 $up_button = '<p id="position_up-' . $id . '"><a href="' . $up_link . '"><img class="action_icon" src="assets/arrow-up.svg"></a></p>' . "\n";
73
74 $down_link = new URL(['page' => CURRENT_PAGE, 'id' => $id, 'action' => 'position_down']);
75 $down_button = '<p id="position_down-' . $id . '"><a href="' . $down_link . '"><img class="action_icon" src="assets/arrow-down.svg"></a></p>' . "\n";
76
77 $delete_js = 'onclick="deleteArticle(\'' . $id . '\')"';
78 $delete_article = '<p id="delete-' . $id . '"><a href="#"><img class="action_icon" src="assets/delete-bin.svg" ' . $delete_js . '></a></p>' . "\n";
79
80 $close_editor = '<p id="cancel-' . $id . '" class="hidden"></p>';
81 $submit_article = '<p id="submit-' . $id . '" class="hidden"></p>';
82
83 $submit_article = '<p id="submit-' . $id . '" class="hidden"></p>';
84 }
85 $admin_buttons = $modify_article . $up_button . $down_button . $delete_article . $close_editor . $submit_article;
86 }
87
88 ob_start();
89 require($viewFile);
90 $this->html .= ob_get_clean();
91 }
92 }
93}
diff --git a/src/view/ViewBuilder.php b/src/view/ViewBuilder.php
new file mode 100644
index 0000000..acac972
--- /dev/null
+++ b/src/view/ViewBuilder.php
@@ -0,0 +1,16 @@
1<?php
2// src/view/ViewBuilder.php
3//
4// appelle les autres Builder
5
6declare(strict_types=1);
7
8use App\Entity\Node;
9
10class ViewBuilder extends AbstractBuilder
11{
12 public function __construct(Node $root_node)
13 {
14 $this->useChildrenBuilder($root_node);
15 }
16}
diff --git a/src/view/password.php b/src/view/password.php
new file mode 100644
index 0000000..0ff717c
--- /dev/null
+++ b/src/view/password.php
@@ -0,0 +1,152 @@
1<?php
2// src/view/password.php
3//
4// ce fichier contient le HTML de deux pages du site:
5// - connexion au mode admin
6// - changement de mot de passe
7//
8// rajouter la page "créationn du mot de passe"?
9
10// insertion du captcha
11ob_start();
12?>
13 <p>Montrez que vous n'êtes pas un robot.<br>
14 <label for="captcha" >Combien font <?= $captcha[0] ?> fois <?= $captcha[1] ?>?</label>
15 <input required type="text" id="captcha" name="captcha" autocomplete="off" size="1">
16 </p>
17<?php
18$captchaHtml = ob_get_clean();
19
20
21// formulaire connexion
22$link = new URL(['page' => 'connexion']);
23isset($_GET['from']) ? $link->addParams(['from' => $_GET['from']]) : '';
24isset($_GET['id']) ? $link->addParams(['id' => $_GET['id']]) : '';
25ob_start();
26?>
27 <form class="connexionFormulaire" method="post" action="<?= $link ?>" >
28 <p><label for="login" >Identifiant (E-mail):</label>
29 <input id="login" type="text" name="login" autofocus required></p>
30 <p><label for="password" >Mot de passe:</label>
31 <input id="password" type="password" name="password" required></p>
32
33 <?= $captchaHtml ?>
34
35 <input type="submit" value="Valider">
36 </form>
37<?php
38$formulaireConnexion = ob_get_clean();
39
40// formulaire création du mot de passe
41ob_start();
42?>
43 <form class="connexionFormulaire" method="post" action="index.php" >
44 <p><label for="login" >Identifiant (e-mail):</label>
45 <input id="login" type="text" name="login" autofocus required></p>
46 <p><label for="password" >Mot de passe:</label>
47 <input id="password" type="password" name="password" required></p>
48
49 <?= $captchaHtml ?>
50
51 <input type="submit" value="Valider">
52 </form>
53<?php
54$formulaireNouveauMDP = ob_get_clean();
55
56// formulaire changement de mot de passe
57$link = new URL(['action' => 'modif_mdp']);
58isset($_GET['from']) ? $link->addParams(['from' => $_GET['from']]) : '';
59isset($_GET['id']) ? $link->addParams(['id' => $_GET['id']]) : '';
60ob_start();
61?>
62 <form class="connexionFormulaire" method="post" action="<?= $link ?>" >
63 <label for="login" >Identifiant (e-mail):</label>
64 <input id="login" type="login" name="login" autofocus required ><br><br>
65 <label for="old_password" >Ancien mot de passe:</label>
66 <input id="old_password" type="password" name="old_password" required ><br><br>
67 <label for="new_password" >Nouveau mot de passe:</label>
68 <input id="new_password" type="password" name="new_password" required autocomplete="off">
69 <br><br>
70 <input type="submit" value="Valider" >
71 <!-- <a href="index.php<?= $from ?>" >
72 <input type="button" value="Annuler">
73 </a> -->
74 </form>
75<?php
76$formulaireModifMDP = ob_get_clean();
77
78// en-tête
79ob_start();
80?>
81<!DOCTYPE html>
82
83<html lang="fr">
84 <head>
85 <meta charset="utf-8">
86 <title><?= $title ?></title>
87
88 <link rel="icon" type="image/png" href="assets/favicon48x48.png">
89 <script src="js/main.js" ></script>
90 <meta name="viewport" content="width=device-width, initial-scale=1.0">
91 <style>
92 body{background-color: #E3F3FF;}
93 #bloc_page{text-align: center;}
94 .avertissement{color: red;}
95 </style>
96 </head>
97
98 <body>
99 <div id="bloc_page" >
100 <h2 class="connexionTitre" ><?= $title ?></h2>
101 <p class="connexionP" ><?= $subHeading ?></p>
102<?php
103$header = ob_get_clean();
104
105
106$error_messages = [
107 'error_non_valid_captcha' => '<p class="avertissement" >Erreur au test anti-robot, veuillez saisir un nombre entier.</p>',
108 'bad_solution_captcha' => '<p class="avertissement" >Erreur au test anti-robot, veuillez réessayer.</p>',
109 'bad_login_or_password' => '<p class="avertissement" >Saisir un Identifiant (e-mail) et un mot de passe.</p>',
110 'bad_password' => '<p class="avertissement" >Mauvais mot de passe, veuillez réessayer.</p>',
111 'forbidden_characters' => '<p class="avertissement" >Caractères interdits: espaces, tabulations, sauts CR/LF.</p>'
112];
113
114$warning_messages = [
115 'message_disconnect' => "<p class='connexionP' ><i>N'oubliez de cliquer sur 'déconnexion' quand vous aurez fini.</i></p>",
116 //'message_cookie' => "<p class='connexionP' style='color: red;'>Ce site utilise un cookie « obligatoire » lorsque vous êtes connecté ainsi que sur cette page.<br>Il sera supprimé à votre déconnexion ou dès que vous aurez quitté le site.</p>",
117 'private_browsing' =>"<p class='connexionP' >Au fait? Vous n'utilisez pas votre propre ordinateur ou téléphone?<br/>
118 Utilisez la navigation privée.</p>"
119];
120
121
122// confirmation modification du mot de passe
123$page = isset($_GET['from']) ? $_GET['from'] : 'accueil';
124$id = isset($_GET['id']) ? ', \'' . $_GET['id'] . '\'' : '';
125$js = "newPassword('" . $page . "'" . $id . ");";
126ob_start();
127?>
128<script><?= $js ?></script>
129<noscript>
130 <p class="avertissement" >Le mot de passe a été modifié<br>
131 <a href="<?= $link ?>" ><button>Retour au site.</button></a></p>
132</noscript>
133<?php
134$alertJSNewPassword = ob_get_clean();
135
136
137// bas de la page
138$link = new URL();
139isset($_GET['from']) ? $link->addParams(['page' => $_GET['from'] ]) : '';
140isset($_GET['id']) ? $link->addParams(['id' => $_GET['id']]) : '';
141ob_start();
142if(isset($_GET['from'])) // exclue la "création du mot de passe"
143{
144?>
145 <p class="connexionP connexionFooter" >
146 <a href="<?= $link ?>" >
147 <button>Retour au site.</button>
148 </a>
149 </p>
150<?php
151}
152$footer = ob_get_clean(); \ No newline at end of file
diff --git a/src/view/templates/article.php b/src/view/templates/article.php
new file mode 100644
index 0000000..d34697e
--- /dev/null
+++ b/src/view/templates/article.php
@@ -0,0 +1,15 @@
1<article>
2 <div class="article_content">
3 <div class="article_title_zone">
4 <h4><?= $title ?></h4>
5 <?= $share_button ?>
6 </div>
7 <div id="<?= $id ?>" class="html_from_editor">
8 <?= $html ?>
9 </div>
10 <div class="article_admin_zone">
11 <?= $admin_buttons ?>
12 </div>
13 </div>
14
15</article> \ No newline at end of file
diff --git a/src/view/templates/blog.php b/src/view/templates/blog.php
new file mode 100644
index 0000000..35cac8b
--- /dev/null
+++ b/src/view/templates/blog.php
@@ -0,0 +1,6 @@
1<section class="blog">
2 <h3><?= $title ?></h3>
3<?= $new_article ?>
4<?= $new_article_admin_buttons ?>
5<?= $content ?>
6</section> \ No newline at end of file
diff --git a/src/view/templates/footer.php b/src/view/templates/footer.php
new file mode 100644
index 0000000..5af7924
--- /dev/null
+++ b/src/view/templates/footer.php
@@ -0,0 +1,15 @@
1 <footer>
2 <div>
3 <p class="contact"><?= $contact_nom ?><br>
4 <?= $adresse ?><br>
5 <a href="mailto:<?= $e_mail ?>"><?= $e_mail ?></a></p>
6<?= $breadcrumb ?>
7 </div>
8 <div class="<?= $empty_admin_zone ?>"></div>
9 <div class="<?= $div_admin ?>">
10<?= $zone_admin ?>
11 </div>
12 </footer>
13 </div>
14 </body>
15</html> \ No newline at end of file
diff --git a/src/view/templates/galery.php b/src/view/templates/galery.php
new file mode 100644
index 0000000..306526f
--- /dev/null
+++ b/src/view/templates/galery.php
@@ -0,0 +1,10 @@
1<section class="galery">
2 <h3><?= $title ?></h3>
3<?= $new_article ?>
4<?= $new_article_admin_buttons ?>
5 <div class="galery_photos">
6 <a><p></p></a>
7<?= $content ?>
8 </div>
9 <script>enableGaleryScroller();</script>
10</section> \ No newline at end of file
diff --git a/src/view/templates/grid.php b/src/view/templates/grid.php
new file mode 100644
index 0000000..a09ed40
--- /dev/null
+++ b/src/view/templates/grid.php
@@ -0,0 +1,8 @@
1<section class="grid">
2 <h3><?= $title ?></h3>
3<?= $new_article ?>
4<?= $new_article_admin_buttons ?>
5 <div class="grid_columns">
6<?= $content ?>
7 </div>
8</section> \ No newline at end of file
diff --git a/src/view/templates/head.php b/src/view/templates/head.php
new file mode 100644
index 0000000..1ebb17e
--- /dev/null
+++ b/src/view/templates/head.php
@@ -0,0 +1,11 @@
1<!DOCTYPE html>
2<html lang="fr">
3 <head>
4 <meta charset="utf-8">
5 <title><?= $title ?></title>
6 <link rel="icon" type="image/png" href="<?= $favicon ?>" alt="<?= $alt ?>">
7 <meta name="description" content="<?= $description ?>">
8 <meta name="viewport" content="width=device-width">
9 <?= $css ?>
10 <?= $js ?>
11 </head> \ No newline at end of file
diff --git a/src/view/templates/header.php b/src/view/templates/header.php
new file mode 100644
index 0000000..fa55cea
--- /dev/null
+++ b/src/view/templates/header.php
@@ -0,0 +1,23 @@
1<body>
2 <div>
3 <header>
4 <div class="empty_nav_zone">
5 <?= $nav ?>
6 </div>
7
8 <div class="header-content">
9 <div class="head_logo">
10 <a href="<?= new URL ?>"><img src="<?= $logo ?>" alt="<?= $logo_alt ?>"></a>
11 </div>
12 <div class="site_title">
13 <a href="<?= new URL ?>"><h1><?= $title ?></h1></a>
14 <h2><?= $description ?></h2>
15 </div>
16 <div class="social">
17 <a href="<?= $facebook_link ?>" target="_blank" rel="noopener noreferrer">
18 <img src="<?= $facebook ?>" alt="<?= $facebook_alt ?>"></a>
19 <a href="<?= $instagram_link ?>" target="_blank" rel="noopener noreferrer">
20 <img src="<?= $instagram ?>" alt="<?= $instagram_alt ?>"></a>
21 </div>
22 </div>
23 </header> \ No newline at end of file
diff --git a/src/view/templates/new.php b/src/view/templates/new.php
new file mode 100644
index 0000000..9dd8969
--- /dev/null
+++ b/src/view/templates/new.php
@@ -0,0 +1,21 @@
1<article>
2 <div class="new_content">
3 <div class="article_title_zone">
4 <h4><?= $title ?></h4>
5 <?= $share_button ?>
6 </div>
7 <div class="new_content_text">
8 <?= $preview ?>
9 </div>
10 <div id="<?= $id ?>" class="article_content_text">
11 <?= $content ?>
12 </div>
13 <div class="under_an_article">
14 <p><img src="assets/calendar.svg"><?= $date ?></p>
15 <?= $from_to_button ?>
16 </div>
17 <div class="article_admin_zone">
18 <?= $admin_buttons ?>
19 </div>
20 </div>
21</article> \ No newline at end of file