aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpolo <ordipolo@gmx.fr>2026-10-23 23:29:59 +0200
committerpolo <ordipolo@gmx.fr>2026-10-23 23:29:59 +0200
commit126a49185229f248eae8c485b3e16e029ebcb353 (patch)
tree7877918b7067103d667d522883e04f0129a1addc
parent5a25142d778eafdec33d10d673bd4abc5fe38d1c (diff)
downloadcms-polo1.tar.gz
cms-polo1.tar.bz2
cms-polo1.zip
audit claude code 1, validation formulaire toujours ok, échappement dans URL, quelques htmlspecialchars manquants, fonction Security::validateURLpolo1
-rw-r--r--src/Controller/HeadFootController.php9
-rw-r--r--src/Controller/MenuAndPathsController.php22
-rw-r--r--src/Controller/ViewController.php3
-rw-r--r--src/Service/FormValidation.php2
-rw-r--r--src/Service/Router.php4
-rw-r--r--src/Service/Security.php8
-rw-r--r--src/Service/URL.php27
-rw-r--r--src/View/HeaderBuilder.php4
-rw-r--r--src/View/NewBuilder.php2
-rw-r--r--à faire.txt4
10 files changed, 44 insertions, 41 deletions
diff --git a/src/Controller/HeadFootController.php b/src/Controller/HeadFootController.php
index e39c07b..2bf210e 100644
--- a/src/Controller/HeadFootController.php
+++ b/src/Controller/HeadFootController.php
@@ -28,14 +28,19 @@ class HeadFootController
28 $node_data = $model->getNode()->getNodeData(); 28 $node_data = $model->getNode()->getNodeData();
29 29
30 // liens réseaux sociaux 30 // liens réseaux sociaux
31 $link = trim($json['new_text'] ?? ''); // trim n'aime pas recevoir un NULL
31 if(in_array($params_array[1], NodeData::$social_networks)){ 32 if(in_array($params_array[1], NodeData::$social_networks)){
33 if($link !== '' && !Security::validateURL($link)){
34 return new JsonResponse(['success' => false, 'error' => 'invalid_url']);
35 }
36
32 $social = $node_data->getData()['social'] ?? []; 37 $social = $node_data->getData()['social'] ?? [];
33 $social[$params_array[1]] = $json['new_text']; 38 $social[$params_array[1]] = $link;
34 $node_data->updateData('social', $social); 39 $node_data->updateData('social', $social);
35 } 40 }
36 // autres textes 41 // autres textes
37 else{ 42 else{
38 $node_data->updateData($params_array[1], $json['new_text']); // $params_array[1] n'est pas contrôlé 43 $node_data->updateData($params_array[1], $link); // $params_array[1] n'est pas contrôlé
39 } 44 }
40 45
41 $entityManager->flush(); 46 $entityManager->flush();
diff --git a/src/Controller/MenuAndPathsController.php b/src/Controller/MenuAndPathsController.php
index 1916f06..aa283b6 100644
--- a/src/Controller/MenuAndPathsController.php
+++ b/src/Controller/MenuAndPathsController.php
@@ -6,31 +6,33 @@ declare(strict_types=1);
6namespace App\Controller; 6namespace App\Controller;
7 7
8use Doctrine\ORM\EntityManager; 8use Doctrine\ORM\EntityManager;
9use Symfony\Component\HttpFoundation\Request;
9use Symfony\Component\HttpFoundation\JsonResponse; 10use Symfony\Component\HttpFoundation\JsonResponse;
10use Symfony\Component\HttpFoundation\RedirectResponse; 11use Symfony\Component\HttpFoundation\RedirectResponse;
11use App\Entity\Page; 12use App\Entity\Page;
12use App\Model\Menu; 13use App\Model\Menu;
13use App\Model\Model; 14use App\Model\Model;
14use App\Service\URL; 15use App\Service\URL;
16use App\Service\Security;
15use App\View\NavBuilder; 17use App\View\NavBuilder;
16use App\View\MenuBuilder; 18use App\View\MenuBuilder;
17 19
18class MenuAndPathsController 20class MenuAndPathsController
19{ 21{
20 static public function newUrlMenuEntry(EntityManager $entityManager): RedirectResponse 22 static public function newUrlMenuEntry(EntityManager $entityManager, Request $request): RedirectResponse
21 { 23 {
22 Model::$menu = new Menu($entityManager); 24 Model::$menu = new Menu($entityManager);
23 $previous_page = Model::$menu->findPageById((int)$_POST["location"]); // (int) à cause de declare(strict_types=1); 25 $previous_page = Model::$menu->findPageById((int)$_POST["location"]); // (int) à cause de declare(strict_types=1);
24 $parent = $previous_page->getParent(); 26 $parent = $previous_page->getParent();
25 27
26 $url_input = trim($_POST["url_input"]); // faire htmlspecialchars à l'affichage 28 $url_input = trim($request->request->get("url_input")); // faire htmlspecialchars à l'affichage
27 if(!filter_var($url_input, FILTER_VALIDATE_URL) || !str_starts_with($url_input, 'http')){ 29 if(!Security::validateURL($url_input)){
28 // utiliser une flash error 30 // utiliser une flash error
29 return new RedirectResponse((string)new URL(['page' => $_GET['from'], 'error' => 'invalide_url'])); 31 return new RedirectResponse((string)new URL(['page' => $request->query->get('from'), 'error' => 'invalide_url']));
30 } 32 }
31 33
32 $page = new Page( 34 $page = new Page(
33 trim(htmlspecialchars($_POST["label_input"])), 35 trim(htmlspecialchars($request->request->get("label_input"))),
34 $url_input, '', 36 $url_input, '',
35 true, true, false, 37 true, true, false,
36 $previous_page->getPosition(), 38 $previous_page->getPosition(),
@@ -47,7 +49,7 @@ class MenuAndPathsController
47 49
48 $entityManager->persist($page); 50 $entityManager->persist($page);
49 $entityManager->flush(); 51 $entityManager->flush();
50 return new RedirectResponse((string)new URL(['page' => $_GET['from']])); 52 return new RedirectResponse((string)new URL(['page' => $request->query->get('from')]));
51 } 53 }
52 54
53 // on pourrait utiliser FormValidation ici 55 // on pourrait utiliser FormValidation ici
@@ -62,7 +64,7 @@ class MenuAndPathsController
62 elseif(!in_array($json['field'], ['url_name', 'url_content'])){ 64 elseif(!in_array($json['field'], ['url_name', 'url_content'])){
63 return new JsonResponse(['success' => false, 'message' => "champ invalide"]); 65 return new JsonResponse(['success' => false, 'message' => "champ invalide"]);
64 } 66 }
65 elseif($json['field'] === 'url_content' && (!filter_var($url_data, FILTER_VALIDATE_URL) || !str_starts_with($url_data, 'http'))){ 67 elseif($json['field'] === 'url_content' && !Security::validateURL($url_data)){
66 return new JsonResponse(['success' => false, 'message' => "la chaîne envoyée n'est pas une URL valide"]); 68 return new JsonResponse(['success' => false, 'message' => "la chaîne envoyée n'est pas une URL valide"]);
67 } 69 }
68 else{ 70 else{
@@ -77,10 +79,10 @@ class MenuAndPathsController
77 } 79 }
78 } 80 }
79 81
80 static public function deleteUrlMenuEntry(EntityManager $entityManager): RedirectResponse 82 static public function deleteUrlMenuEntry(EntityManager $entityManager, Request $request): RedirectResponse
81 { 83 {
82 Model::$menu = new Menu($entityManager); 84 Model::$menu = new Menu($entityManager);
83 $page = Model::$menu->findPageById((int)$_POST["delete"]); 85 $page = Model::$menu->findPageById((int)$request->request->get("delete"));
84 $parent = $page->getParent(); 86 $parent = $page->getParent();
85 if($parent == null){ 87 if($parent == null){
86 $parent = Model::$menu; 88 $parent = Model::$menu;
@@ -91,7 +93,7 @@ class MenuAndPathsController
91 93
92 $entityManager->remove($page); // suppression en BDD 94 $entityManager->remove($page); // suppression en BDD
93 $entityManager->flush(); 95 $entityManager->flush();
94 return new RedirectResponse((string)new URL(['page' => $_GET['from']])); 96 return new RedirectResponse((string)new URL(['page' => $request->query->get('from')]));
95 } 97 }
96 98
97 static public function MoveOneLevelUp(EntityManager $entityManager, array $json): JsonResponse 99 static public function MoveOneLevelUp(EntityManager $entityManager, array $json): JsonResponse
diff --git a/src/Controller/ViewController.php b/src/Controller/ViewController.php
index 1f13861..5be4ffd 100644
--- a/src/Controller/ViewController.php
+++ b/src/Controller/ViewController.php
@@ -38,7 +38,8 @@ class ViewController{
38 else{ 38 else{
39 // mode création d'article 39 // mode création d'article
40 // l'id du bloc et 'from=' sont vérifiés dans ArticleController::editorSubmit 40 // l'id du bloc et 'from=' sont vérifiés dans ArticleController::editorSubmit
41 if($request->query->get('id')[0] === 'n' && $request->query->has('from') && !empty($request->query->get('from'))){ 41 if(preg_match('/^n\d+\z/', $request->query->get('id')) // débute par 'n' puis des chiffres, \z au lieu de $ pour éviter un saut de ligne à la fin
42 && $request->query->has('from') && !empty($request->query->get('from'))){
42 NewBuilder::$new_article_mode = true; 43 NewBuilder::$new_article_mode = true;
43 } 44 }
44 } 45 }
diff --git a/src/Service/FormValidation.php b/src/Service/FormValidation.php
index d91aa12..9f4564f 100644
--- a/src/Service/FormValidation.php
+++ b/src/Service/FormValidation.php
@@ -46,7 +46,7 @@ class FormValidation
46 public function validate(): bool 46 public function validate(): bool
47 { 47 {
48 $this->error = ($this->callStrategy)(); // appel de la callback 48 $this->error = ($this->callStrategy)(); // appel de la callback
49 return empty($this->errors); 49 return $this->error === null;
50 } 50 }
51 51
52 public function getError(): string 52 public function getError(): string
diff --git a/src/Service/Router.php b/src/Service/Router.php
index 98cecc5..3055099 100644
--- a/src/Service/Router.php
+++ b/src/Service/Router.php
@@ -347,11 +347,11 @@ class Router{
347 /* -- page Menu et chemins -- */ 347 /* -- page Menu et chemins -- */
348 // création d'une entrée de menu avec une URL 348 // création d'une entrée de menu avec une URL
349 elseif($this->request->request->has("label_input") && $this->request->request->has("url_input") && $this->request->request->has("location")){ 349 elseif($this->request->request->has("label_input") && $this->request->request->has("url_input") && $this->request->request->has("location")){
350 return MenuAndPathsController::newUrlMenuEntry($this->entityManager); 350 return MenuAndPathsController::newUrlMenuEntry($this->entityManager, $this->request);
351 } 351 }
352 // suppression d'une entrée de menu avec une URL 352 // suppression d'une entrée de menu avec une URL
353 elseif($this->request->request->has('delete') && $this->request->request->has('x') && $this->request->request->has('y')){ // 2 params x et y sont là parce qu'on a cliqué sur une image 353 elseif($this->request->request->has('delete') && $this->request->request->has('x') && $this->request->request->has('y')){ // 2 params x et y sont là parce qu'on a cliqué sur une image
354 return MenuAndPathsController::deleteUrlMenuEntry($this->entityManager); 354 return MenuAndPathsController::deleteUrlMenuEntry($this->entityManager, $this->request);
355 } 355 }
356 356
357 357
diff --git a/src/Service/Security.php b/src/Service/Security.php
index 0fbd8b7..bb04535 100644
--- a/src/Service/Security.php
+++ b/src/Service/Security.php
@@ -27,6 +27,12 @@ class Security
27 return trim(htmLawed($chaine, self::$configHtmLawed, self::$specHtmLawed)); 27 return trim(htmLawed($chaine, self::$configHtmLawed, self::$specHtmLawed));
28 } 28 }
29 29
30 public static function validateURL(string $link): bool
31 {
32 return filter_var($link, FILTER_VALIDATE_URL) !== false // c'est une URL?
33 && in_array(strtolower(parse_url($link, PHP_URL_SCHEME) ?? ''), ['http', 'https'], true); // PHP_URL_SCHEME pour récupérer le protocole
34 }
35
30 public static function secureFileName(string $chaine): string 36 public static function secureFileName(string $chaine): string
31 { 37 {
32 // sécuriser un nom avec chemin avec basename? 38 // sécuriser un nom avec chemin avec basename?
@@ -83,7 +89,7 @@ class Security
83} 89}
84 90
85// lien sans http:// 91// lien sans http://
86function fixLinks($data) 92function fixLinks(string $data): string
87{ 93{
88 // 1/ 94 // 1/
89 // 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:// 95 // 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://
diff --git a/src/Service/URL.php b/src/Service/URL.php
index 035f572..e6f986c 100644
--- a/src/Service/URL.php
+++ b/src/Service/URL.php
@@ -58,33 +58,20 @@ class URL implements \Stringable
58 public function setAnchor(string $anchor = ''): void 58 public function setAnchor(string $anchor = ''): void
59 { 59 {
60 if($anchor != ''){ 60 if($anchor != ''){
61 $this->anchor = '#' . ltrim($anchor, '#'); 61 $this->anchor = '#' . rawurlencode(ltrim($anchor, '#'));// rawurlencode échappe les mêmes caractères
62 } 62 }
63 else{ 63 else{
64 $this->anchor = ''; 64 $this->anchor = '';
65 } 65 }
66 } 66 }
67 67
68 private function makeParams(): string
69 {
70 $output = '';
71 $first = true;
72
73 foreach($this->params as $key => $value) {
74 if($first){
75 $output .= '?';
76 $first = false;
77 }
78 else{
79 $output .= '&';
80 }
81 $output .= $key . '=' . $value;
82 }
83 return $output;
84 }
85
86 public function __toString(): string 68 public function __toString(): string
87 { 69 {
88 return self::$protocol . self::$host . self::$port . self::$path . $this->makeParams() . $this->anchor; 70 return self::$protocol . self::$host . self::$port . self::$path
71 . ($this->params ? '?' . http_build_query($this->params) : '')
72 . $this->anchor;
73 // http_build_query change un tableau comme ['key1' => 'value 1', 'key2' => 'value 2']
74 // en chaine: key1=value+1&key2=value+2
75 // les caractères ", ', <, >, &, #, = et espaces sont échappés (comme avec rawurlencode pour les ancres)
89 } 76 }
90} \ No newline at end of file 77} \ No newline at end of file
diff --git a/src/View/HeaderBuilder.php b/src/View/HeaderBuilder.php
index 14f5ffa..307c0bb 100644
--- a/src/View/HeaderBuilder.php
+++ b/src/View/HeaderBuilder.php
@@ -85,7 +85,7 @@ class HeaderBuilder extends AbstractBuilder
85 // boucle sur la liste complète de réseaux sociaux 85 // boucle sur la liste complète de réseaux sociaux
86 foreach(NodeData::$social_networks as $network){ 86 foreach(NodeData::$social_networks as $network){
87 $checked = (isset($social_show[$network]) && $social_show[$network]) ? 'checked' : ''; 87 $checked = (isset($social_show[$network]) && $social_show[$network]) ? 'checked' : '';
88 $href = (isset($social[$network]) && $social[$network] !== '') ? 'href="' . $social[$network] . '"' : ''; 88 $href = (isset($social[$network]) && $social[$network] !== '') ? 'href="' . htmlspecialchars($social[$network]) . '"' : '';
89 89
90 $social_networks .= '<div id="header_' . $network . '"> 90 $social_networks .= '<div id="header_' . $network . '">
91 <input type="checkbox" onclick="checkSocialNetwork(\'header_' . $network . '\')" ' . $checked . '> 91 <input type="checkbox" onclick="checkSocialNetwork(\'header_' . $network . '\')" ' . $checked . '>
@@ -112,7 +112,7 @@ class HeaderBuilder extends AbstractBuilder
112 // boucle sur les réseaux sociaux "activés" 112 // boucle sur les réseaux sociaux "activés"
113 foreach(array_keys($social_show) as $network){ 113 foreach(array_keys($social_show) as $network){
114 if($social_show[$network]){ 114 if($social_show[$network]){
115 $href = (isset($social[$network]) && $social[$network] !== '') ? 'href="' . $social[$network] . '"' : ''; 115 $href = (isset($social[$network]) && $social[$network] !== '') ? 'href="' . htmlspecialchars($social[$network]) . '"' : '';
116 $social_networks .= '<div id="header_' . $network . '"> 116 $social_networks .= '<div id="header_' . $network . '">
117 <a ' . $href . ' target="_blank" rel="noopener noreferrer">' 117 <a ' . $href . ' target="_blank" rel="noopener noreferrer">'
118 . $this->insertSVG(self::ICON_PATH . $network . '.svg', ['id' => 'header_' . $network . '_content','class' => 'svg_fill_red']) 118 . $this->insertSVG(self::ICON_PATH . $network . '.svg', ['id' => 'header_' . $network . '_content','class' => 'svg_fill_red'])
diff --git a/src/View/NewBuilder.php b/src/View/NewBuilder.php
index 598bb1c..bcc0b6e 100644
--- a/src/View/NewBuilder.php
+++ b/src/View/NewBuilder.php
@@ -118,7 +118,7 @@ class NewBuilder extends AbstractBuilder
118 if(self::$new_article_mode){ 118 if(self::$new_article_mode){
119 $delete_article = ''; 119 $delete_article = '';
120 // valider la création d'un nouvel article 120 // valider la création d'un nouvel article
121 $submit_js = 'onclick="submitArticle(\'' . $_GET['id'] . '\')"'; 121 $submit_js = 'onclick="submitArticle(\'' . $id . '\')"';
122 $submit_article = '<p id="save-' . $id . '"><img class="action_icon delete_button" src="assets/save.svg" ' . $submit_js . '></p>' . "\n"; 122 $submit_article = '<p id="save-' . $id . '"><img class="action_icon delete_button" src="assets/save.svg" ' . $submit_js . '></p>' . "\n";
123 } 123 }
124 // mode article existant 124 // mode article existant
diff --git a/à faire.txt b/à faire.txt
index 9ad086c..9c817d9 100644
--- a/à faire.txt
+++ b/à faire.txt
@@ -1,4 +1,6 @@
1retester toutes les fonctionnalités 1les id dans NewBuilder sont pas terribles, les $_GET non plus
2
3à l'envoie d'un e-mail, envoyer un nouveau captcha à javascript et modifier le html
2 4
3dans Backup, détecter zlib pour la compression gzip optionnelle 5dans Backup, détecter zlib pour la compression gzip optionnelle
4- le serveur stocke avec gzip si dispo parce qu'il est peut probable qu'il perde un jour une fonctionnalité 6- le serveur stocke avec gzip si dispo parce qu'il est peut probable qu'il perde un jour une fonctionnalité