diff options
Diffstat (limited to 'src/Service')
| -rw-r--r-- | src/Service/Backup.php | 272 | ||||
| -rw-r--r-- | src/Service/Captcha.php | 57 | ||||
| -rw-r--r-- | src/Service/Config.php | 90 | ||||
| -rw-r--r-- | src/Service/EmailService.php | 106 | ||||
| -rw-r--r-- | src/Service/FormValidation.php | 216 | ||||
| -rw-r--r-- | src/Service/Installation.php | 109 | ||||
| -rw-r--r-- | src/Service/Router.php | 393 | ||||
| -rw-r--r-- | src/Service/Security.php | 112 | ||||
| -rw-r--r-- | src/Service/URL.php | 90 | ||||
| -rw-r--r-- | src/Service/User.php | 25 | ||||
| -rw-r--r-- | src/Service/session.php | 94 |
11 files changed, 1564 insertions, 0 deletions
diff --git a/src/Service/Backup.php b/src/Service/Backup.php new file mode 100644 index 0000000..546e81e --- /dev/null +++ b/src/Service/Backup.php | |||
| @@ -0,0 +1,272 @@ | |||
| 1 | <?php | ||
| 2 | // src/Service/Backup.php | ||
| 3 | |||
| 4 | declare(strict_types=1); | ||
| 5 | |||
| 6 | namespace App\Service; | ||
| 7 | |||
| 8 | use Doctrine\ORM\EntityManager; | ||
| 9 | use Symfony\Component\Process\Process; // protection injection dans le shell | ||
| 10 | |||
| 11 | class Backup | ||
| 12 | { | ||
| 13 | static public string $backup_dir = '../var/backups'; | ||
| 14 | static private int $amount_to_keep = 20; | ||
| 15 | |||
| 16 | static public function mySQLdump(EntityManager $entityManager, string $type): string | ||
| 17 | { | ||
| 18 | $date = new \DateTime; | ||
| 19 | $file_path = self::$backup_dir . '/' . Config::$database . '_' . $date->format('Y-m-d') . '_' . $type . '.sql'; | ||
| 20 | |||
| 21 | // les versions de mysql sont comme ci: 8.0.36 | ||
| 22 | // celles de mariadb sont comme ça: 10.11.6-MariaDB | ||
| 23 | $version = $entityManager->getConnection()->fetchOne('SELECT VERSION()'); | ||
| 24 | $engine = stripos($version, 'mariadb') !== false ? 'mariadb-dump' : 'mysqldump'; | ||
| 25 | |||
| 26 | $tmp = tempnam('../var', 'tmp_db_codes_'); // crée un fichier avec un nom aléatoire et des droits 600 (concurrence) | ||
| 27 | // attention, si le dossier de destination n'est pas disponible, le fichier est placé avec les fichiers temporaires | ||
| 28 | file_put_contents($tmp, | ||
| 29 | "[client]\n | ||
| 30 | user=" . Config::$user . "\n | ||
| 31 | password=" . Config::$password . "\n | ||
| 32 | host=" . Config::$db_host . "\n"); | ||
| 33 | |||
| 34 | $command = new Process([ | ||
| 35 | $engine, | ||
| 36 | '--defaults-extra-file=' . $tmp, // pour ne pas enregistrer les codes dans l'historique de la console ou dans les processus de l'OS | ||
| 37 | '--single-transaction', | ||
| 38 | '--quick', // évite d'exploser la RAM si beaucoup de données | ||
| 39 | '--result-file=' . $file_path, | ||
| 40 | Config::$database | ||
| 41 | ]); | ||
| 42 | |||
| 43 | try{ | ||
| 44 | // unlink et chmod permettent que le serveur et l'utilisateur appelant bin/mysqldump.php réussissent | ||
| 45 | if(file_exists($file_path)){ | ||
| 46 | unlink($file_path); | ||
| 47 | } | ||
| 48 | $command->mustRun(); // comme run() mais lance une ProcessFailedException | ||
| 49 | |||
| 50 | //$file_path = self::gzipCompress($file_path); // '.gz' ajouté à la fin | ||
| 51 | |||
| 52 | chmod($file_path, 0666); | ||
| 53 | return $file_path; | ||
| 54 | } | ||
| 55 | finally{ | ||
| 56 | // exécuté même quand situé après "return" | ||
| 57 | unlink($tmp); | ||
| 58 | self::cleanBackups(); | ||
| 59 | } | ||
| 60 | } | ||
| 61 | |||
| 62 | // compression gzip (gros gain de place sur le serveur), nécessite l'extension zlib | ||
| 63 | static public function gzipCompress(string $file_path): string | ||
| 64 | { | ||
| 65 | try{ | ||
| 66 | file_put_contents( | ||
| 67 | $file_path . '.gz', | ||
| 68 | gzencode(file_get_contents($file_path), 5), // plus rapide que 9 et taille identique d'après mes essais | ||
| 69 | ); | ||
| 70 | unlink($file_path); | ||
| 71 | $file_path .= '.gz'; | ||
| 72 | } | ||
| 73 | finally{ | ||
| 74 | return $file_path; | ||
| 75 | } | ||
| 76 | } | ||
| 77 | |||
| 78 | static public function getBackupList(): array | ||
| 79 | { | ||
| 80 | $files = scandir(Backup::$backup_dir); // affiche un warning si échoue (à cacher en prod) | ||
| 81 | if(!$files){ | ||
| 82 | throw new \RuntimeException("Le serveur a rencontré une erreur:<br>Accès aux backups impossible faute de permissions."); | ||
| 83 | } | ||
| 84 | |||
| 85 | $backup_array = []; | ||
| 86 | foreach($files as $file){ | ||
| 87 | if($file[0] === '.'){ | ||
| 88 | continue; | ||
| 89 | } | ||
| 90 | $backup_array[] = $file; | ||
| 91 | } | ||
| 92 | return $backup_array; | ||
| 93 | } | ||
| 94 | |||
| 95 | static public function cleanBackups(): void | ||
| 96 | { | ||
| 97 | $files = glob(self::$backup_dir . '/*.sql'); | ||
| 98 | //usort($files, fn($a, $b) => filemtime($b) <=> filemtime($a)); // filemtime = date de dernière modification | ||
| 99 | arsort($files); | ||
| 100 | |||
| 101 | // tri par nom de BDD puis par date | ||
| 102 | $sorted_files = []; | ||
| 103 | $list_by_database = []; // pour le nettoyage 2 | ||
| 104 | foreach($files as $file){ | ||
| 105 | $exploded = explode('_', basename($file)); | ||
| 106 | $sorted_files[$exploded[0]][$exploded[1]][] = $file; | ||
| 107 | $list_by_database[$exploded[0]][] = $file; | ||
| 108 | } | ||
| 109 | |||
| 110 | $date = new \DateTime; | ||
| 111 | foreach($sorted_files as $db_name => $from_one_database){ | ||
| 112 | // on garde une "quantité à garder" par BDD | ||
| 113 | if(count($from_one_database) > self::$amount_to_keep){ | ||
| 114 | // nettoyage 1 | ||
| 115 | foreach($from_one_database as $date_key => $with_same_date){ | ||
| 116 | // pas touche à aujourd'hui | ||
| 117 | if($date_key != $date->format('Y-m-d')){ | ||
| 118 | self::cleanBackupsByPriority($with_same_date); | ||
| 119 | } | ||
| 120 | } | ||
| 121 | // nettoyage 2 | ||
| 122 | $files_to_delete = array_slice($list_by_database[$db_name], self::$amount_to_keep); | ||
| 123 | foreach($files_to_delete as $file){ | ||
| 124 | unlink($file); | ||
| 125 | } | ||
| 126 | } | ||
| 127 | } | ||
| 128 | } | ||
| 129 | |||
| 130 | // conserver un seul backup par jour choisi dans cet ordre de préférence: console => before-restore => download => auto | ||
| 131 | // cet ordre correspond à: volonté de l'utilisateur => état du jour avant changement => volonté de changement => automatique sans contrôle | ||
| 132 | static private function cleanBackupsByPriority(array $files): void | ||
| 133 | { | ||
| 134 | $priorities = [ | ||
| 135 | 'console' => 1, | ||
| 136 | 'before-restore' => 2, | ||
| 137 | 'uploaded' => 3, | ||
| 138 | 'auto' => 4, | ||
| 139 | ]; | ||
| 140 | $best_priority = PHP_INT_MAX; | ||
| 141 | |||
| 142 | // recherche du fichier à conserver | ||
| 143 | $to_keep = null; | ||
| 144 | foreach($files as $file){ | ||
| 145 | foreach($priorities as $keyword => $priority){ | ||
| 146 | if(str_contains(basename($file), $keyword) && $priority < $best_priority){ | ||
| 147 | $best_priority = $priority; | ||
| 148 | $to_keep = $file; | ||
| 149 | break; | ||
| 150 | } | ||
| 151 | } | ||
| 152 | } | ||
| 153 | // suppression des autres | ||
| 154 | foreach($files as $file){ | ||
| 155 | if($file !== $to_keep){ | ||
| 156 | unlink($file); | ||
| 157 | } | ||
| 158 | } | ||
| 159 | } | ||
| 160 | |||
| 161 | static public function restoreDatabase(EntityManager $entityManager, string $file_name, array $exclusion_list = []): void | ||
| 162 | { | ||
| 163 | // création d'un backup de sécurité non écrasable | ||
| 164 | $date = new \DateTime; | ||
| 165 | if(!file_exists(self::$backup_dir . '/' . Config::$database . '_' . $date->format('Y-m-d') . '_before-restore.sql')){ | ||
| 166 | Backup::mySQLdump($entityManager, 'before-restore'); | ||
| 167 | } | ||
| 168 | |||
| 169 | $version = $entityManager->getConnection()->fetchOne('SELECT VERSION()'); | ||
| 170 | $engine = stripos($version, 'mariadb') !== false ? 'mariadb' : 'mysql'; | ||
| 171 | |||
| 172 | $tables = self::getDBListOfTables($entityManager, $exclusion_list); // choisir les tables à effacer et restaurer, exclure "user" | ||
| 173 | |||
| 174 | $tmp_db_codes = tempnam('../var', 'tmp_db_codes_'); // crée un fichier avec un nom aléatoire et des droits 600 (concurrence) | ||
| 175 | file_put_contents($tmp_db_codes, | ||
| 176 | "[client]\n | ||
| 177 | user=" . Config::$user . "\n | ||
| 178 | password=" . Config::$password . "\n | ||
| 179 | host=" . Config::$db_host . "\n"); | ||
| 180 | |||
| 181 | //$file_name = self::gzipExtract($file_name); // '.gz' ajouté à la fin | ||
| 182 | |||
| 183 | $command = new Process([ | ||
| 184 | $engine, // mariadb ou mysql | ||
| 185 | '--defaults-extra-file=' . $tmp_db_codes, // pour ne pas enregistrer les codes dans l'historique de la console ou dans les processus de l'OS | ||
| 186 | Config::$database | ||
| 187 | ]); | ||
| 188 | $command->setInput(file_get_contents(Backup::$backup_dir . '/' . $file_name)); // l'entrée < | ||
| 189 | |||
| 190 | // utiliser une liste de tables à exclure dynamique | ||
| 191 | try{ | ||
| 192 | // tout effacer | ||
| 193 | $tables_with_backquotes = array_map(fn($t) => '`' . $t . '`', $tables); | ||
| 194 | if(!empty($tables_with_backquotes)){ | ||
| 195 | $sql = "SET FOREIGN_KEY_CHECKS=0; DROP TABLE " . implode(', ', $tables_with_backquotes) . "; SET FOREIGN_KEY_CHECKS=1;"; | ||
| 196 | $entityManager->getConnection()->executeStatement($sql); | ||
| 197 | } | ||
| 198 | |||
| 199 | // copie des tables à ne pas restaurer | ||
| 200 | foreach($exclusion_list as $excluded){ | ||
| 201 | $entityManager->getConnection()->executeStatement('RENAME TABLE `' . TABLE_PREFIX . $excluded . '` TO `' . TABLE_PREFIX . $excluded . '_dont_touch`;'); | ||
| 202 | } | ||
| 203 | |||
| 204 | // restaurer | ||
| 205 | $command->mustRun(); // comme run() mais lance une ProcessFailedException | ||
| 206 | |||
| 207 | // remettre table user comme avant | ||
| 208 | foreach($exclusion_list as $excluded){ | ||
| 209 | $entityManager->getConnection()->executeStatement('DROP TABLE `' . TABLE_PREFIX . $excluded . '`;'); | ||
| 210 | $entityManager->getConnection()->executeStatement('RENAME TABLE `' . TABLE_PREFIX . $excluded . '_dont_touch` TO `' . TABLE_PREFIX . $excluded . '`;'); | ||
| 211 | } | ||
| 212 | } | ||
| 213 | finally{ | ||
| 214 | // exécuté même quand situé après "return" | ||
| 215 | unlink($tmp_db_codes); | ||
| 216 | } | ||
| 217 | } | ||
| 218 | |||
| 219 | static private function getDBListOfTables(EntityManager $entityManager, array $exclusion_list = []): array | ||
| 220 | { | ||
| 221 | $tables = $entityManager->getConnection()->createSchemaManager()->listTableNames(); | ||
| 222 | foreach($tables as $key => $elem){ | ||
| 223 | foreach($exclusion_list as $excluded){ | ||
| 224 | if(!is_string($excluded)){ // cas pas du tout censé arriver! | ||
| 225 | throw new \LogicException("Un nom de table doit etre une chaîne de caractères."); | ||
| 226 | } | ||
| 227 | if($elem === TABLE_PREFIX . $excluded){ | ||
| 228 | unset($tables[$key]); | ||
| 229 | break; // petite optimisation | ||
| 230 | } | ||
| 231 | } | ||
| 232 | } | ||
| 233 | return $tables; | ||
| 234 | } | ||
| 235 | |||
| 236 | static public function createZip(string $zip_name, array $source_directories, array $pattern_to_target_in_user_data = []): string | ||
| 237 | { | ||
| 238 | $file_path = '../var/' . $zip_name; | ||
| 239 | $Zip = new \ZipArchive(); | ||
| 240 | |||
| 241 | if($Zip->open($file_path, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== TRUE){ // ou ZipArchive::FL_OVERWRITE, à comparer à ZipArchive::OVERWRITE | ||
| 242 | throw new \RuntimeException("Création ou ouverture du fichier demandé impossible à cause d'une erreur de permissions sur le serveur."); | ||
| 243 | } | ||
| 244 | |||
| 245 | // recherche récursive dans les dossiers dans $source_directories, c'est comme le paramètre "-r" dans la console | ||
| 246 | $counter = 0; | ||
| 247 | foreach($source_directories as $path){ | ||
| 248 | $directory = new \RecursiveDirectoryIterator($path); | ||
| 249 | $iterator = new \RecursiveIteratorIterator($directory); | ||
| 250 | |||
| 251 | foreach($iterator as $info){ | ||
| 252 | if($info->getFilename() != "." && $info->getFilename() != ".." && $info->getFilename() != '.htaccess'){ // chemins inutiles . et .. et le .htaccess | ||
| 253 | $Zip->addGlob($info->getPathname(), 0, array('')); | ||
| 254 | $counter++; | ||
| 255 | } | ||
| 256 | } | ||
| 257 | } | ||
| 258 | // recherche à la racine avec des pattern de noms de fichiers (optionnel) | ||
| 259 | foreach($pattern_to_target_in_user_data as $one_pattern){ | ||
| 260 | $Zip->addGlob($one_pattern, 0, array('')); | ||
| 261 | } | ||
| 262 | |||
| 263 | $Zip->close(); | ||
| 264 | if($counter > 0){ | ||
| 265 | //chmod($file_path, 0666); | ||
| 266 | return $zip_name; | ||
| 267 | } | ||
| 268 | else{ | ||
| 269 | throw new \RuntimeException("Téléchargement des fichiers impossible, aucun fichier n'a été trouvé sur le serveur."); | ||
| 270 | } | ||
| 271 | } | ||
| 272 | } \ No newline at end of file | ||
diff --git a/src/Service/Captcha.php b/src/Service/Captcha.php new file mode 100644 index 0000000..28c7f71 --- /dev/null +++ b/src/Service/Captcha.php | |||
| @@ -0,0 +1,57 @@ | |||
| 1 | <?php | ||
| 2 | // src/Service/Captcha.php | ||
| 3 | // | ||
| 4 | // la solution est stockée dans une unique variable $_SESSION['captcha'] | ||
| 5 | // => on pourrait appliquer le pattern "singleton" (justification = le captcha devient une sorte de ressource partagée) | ||
| 6 | |||
| 7 | declare(strict_types=1); | ||
| 8 | |||
| 9 | namespace App\Service; | ||
| 10 | |||
| 11 | class Captcha | ||
| 12 | { | ||
| 13 | private int $a; | ||
| 14 | private int $b; | ||
| 15 | |||
| 16 | public function __construct(){ | ||
| 17 | $this->a = rand(2, 9); | ||
| 18 | $this->b = rand(2, 9); | ||
| 19 | } | ||
| 20 | |||
| 21 | public function getA(): string | ||
| 22 | { | ||
| 23 | return $this->toLettersFrench($this->a); | ||
| 24 | } | ||
| 25 | public function getB(): string | ||
| 26 | { | ||
| 27 | return $this->toLettersFrench($this->b); | ||
| 28 | } | ||
| 29 | public function getSolution(): int | ||
| 30 | { | ||
| 31 | return ($this->a * $this->b); | ||
| 32 | } | ||
| 33 | |||
| 34 | private function toLettersFrench(int $number): string | ||
| 35 | { | ||
| 36 | return match($number){ | ||
| 37 | 2 => 'deux', | ||
| 38 | 3 => 'trois', | ||
| 39 | 4 => 'quatre', | ||
| 40 | 5 => 'cinq', | ||
| 41 | 6 => 'six', | ||
| 42 | 7 => 'sept', | ||
| 43 | 8 => 'huit', | ||
| 44 | 9 => 'neuf', | ||
| 45 | default => '', // erreur | ||
| 46 | }; | ||
| 47 | } | ||
| 48 | |||
| 49 | // (à déplacer dans FormValidation?) | ||
| 50 | static public function controlInput(string $input = '0'): int | ||
| 51 | { | ||
| 52 | // un POST est une chaîne qu'on doit convertir en nombre dans deux conditions: | ||
| 53 | // test de format: $input est un nombre | ||
| 54 | // test d'intégrité: supprimer les décimales avec (int) ne change pas la valeur du nombre | ||
| 55 | return is_numeric($input) && $input == (int)$input ? (int)$input : 0; | ||
| 56 | } | ||
| 57 | } \ No newline at end of file | ||
diff --git a/src/Service/Config.php b/src/Service/Config.php new file mode 100644 index 0000000..fc4d127 --- /dev/null +++ b/src/Service/Config.php | |||
| @@ -0,0 +1,90 @@ | |||
| 1 | <?php | ||
| 2 | // src/Service/Config.php | ||
| 3 | |||
| 4 | declare(strict_types=1); | ||
| 5 | |||
| 6 | namespace App\Service; | ||
| 7 | |||
| 8 | class Config | ||
| 9 | { | ||
| 10 | // BDD | ||
| 11 | static public string $db_host = 'localhost'; | ||
| 12 | static public string $database = ''; | ||
| 13 | static public string $db_driver = 'pdo_mysql'; | ||
| 14 | static public string $user = ''; | ||
| 15 | static public string $password = ''; | ||
| 16 | static public string $table_prefix = ''; | ||
| 17 | |||
| 18 | // classe URL | ||
| 19 | static public string $protocol = 'http'; | ||
| 20 | static public string $index_path = ''; | ||
| 21 | static public string $port = '80'; | ||
| 22 | |||
| 23 | // envoi e-mails | ||
| 24 | static public string $smtp_host = ''; | ||
| 25 | static public string $smtp_username = ''; | ||
| 26 | static public string $smtp_password = ''; | ||
| 27 | static public string $smtp_secure = ''; // tls (smarttls) ou ssl (smtps) ou plain_text/chaine vide | ||
| 28 | |||
| 29 | // destination e-mails | ||
| 30 | static public string $email_from = ''; // correspondant affiché dans la boite de réception | ||
| 31 | static public string $email_from_name = 'site web'; | ||
| 32 | static public string $email_dest = ''; // destinataire affiché dans la boite de réception | ||
| 33 | static public string $email_dest_name = 'moi'; | ||
| 34 | |||
| 35 | // copier dans ce tableau les variables contenant des chemins | ||
| 36 | static private array $path_vars = []; | ||
| 37 | |||
| 38 | static public function load(string $file_path): void | ||
| 39 | { | ||
| 40 | if(file_exists($file_path)) | ||
| 41 | { | ||
| 42 | // ce serait bien de gérer aussi les fichiers corrompus? | ||
| 43 | $raw_data = parse_ini_file($file_path); | ||
| 44 | self::hydrate($raw_data); | ||
| 45 | } | ||
| 46 | else | ||
| 47 | { | ||
| 48 | echo "<p>Le fichier config/config.ini n'existe pas ou n'est pas lisible.</p>"; | ||
| 49 | } | ||
| 50 | define('TABLE_PREFIX', self::$table_prefix); | ||
| 51 | } | ||
| 52 | |||
| 53 | // renseigner les variables internes de Config | ||
| 54 | static private function hydrate(array $raw_data): void | ||
| 55 | { | ||
| 56 | foreach($raw_data as $field => $value) | ||
| 57 | { | ||
| 58 | if($value != '') // valeur par défaut | ||
| 59 | { | ||
| 60 | if(isset(self::$$field)) // le champ existe dans Config | ||
| 61 | { | ||
| 62 | // problème du slash à la fin du nom d'un dossier | ||
| 63 | $value = self::slashAtEndOfPath($field, $value); | ||
| 64 | self::$$field = $value; | ||
| 65 | } | ||
| 66 | else | ||
| 67 | { | ||
| 68 | 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"; | ||
| 69 | } | ||
| 70 | } | ||
| 71 | /*else | ||
| 72 | { | ||
| 73 | echo "debug: le champ " . $field . " est vide, la valeur par défaut " . self::$$field . " sera utilisée.\n"; | ||
| 74 | }*/ | ||
| 75 | } | ||
| 76 | } | ||
| 77 | |||
| 78 | |||
| 79 | // pour que les chemins finissent toujours par un / | ||
| 80 | static private function slashAtEndOfPath(string $field, string $value): string | ||
| 81 | { | ||
| 82 | foreach(self::$path_vars as $item) | ||
| 83 | { | ||
| 84 | if($field === $item){ | ||
| 85 | return !str_ends_with($value, '/') ? $value . '/' : $value; | ||
| 86 | } | ||
| 87 | } | ||
| 88 | return $value; | ||
| 89 | } | ||
| 90 | } | ||
diff --git a/src/Service/EmailService.php b/src/Service/EmailService.php new file mode 100644 index 0000000..38b4cf8 --- /dev/null +++ b/src/Service/EmailService.php | |||
| @@ -0,0 +1,106 @@ | |||
| 1 | <?php | ||
| 2 | // src/Service/EmailService.php | ||
| 3 | |||
| 4 | declare(strict_types=1); | ||
| 5 | |||
| 6 | namespace App\Service; | ||
| 7 | |||
| 8 | use PHPMailer\PHPMailer\PHPMailer; | ||
| 9 | //use PHPMailer\PHPMailer\Exception; | ||
| 10 | use Doctrine\ORM\EntityManager; | ||
| 11 | use App\Entity\Email; | ||
| 12 | use App\Entity\EmailForm; | ||
| 13 | |||
| 14 | class EmailService | ||
| 15 | { | ||
| 16 | const KEEP_EMAILS_DEFAULT = false; | ||
| 17 | |||
| 18 | static public function send(EntityManager $entityManager, EmailForm $form_data, bool $test_email, string $name = '', string $email = '', string $message = ''): bool | ||
| 19 | { | ||
| 20 | $mail = new PHPMailer(true); // true => exceptions | ||
| 21 | $mail->CharSet = 'UTF-8'; | ||
| 22 | |||
| 23 | $smtp_host = $form_data->getData()['smtp_host'] ?? Config::$smtp_host; | ||
| 24 | $smtp_secure = $form_data->getData()['smtp_secure'] ?? Config::$smtp_secure; | ||
| 25 | $smtp_username = $form_data->getData()['smtp_username'] ?? Config::$smtp_username; | ||
| 26 | $smtp_password = $form_data->getData()['smtp_password'] ?? Config::$smtp_password; | ||
| 27 | |||
| 28 | $email_from = $form_data->getData()['email_from'] ?? Config::$email_from; // adresse de l'expéditeur affichée | ||
| 29 | $email_from = empty($email_from) ? $smtp_username : $email_from; // si vide | ||
| 30 | $email_from_name = $form_data->getData()['email_from_name'] ?? Config::$email_from_name; // = nom de l'expéditeur affiché | ||
| 31 | $email_dest = $form_data->getData()['email_dest'] ?? Config::$email_dest; | ||
| 32 | $email_dest_name = $form_data->getData()['email_dest_name'] ?? Config::$email_dest_name; // = destinataire formulaire | ||
| 33 | |||
| 34 | try{ | ||
| 35 | // Paramètres du serveur | ||
| 36 | $mail->isSMTP(); | ||
| 37 | $mail->Host = $smtp_host; | ||
| 38 | $mail->SMTPAuth = true; | ||
| 39 | $mail->Port = 25; | ||
| 40 | |||
| 41 | if($mail->SMTPAuth){ | ||
| 42 | $mail->Username = $smtp_username; // e-mail | ||
| 43 | $mail->Password = $smtp_password; | ||
| 44 | $mail->SMTPSecure = $smtp_secure; // tls (starttls) ou ssl (smtps) | ||
| 45 | if($mail->SMTPSecure === 'tls'){ | ||
| 46 | $mail->Port = 587; | ||
| 47 | } | ||
| 48 | elseif($mail->SMTPSecure === 'ssl'){ | ||
| 49 | $mail->Port = 465; | ||
| 50 | } | ||
| 51 | } | ||
| 52 | //var_dump($mail->smtpConnect());die; // test de connexion | ||
| 53 | |||
| 54 | // Expéditeur et destinataire | ||
| 55 | // $email_from, $email_from_name et $email_dest_name sont modifiables uniquement dans le config.ini pour l'instant | ||
| 56 | $mail->setFrom(strtolower($email_from), $email_from_name); | ||
| 57 | $mail->addAddress(strtolower($email_dest), $email_dest_name); | ||
| 58 | |||
| 59 | // Contenu | ||
| 60 | $mail->isHTML(true); | ||
| 61 | if($test_email){ | ||
| 62 | $mail->Subject = "TEST d'un envoi d'e-mail depuis le site web"; | ||
| 63 | } | ||
| 64 | else{ | ||
| 65 | $mail->Subject = 'Message envoyé par: ' . $name . ' (' . $email . ') depuis le site web'; | ||
| 66 | } | ||
| 67 | $mail->Body = $message; | ||
| 68 | $mail->AltBody = $message; | ||
| 69 | |||
| 70 | $mail->send(); | ||
| 71 | |||
| 72 | // copie en BDD | ||
| 73 | if(!$test_email && ($form_data->getData()['keep_emails'] ?? self::KEEP_EMAILS_DEFAULT)){ | ||
| 74 | $db_email = new Email($name, $email, $email_dest, $message, $form_data); | ||
| 75 | $entityManager->persist($db_email); | ||
| 76 | self::updateLastContactDate($entityManager, $email); | ||
| 77 | $entityManager->flush(); | ||
| 78 | } | ||
| 79 | |||
| 80 | return true; | ||
| 81 | } | ||
| 82 | catch(\Exception $e){ | ||
| 83 | echo "Le message n'a pas pu être envoyé. Erreur : {$e} <br> {$mail->ErrorInfo}"; | ||
| 84 | return false; | ||
| 85 | } | ||
| 86 | } | ||
| 87 | |||
| 88 | static public function updateLastContactDate(EntityManager $entityManager, string $sender): void | ||
| 89 | { | ||
| 90 | foreach($entityManager->getRepository('App\Entity\Email')->findAll() as $email){ | ||
| 91 | $email->getSenderAddress() === $sender ? $email->updateLastContactDate() : null; | ||
| 92 | } | ||
| 93 | } | ||
| 94 | |||
| 95 | // peut être appelée par bin/clean_emails_cron.php | ||
| 96 | static public function cleanEmails(EntityManager $entityManager): void | ||
| 97 | { | ||
| 98 | $emails = $entityManager->getRepository('App\Entity\Email')->findAll(); | ||
| 99 | foreach($emails as $email){ | ||
| 100 | if($email->getDeletionDate() < new \DateTime()){ | ||
| 101 | $entityManager->remove($email); | ||
| 102 | } | ||
| 103 | } | ||
| 104 | $entityManager->flush(); | ||
| 105 | } | ||
| 106 | } \ No newline at end of file | ||
diff --git a/src/Service/FormValidation.php b/src/Service/FormValidation.php new file mode 100644 index 0000000..d91aa12 --- /dev/null +++ b/src/Service/FormValidation.php | |||
| @@ -0,0 +1,216 @@ | |||
| 1 | <?php | ||
| 2 | // src/Service/FormValidation.php | ||
| 3 | // | ||
| 4 | // pattern strategy simplifié, permet évolution future vers le vrai pattern | ||
| 5 | |||
| 6 | declare(strict_types=1); | ||
| 7 | |||
| 8 | namespace App\Service; | ||
| 9 | |||
| 10 | // classe spéciale permettant de bénéficier du typage de classe au lieu de passer une chaîne à FormValidation | ||
| 11 | enum ValidationStrategy { | ||
| 12 | // bloc formulaire de contact | ||
| 13 | case EmailSend; | ||
| 14 | case EmailParam; | ||
| 15 | // formulaires pages spéciales | ||
| 16 | case CreateUser; | ||
| 17 | case Connection; | ||
| 18 | case UsernameUpdate; | ||
| 19 | case PasswordUpdate; | ||
| 20 | |||
| 21 | public function method(): string | ||
| 22 | { | ||
| 23 | return match($this){ | ||
| 24 | self::EmailSend => 'emailSendStrategy', | ||
| 25 | self::EmailParam => 'emailParamStrategy', | ||
| 26 | self::CreateUser => 'createUserStrategy', | ||
| 27 | self::Connection => 'connectionStrategy', | ||
| 28 | self::UsernameUpdate => 'usernameUpdateStrategy', | ||
| 29 | self::PasswordUpdate => 'passwordUpdateStrategy' | ||
| 30 | }; | ||
| 31 | } | ||
| 32 | } | ||
| 33 | |||
| 34 | class FormValidation | ||
| 35 | { | ||
| 36 | private array $data; // tableau associatif (probablement $_POST) | ||
| 37 | private \Closure $callStrategy; | ||
| 38 | private ?string $error; | ||
| 39 | |||
| 40 | public function __construct(array $data, ValidationStrategy $strategy){ | ||
| 41 | $this->data = $data; | ||
| 42 | $method = $strategy->method(); | ||
| 43 | $this->callStrategy = fn() => $this->$method(); | ||
| 44 | } | ||
| 45 | |||
| 46 | public function validate(): bool | ||
| 47 | { | ||
| 48 | $this->error = ($this->callStrategy)(); // appel de la callback | ||
| 49 | return empty($this->errors); | ||
| 50 | } | ||
| 51 | |||
| 52 | public function getError(): string | ||
| 53 | { | ||
| 54 | return $this->error; | ||
| 55 | } | ||
| 56 | |||
| 57 | public function getField(string $field): string | ||
| 58 | { | ||
| 59 | return $this->data[$field] ?? ''; | ||
| 60 | } | ||
| 61 | |||
| 62 | // méthodes de validation | ||
| 63 | private function captchaValidate(bool $clean_session = true): ?string | ||
| 64 | { | ||
| 65 | $captcha_solution = (isset($_SESSION['captcha']) && is_int($_SESSION['captcha'])) ? $_SESSION['captcha'] : 0; | ||
| 66 | $captcha_try = isset($this->data['captcha']) ? Captcha::controlInput($this->data['captcha']) : 0; | ||
| 67 | if($clean_session){ | ||
| 68 | unset($_SESSION['captcha']); | ||
| 69 | } | ||
| 70 | |||
| 71 | if($captcha_try == 0){ | ||
| 72 | return 'error_non_valid_captcha'; | ||
| 73 | } | ||
| 74 | elseif($captcha_solution == 0){ // ne peut pas arriver, si? | ||
| 75 | return 'captcha_server_error'; | ||
| 76 | } | ||
| 77 | elseif($captcha_try !== $captcha_solution){ | ||
| 78 | return 'bad_solution_captcha'; | ||
| 79 | } | ||
| 80 | return null; | ||
| 81 | } | ||
| 82 | |||
| 83 | // erreurs à la création des mots de passe | ||
| 84 | static private function removeSpacesTabsCRLF(string $chaine): string | ||
| 85 | { | ||
| 86 | $cibles = [' ', "\t", "\n", "\r"]; // doubles quotes !! | ||
| 87 | return(str_replace($cibles, '', $chaine)); | ||
| 88 | } | ||
| 89 | |||
| 90 | |||
| 91 | // stratégies | ||
| 92 | private function emailSendStrategy(): ?string | ||
| 93 | { | ||
| 94 | if($error = $this->captchaValidate(false)){ | ||
| 95 | return $error; | ||
| 96 | } | ||
| 97 | |||
| 98 | if(empty($this->data['name']) || empty($this->data['email']) || empty($this->data['message']) | ||
| 99 | || !isset($this->data['hidden']) || !empty($this->data['hidden'])){ | ||
| 100 | return 'missing_fields'; | ||
| 101 | } | ||
| 102 | |||
| 103 | elseif(!filter_var(trim($this->data['email']), FILTER_VALIDATE_EMAIL)){ | ||
| 104 | return 'bad_email_address'; | ||
| 105 | } | ||
| 106 | |||
| 107 | $this->data['name'] = htmlspecialchars(trim($this->data['name'])); | ||
| 108 | $this->data['email'] = htmlspecialchars(trim($this->data['email'])); | ||
| 109 | $this->data['message'] = htmlspecialchars($this->data['message']); | ||
| 110 | |||
| 111 | return null; | ||
| 112 | } | ||
| 113 | private function emailParamStrategy(): ?string | ||
| 114 | { | ||
| 115 | if(empty($this->data['id']) || empty($this->data['what_param']) || empty($this->data['value']) | ||
| 116 | || !isset($this->data['hidden']) || !empty($this->data['hidden'])){ | ||
| 117 | return 'missing_fields'; | ||
| 118 | } | ||
| 119 | |||
| 120 | if(!in_array($this->data['what_param'], ['smtp_host', 'smtp_secure', 'smtp_username', 'smtp_password', 'email_dest'])){ | ||
| 121 | return 'unknown_parameter'; | ||
| 122 | } | ||
| 123 | elseif($this->data['what_param'] === 'smtp_username' || $this->data['what_param'] === 'email_dest'){ | ||
| 124 | if(!filter_var($this->data['value'], FILTER_VALIDATE_EMAIL)){ | ||
| 125 | return 'invalide_email_address'; | ||
| 126 | } | ||
| 127 | } | ||
| 128 | return null; | ||
| 129 | } | ||
| 130 | private function createUserStrategy(): ?string | ||
| 131 | { | ||
| 132 | if($error = $this->captchaValidate()){ | ||
| 133 | return $error; | ||
| 134 | } | ||
| 135 | |||
| 136 | // test mauvais paramètres | ||
| 137 | if(empty($this->data['login']) || empty($this->data['password']) || empty($this->data['password_confirmation']) | ||
| 138 | || !isset($this->data['create_user_hidden']) || !empty($this->data['create_user_hidden'])) | ||
| 139 | { | ||
| 140 | return 'bad_login_or_password'; | ||
| 141 | } | ||
| 142 | |||
| 143 | if($this->data['password'] !== $this->data['password_confirmation']){ | ||
| 144 | return 'different_passwords'; | ||
| 145 | } | ||
| 146 | |||
| 147 | if($this->data['login'] !== self::removeSpacesTabsCRLF(htmlspecialchars($this->data['login'])) | ||
| 148 | || $this->data['password'] !== self::removeSpacesTabsCRLF(htmlspecialchars($this->data['password']))){ | ||
| 149 | return 'forbidden_characters'; | ||
| 150 | } | ||
| 151 | return null; | ||
| 152 | } | ||
| 153 | private function connectionStrategy(): ?string | ||
| 154 | { | ||
| 155 | if($error = $this->captchaValidate()){ | ||
| 156 | return $error; | ||
| 157 | } | ||
| 158 | |||
| 159 | if(empty($this->data['login']) || empty($this->data['password']) | ||
| 160 | || !isset($this->data['connection_hidden']) || !empty($this->data['connection_hidden'])) | ||
| 161 | { | ||
| 162 | return 'bad_login_or_password'; | ||
| 163 | } | ||
| 164 | return null; | ||
| 165 | } | ||
| 166 | private function usernameUpdateStrategy(): ?string | ||
| 167 | { | ||
| 168 | if($error = $this->captchaValidate()){ | ||
| 169 | return $error; | ||
| 170 | } | ||
| 171 | |||
| 172 | if(empty($this->data['login']) || empty($this->data['password']) || empty($this->data['new_login']) | ||
| 173 | || !isset($this->data['modify_username_hidden']) || !empty($this->data['modify_username_hidden'])) | ||
| 174 | { | ||
| 175 | return 'bad_login_or_password'; | ||
| 176 | } | ||
| 177 | |||
| 178 | $new_login = self::removeSpacesTabsCRLF(htmlspecialchars($this->data['new_login'])); | ||
| 179 | if($new_login !== $this->data['new_login']){ | ||
| 180 | return 'forbidden_characters'; | ||
| 181 | } | ||
| 182 | |||
| 183 | if($this->data['login'] !== $_SESSION['user']['username']){ | ||
| 184 | return 'bad_login_or_password'; | ||
| 185 | } | ||
| 186 | if($this->data['login'] === $new_login){ | ||
| 187 | return 'same_username_as_before'; | ||
| 188 | } | ||
| 189 | return null; | ||
| 190 | } | ||
| 191 | private function passwordUpdateStrategy(): ?string | ||
| 192 | { | ||
| 193 | if($error = $this->captchaValidate()){ | ||
| 194 | return $error; | ||
| 195 | } | ||
| 196 | |||
| 197 | if(empty($this->data['login']) || empty($this->data['password']) || empty($this->data['new_password']) | ||
| 198 | || !isset($this->data['modify_password_hidden']) || !empty($this->data['modify_password_hidden'])) | ||
| 199 | { | ||
| 200 | return 'bad_login_or_password'; | ||
| 201 | } | ||
| 202 | |||
| 203 | $new_password = self::removeSpacesTabsCRLF(htmlspecialchars($this->data['new_password'])); | ||
| 204 | if($new_password !== $this->data['new_password']){ | ||
| 205 | return 'forbidden_characters'; | ||
| 206 | } | ||
| 207 | |||
| 208 | if($this->data['login'] !== $_SESSION['user']['username']){ | ||
| 209 | return 'bad_login_or_password'; | ||
| 210 | } | ||
| 211 | if($this->data['password'] === $new_password){ | ||
| 212 | return 'same_password_as_before'; | ||
| 213 | } | ||
| 214 | return null; | ||
| 215 | } | ||
| 216 | } \ No newline at end of file | ||
diff --git a/src/Service/Installation.php b/src/Service/Installation.php new file mode 100644 index 0000000..ed9f20a --- /dev/null +++ b/src/Service/Installation.php | |||
| @@ -0,0 +1,109 @@ | |||
| 1 | <?php | ||
| 2 | // src/Service/Installation.php | ||
| 3 | |||
| 4 | declare(strict_types=1); | ||
| 5 | |||
| 6 | namespace App\Service; | ||
| 7 | |||
| 8 | class Installation | ||
| 9 | { | ||
| 10 | static public function phpDependancies(): void | ||
| 11 | { | ||
| 12 | $flag = false; | ||
| 13 | $extensions = ['pdo_mysql', 'mbstring', 'ctype', 'json', 'tokenizer', 'imagick', 'zip']; // les 5 premières sont pour doctrine | ||
| 14 | // ajouter plus tard zlib pour la compression des backups | ||
| 15 | foreach($extensions as $extension){ | ||
| 16 | if(!extension_loaded($extension)){ | ||
| 17 | echo("<p>L'extension <b>" . $extension . "</b> est manquante.</p>"); | ||
| 18 | $flag = true; | ||
| 19 | } | ||
| 20 | } | ||
| 21 | if(!class_exists(\DOMDocument::class)){ // théoriquement plus fiable que extension_loaded() | ||
| 22 | echo("<p>L'extension <b>dom</b> est manquante.</p>"); | ||
| 23 | $flag = true; | ||
| 24 | } | ||
| 25 | |||
| 26 | /*if(!extension_loaded('imagick') && !extension_loaded('gd')){ | ||
| 27 | echo("<p>il manque une de ces extensions au choix pour le traitement des images: <b>imagick</b> (de préférence) ou <b>gd</b>.</p>"); | ||
| 28 | $flag = true; | ||
| 29 | }*/ | ||
| 30 | // si imagick n'est pas disponible, essayer gd (reste encore à coder) | ||
| 31 | |||
| 32 | if($flag){ | ||
| 33 | echo '<p>Réalisez les actions nécéssaires sur le serveur ou contactez l\'administrateur du site.<br> | ||
| 34 | Quand le problème sera résolu, il vous suffira de <a href="#">recharger la page<a>.</p>'; | ||
| 35 | die; | ||
| 36 | } | ||
| 37 | } | ||
| 38 | |||
| 39 | static public function checkFilesAndFoldersRights(): void | ||
| 40 | { | ||
| 41 | // -- droits des fichiers et dossiers -- | ||
| 42 | $droits_dossiers = 0777; | ||
| 43 | |||
| 44 | $flag = false; | ||
| 45 | if(!file_exists('user_data')){ | ||
| 46 | try{ | ||
| 47 | mkdir('user_data/'); | ||
| 48 | chmod('user_data/', $droits_dossiers); | ||
| 49 | } | ||
| 50 | catch(\Exception $e){ | ||
| 51 | echo '<p style="color: red;">Le dossier public/user_data introuvable et le serveur n\'a pas la permission de le créer.<br> | ||
| 52 | Pour faire ça bien:<br>sudo -u "serveur web" mkdir /chemin/du/site/public/user_data</p>'; | ||
| 53 | echo $e; | ||
| 54 | $flag = true; | ||
| 55 | } | ||
| 56 | } | ||
| 57 | if(!file_exists('../var')){ | ||
| 58 | try{ | ||
| 59 | mkdir('../var'); | ||
| 60 | chmod('../var', $droits_dossiers); | ||
| 61 | } | ||
| 62 | catch(\Exception $e){ | ||
| 63 | echo $e; | ||
| 64 | $flag = true; | ||
| 65 | } | ||
| 66 | } | ||
| 67 | if(!file_exists('../var/backups')){ | ||
| 68 | try{ | ||
| 69 | mkdir('../var/backups'); | ||
| 70 | chmod('../var/backups', $droits_dossiers); // autoriser à la fois le serveur et les scripts dans bin/ | ||
| 71 | } | ||
| 72 | catch(\Exception $e){ | ||
| 73 | echo $e; | ||
| 74 | $flag = true; | ||
| 75 | } | ||
| 76 | } | ||
| 77 | |||
| 78 | // droits 600 pour celui-ci | ||
| 79 | if(!file_exists('../config/config.ini')){ | ||
| 80 | // aide à la création du config.ini | ||
| 81 | echo '<p>Le fichier config/config.ini est introuvable.</p>'; | ||
| 82 | echo '<p>Il doit obligatoirement contenir les codes de la base de données, le protocole http ou https (et éventuellement le port) utilisé pour créer les liens internes.<br> | ||
| 83 | Un modèle est disponible, il s\'agit du fichier config/config-template.ini</p> | ||
| 84 | <p>Ce fichier a une importance critique. Si vous le pouvez faites en sorte que le serveur en soit le propriétaire et donner lui des droits 600.</p>'; | ||
| 85 | $flag = true; | ||
| 86 | } | ||
| 87 | /*else{ | ||
| 88 | // propriétaire du fichier | ||
| 89 | if(fileowner('../config/config.ini') != posix_geteuid()){ | ||
| 90 | echo "<p>le fichier config/config.ini n'appartient pas au serveur.</p>"; | ||
| 91 | } | ||
| 92 | else{ | ||
| 93 | // droits du config.ini | ||
| 94 | if(substr(sprintf('%o', fileperms('../config/config.ini')), -4) != 600){ | ||
| 95 | echo '<p>Attention, le </p>'; | ||
| 96 | //chmod('../config/config.ini', $droits_fichiers); | ||
| 97 | } | ||
| 98 | } | ||
| 99 | }*/ | ||
| 100 | if($flag){ | ||
| 101 | die; | ||
| 102 | } | ||
| 103 | |||
| 104 | // tester les liens internes | ||
| 105 | // | ||
| 106 | |||
| 107 | // le test de connexion à la BDD est dans le doctrine bootstrap | ||
| 108 | } | ||
| 109 | } \ No newline at end of file | ||
diff --git a/src/Service/Router.php b/src/Service/Router.php new file mode 100644 index 0000000..98cecc5 --- /dev/null +++ b/src/Service/Router.php | |||
| @@ -0,0 +1,393 @@ | |||
| 1 | <?php | ||
| 2 | // src/Service/Router.php | ||
| 3 | // | ||
| 4 | /* fonctionnement: | ||
| 5 | => 1er test, méthode http GET? POST? | ||
| 6 | => 2ème test, type de contenu: | ||
| 7 | "application/x-www-form-urlencoded" = formulaire | ||
| 8 | "application/json" = requête AJAX avec fetch() | ||
| 9 | "multipart/form-data" = upload d'image par tinymce | ||
| 10 | $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest' requête AJAX xhs, non utilisée | ||
| 11 | => 3ème test, comme le 2ème test mais uniquement si IS_ADMIN est vrai */ | ||
| 12 | |||
| 13 | /* classes de réponses pour les contrôleurs | ||
| 14 | Response($html, Response::HTTP_OK) page html | ||
| 15 | JsonResponse(['success' => true, 'data' => $data]) ajax | ||
| 16 | RedirectResponse('index.php?page=login') redirection | ||
| 17 | BinaryFileResponse($filePath) téléchargement | ||
| 18 | StreamedResponse(function () {echo "ligne 1\n";echo "ligne 2\n";}) gros fichier */ | ||
| 19 | |||
| 20 | // relire ça à l'occaz: | ||
| 21 | // https://symfony.com/doc/current/introduction/from_flat_php_to_symfony.html | ||
| 22 | |||
| 23 | declare(strict_types=1); | ||
| 24 | |||
| 25 | namespace App\Service; | ||
| 26 | |||
| 27 | use Symfony\Component\HttpFoundation\Request; | ||
| 28 | use Doctrine\ORM\EntityManager; | ||
| 29 | use Symfony\Component\HttpFoundation\JsonResponse; | ||
| 30 | use Symfony\Component\HttpFoundation\RedirectResponse; | ||
| 31 | use Symfony\Component\HttpFoundation\Response; | ||
| 32 | use App\Service\User; | ||
| 33 | use App\View\AbstractBuilder; | ||
| 34 | use App\Model\Model; | ||
| 35 | use App\Model\Menu; | ||
| 36 | use App\Controller\UserController; | ||
| 37 | use App\Controller\ArticleController; | ||
| 38 | use App\Controller\CalendarController; | ||
| 39 | use App\Controller\MaintenanceController; | ||
| 40 | use App\Controller\ViewController; | ||
| 41 | use App\Controller\ContactFormController; | ||
| 42 | use App\Controller\ImageUploadController; | ||
| 43 | use App\Controller\HeadFootController; | ||
| 44 | use App\Controller\MenuAndPathsController; | ||
| 45 | use App\Controller\PageManagementController; | ||
| 46 | use App\Controller\FileUploadController; | ||
| 47 | |||
| 48 | class Router{ | ||
| 49 | private Request $request; | ||
| 50 | private EntityManager $entityManager; | ||
| 51 | private string $route = ''; // défaut page d'accueil | ||
| 52 | |||
| 53 | public function __construct(Request $request, EntityManager $entityManager){ | ||
| 54 | $this->request = $request; | ||
| 55 | $this->entityManager = $entityManager; | ||
| 56 | |||
| 57 | if(!User::existUsers($entityManager)){ // table "user" vide | ||
| 58 | $this->route = 'no_user'; | ||
| 59 | } | ||
| 60 | } | ||
| 61 | |||
| 62 | public function dispatch(): Response | ||
| 63 | { | ||
| 64 | if($this->request->getMethod() === 'GET'){ | ||
| 65 | // table "user" vide | ||
| 66 | if($this->route === 'no_user'){ | ||
| 67 | ob_start(); | ||
| 68 | require AbstractBuilder::VIEWS_PATH . 'user_create.php'; | ||
| 69 | return new Response(ob_get_clean()); | ||
| 70 | } | ||
| 71 | |||
| 72 | // bouton déconnexion (méthode GET parce que l'utilisateur ne modifie plus de données à partir de là) | ||
| 73 | if($this->request->query->get('action') === 'deconnection'){ | ||
| 74 | return UserController::disconnect(); // retourne un RedirectResponse | ||
| 75 | } | ||
| 76 | |||
| 77 | // articles suivants | ||
| 78 | if($this->request->query->get('fetch') === 'next_articles'){ | ||
| 79 | return ArticleController::fetch($this->entityManager, $this->request); // retourne un JsonResponse | ||
| 80 | } | ||
| 81 | |||
| 82 | // données du calendrier | ||
| 83 | // création du calendrier ou changement de dates affichées (boutons flèches mais pas changement de vue) | ||
| 84 | if($this->request->query->get('action') === 'get_events' | ||
| 85 | && $this->request->query->has('start') && $this->request->query->has('end') && empty($this->request->getPayload()->all())){ // getPayload ne récupère pas que des POST | ||
| 86 | return CalendarController::getData($this->entityManager); | ||
| 87 | } | ||
| 88 | |||
| 89 | // pages interdites | ||
| 90 | if(!IS_ADMIN && in_array(CURRENT_PAGE, ['menu_paths', 'new_page', 'user_edit', 'emails', 'maintenance'])){ | ||
| 91 | return new RedirectResponse((string)new URL); | ||
| 92 | } | ||
| 93 | |||
| 94 | if(IS_ADMIN){ | ||
| 95 | if($this->request->query->get('action') === 'get_mysqldump'){ | ||
| 96 | return MaintenanceController::getLastDump($this->entityManager); | ||
| 97 | } | ||
| 98 | if($this->request->query->get('action') === 'get_all_media'){ | ||
| 99 | return MaintenanceController::getAllMedia(); | ||
| 100 | } | ||
| 101 | } | ||
| 102 | |||
| 103 | // affichage d'une page | ||
| 104 | return ViewController::getWebPage($this->entityManager, $this->request); | ||
| 105 | } | ||
| 106 | |||
| 107 | |||
| 108 | elseif($this->request->getMethod() === 'POST'){ | ||
| 109 | /* -- contrôleurs appelables par tout le monde -- */ | ||
| 110 | |||
| 111 | // table "user" vide | ||
| 112 | if($this->route === 'no_user'){ | ||
| 113 | return UserController::createAdminUser($this->entityManager); | ||
| 114 | } | ||
| 115 | |||
| 116 | // requête JSON avec fetch() | ||
| 117 | if($this->request->headers->get('Content-Type') === 'application/json') | ||
| 118 | { | ||
| 119 | $json = json_decode($this->request->getContent(), true); // = json_decode(file_get_contents('php://input'), true); | ||
| 120 | |||
| 121 | // formulaire de contact | ||
| 122 | if($this->request->query->get('action') === 'send_email'){ | ||
| 123 | return ContactFormController::sendVisitorEmail($this->entityManager, $json); | ||
| 124 | } | ||
| 125 | /*else{ | ||
| 126 | return new JsonResponse(['success' => false, 'error' => 'tu fais quoi là mec?'], JsonResponse::HTTP_BAD_REQUEST); | ||
| 127 | }*/ | ||
| 128 | } | ||
| 129 | |||
| 130 | // envoi formulaire HTML | ||
| 131 | elseif($this->request->headers->get('Content-Type') === 'application/x-www-form-urlencoded'){ | ||
| 132 | // tentative de connexion | ||
| 133 | if($this->request->query->get('action') === 'connection'){ | ||
| 134 | return UserController::connect($this->entityManager); | ||
| 135 | } | ||
| 136 | /*else{ | ||
| 137 | return new RedirectResponse((string)new URL(['error' => 'tu fais quoi là mec?'])); | ||
| 138 | }*/ | ||
| 139 | } | ||
| 140 | |||
| 141 | if(IS_ADMIN){ | ||
| 142 | /* -- requêtes AJAX -- */ | ||
| 143 | |||
| 144 | // requêtes JSON avec fetch() | ||
| 145 | if($this->request->headers->get('Content-Type') === 'application/json'){ | ||
| 146 | $json = json_decode($this->request->getContent(), true); // = json_decode(file_get_contents('php://input'), true); | ||
| 147 | |||
| 148 | if($this->request->query->has('action')){ | ||
| 149 | /* -- manipulation des articles -- */ | ||
| 150 | if($this->request->query->get('action') === 'editor_submit' && isset($json['id']) && isset($json['content'])){ | ||
| 151 | return ArticleController::editorSubmit($this->entityManager, $json); | ||
| 152 | } | ||
| 153 | elseif($this->request->query->get('action') === 'delete_article' && isset($json['id'])){ | ||
| 154 | return ArticleController::deleteArticle($this->entityManager, $this->request); // version AJAX | ||
| 155 | } | ||
| 156 | elseif($this->request->query->get('action') === 'switch_positions' && isset($json['id1']) && isset($json['id2'])){ | ||
| 157 | return ArticleController::switchPositions($this->entityManager, $json); | ||
| 158 | } | ||
| 159 | elseif($this->request->query->get('action') === 'date_submit' && isset($json['id']) && isset($json['date'])){ | ||
| 160 | return ArticleController::dateSubmit($this->entityManager, $json); | ||
| 161 | } | ||
| 162 | |||
| 163 | switch($this->request->query->get('action')){ | ||
| 164 | /* -- bloc Formulaire -- */ | ||
| 165 | case 'keep_emails': | ||
| 166 | return ContactFormController::keepEmails($this->entityManager, $json); | ||
| 167 | case 'set_retention_period': | ||
| 168 | return ContactFormController::setEmailsRetentionPeriod($this->entityManager, $json); | ||
| 169 | case 'set_email_param': | ||
| 170 | return ContactFormController::setEmailParam($this->entityManager, $json); | ||
| 171 | case 'test_email': | ||
| 172 | return ContactFormController::sendTestEmail($this->entityManager, $json); | ||
| 173 | |||
| 174 | /* -- page emails -- */ | ||
| 175 | case 'delete_email': | ||
| 176 | return ContactFormController::deleteEmail($this->entityManager, $json); | ||
| 177 | case 'toggle_sensitive_email': | ||
| 178 | return ContactFormController::toggleSensitiveEmail($this->entityManager, $json); | ||
| 179 | |||
| 180 | /* -- upload d'image dans tinymce par copier-coller -- */ | ||
| 181 | // collage de HTML contenant une ou plusieurs balises <img> | ||
| 182 | case 'upload_image_url': | ||
| 183 | return ImageUploadController::uploadImageHtml(); | ||
| 184 | // collage d'une image (code base64 dans le presse-papier) non encapsulée dans du HTML | ||
| 185 | case 'upload_image_base64': | ||
| 186 | return ImageUploadController::uploadImageBase64(); | ||
| 187 | |||
| 188 | /* -- requêtes spécifiques au calendrier -- */ | ||
| 189 | case 'new_event': | ||
| 190 | return CalendarController::newEvent($json, $this->entityManager); | ||
| 191 | case 'update_event': | ||
| 192 | return CalendarController::updateEvent($json, $this->entityManager); | ||
| 193 | case 'remove_event': | ||
| 194 | return CalendarController::removeEvent($json, $this->entityManager); | ||
| 195 | |||
| 196 | /* -- mode maintenance -- */ | ||
| 197 | case 'get_logs': | ||
| 198 | return MaintenanceController::getLogs($this->entityManager); | ||
| 199 | case 'erase_logs': | ||
| 200 | return MaintenanceController::eraseLogs($this->entityManager); | ||
| 201 | |||
| 202 | default: | ||
| 203 | return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); | ||
| 204 | } | ||
| 205 | } | ||
| 206 | |||
| 207 | /* -- site entier (header, footer, favicon) -- */ | ||
| 208 | elseif($this->request->query->has('head_foot_text')){ | ||
| 209 | return HeadFootController::setTextData($this->entityManager, $this->request->query->get('head_foot_text'), $json); | ||
| 210 | } | ||
| 211 | elseif($this->request->query->has('head_foot_social_check')){ | ||
| 212 | return HeadFootController::displaySocialNetwork($this->entityManager, $this->request->query->get('head_foot_social_check'), $json); | ||
| 213 | } | ||
| 214 | |||
| 215 | /* -- page Menu et chemins -- */ | ||
| 216 | elseif($this->request->query->has('menu_edit')){ | ||
| 217 | // ne suit pas la règle, faire ça dans un contrôleur? | ||
| 218 | Model::$menu = new Menu($this->entityManager); // récupération des données | ||
| 219 | |||
| 220 | // flèche gauche <=: position = position du parent + 1, parent = grand-parent, recalculer les positions | ||
| 221 | if($this->request->query->get('menu_edit') === 'move_one_level_up' && isset($json['id'])){ | ||
| 222 | return MenuAndPathsController::MoveOneLevelUp($this->entityManager, $json); | ||
| 223 | } | ||
| 224 | // flèche droite =>: position (léments de a fraterie + 1, l'élément précédent devient le parent | ||
| 225 | elseif($this->request->query->get('menu_edit') === 'move_one_level_down' && isset($json['id'])){ | ||
| 226 | return MenuAndPathsController::MoveOneLevelDown($this->entityManager, $json); | ||
| 227 | } | ||
| 228 | elseif($this->request->query->get('menu_edit') === 'switch_positions' && isset($json['id1']) && isset($json['id2'])){ | ||
| 229 | return MenuAndPathsController::switchPositions($this->entityManager, $json); | ||
| 230 | } | ||
| 231 | elseif($this->request->query->get('menu_edit') === 'display_in_menu' && isset($json['id']) && isset($json['checked'])){ | ||
| 232 | return MenuAndPathsController::displayInMenu($this->entityManager, $json); | ||
| 233 | } | ||
| 234 | elseif($this->request->query->get('menu_edit') === 'url_edit' && isset($json['id']) && isset($json['field']) && isset($json['input_data'])){ | ||
| 235 | return MenuAndPathsController::editUrl($this->entityManager, $json); | ||
| 236 | } | ||
| 237 | else{ | ||
| 238 | return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); // code 400 | ||
| 239 | } | ||
| 240 | } | ||
| 241 | |||
| 242 | /* -- mode Modification d'une page -- */ | ||
| 243 | // partie "page" | ||
| 244 | elseif($this->request->query->has('page_edit')){ | ||
| 245 | switch($this->request->query->get('page_edit')){ | ||
| 246 | case 'page_title': | ||
| 247 | return PageManagementController::setPageTitle($this->entityManager, $json); | ||
| 248 | case 'page_description': | ||
| 249 | return PageManagementController::setPageDescription($this->entityManager, $json); | ||
| 250 | default: | ||
| 251 | return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); // code 400 | ||
| 252 | } | ||
| 253 | } | ||
| 254 | |||
| 255 | // partie "blocs" | ||
| 256 | elseif($this->request->query->has('bloc_edit')){ | ||
| 257 | switch($this->request->query->get('bloc_edit')){ | ||
| 258 | case 'rename_page_bloc': | ||
| 259 | return PageManagementController::renameBloc($this->entityManager, $json); | ||
| 260 | case 'switch_blocs_positions': | ||
| 261 | return PageManagementController::SwitchBlocsPositions($this->entityManager, $json, $this->request->query->get('page')); | ||
| 262 | case 'change_articles_order': | ||
| 263 | return PageManagementController::changeArticlesOrder($this->entityManager, $json); | ||
| 264 | case 'change_presentation': | ||
| 265 | return PageManagementController::changePresentation($this->entityManager, $json); | ||
| 266 | case 'change_cols_min_width': | ||
| 267 | return PageManagementController::changeColsMinWidth($this->entityManager, $json); | ||
| 268 | case 'change_pagination_limit': | ||
| 269 | return PageManagementController::changePaginationLimit($this->entityManager, $json); | ||
| 270 | default: | ||
| 271 | return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); // code 400 | ||
| 272 | } | ||
| 273 | } | ||
| 274 | |||
| 275 | else{ | ||
| 276 | return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); // code 400 | ||
| 277 | } | ||
| 278 | } | ||
| 279 | |||
| 280 | /* -- upload avec FormData OU formulaire HTML avec fichier -- */ | ||
| 281 | elseif(str_starts_with($this->request->headers->get('Content-Type'), 'multipart/form-data')){ // = $_SERVER['CONTENT_TYPE'] | ||
| 282 | // dans tinymce avec le plugin (bouton "insérer une image" de l'éditeur ou glisser-déposer) | ||
| 283 | if($this->request->query->get('action') === 'upload_image_tinymce'){ | ||
| 284 | return ImageUploadController::imageUploadTinyMce(); | ||
| 285 | } | ||
| 286 | // dans tinymce, des quatre méthodes: bouton "link", drag & drop, html, base64 | ||
| 287 | elseif($this->request->query->get('action') === 'upload_file_tinymce'){ | ||
| 288 | return FileUploadController::fileUploadTinyMce(); | ||
| 289 | } | ||
| 290 | elseif($this->request->query->has('head_foot_image')){ | ||
| 291 | return HeadFootController::uploadAsset($this->entityManager, $this->request->query->get('head_foot_image')); | ||
| 292 | } | ||
| 293 | |||
| 294 | /* -- page Maintenance -- */ | ||
| 295 | elseif($this->request->query->get('action') === 'restore_database' && $this->request->request->get('hidden') === '' | ||
| 296 | && $this->request->files->has('uploaded_sql')){ | ||
| 297 | return MaintenanceController::downloadSQL($this->entityManager, $this->request); | ||
| 298 | } | ||
| 299 | else{ | ||
| 300 | // choix ici entre répondre en JSON ou par une redirection, choix du JSON pour pouvoir passer un message | ||
| 301 | return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); // code 400 | ||
| 302 | } | ||
| 303 | } | ||
| 304 | |||
| 305 | /* -- formulaire HTML sans fichier -- */ | ||
| 306 | elseif($this->request->headers->get('Content-Type') === 'application/x-www-form-urlencoded'){ | ||
| 307 | if($this->request->query->get('action') === 'delete_article' && $this->request->query->has('id')){ | ||
| 308 | return ArticleController::deleteArticle($this->entityManager, $this->request); // version formulaire | ||
| 309 | } | ||
| 310 | |||
| 311 | /* -- nouvelle page -- */ | ||
| 312 | elseif($this->request->request->get('page_name') !== null | ||
| 313 | && $this->request->request->get('page_name_path') !== null | ||
| 314 | && $this->request->request->get('page_location') !== null | ||
| 315 | && $this->request->request->get('page_description') !== null | ||
| 316 | && $this->request->request->get('new_page_hidden') === ''){ | ||
| 317 | return PageManagementController::newPage($this->entityManager, $this->request->request); | ||
| 318 | } | ||
| 319 | |||
| 320 | /* -- suppression d'une page -- */ | ||
| 321 | elseif($this->request->request->get('page_id') !== null | ||
| 322 | && $this->request->request->get('submit_hidden') === ''){ | ||
| 323 | return PageManagementController::deletePage($this->entityManager, $this->request->request->get('page_id')); | ||
| 324 | } | ||
| 325 | |||
| 326 | |||
| 327 | /* -- mode Modification d'une page -- */ | ||
| 328 | // modification du chemins en snake_case | ||
| 329 | elseif($this->request->request->get('page_menu_path') !== null | ||
| 330 | && $this->request->request->get('page_id') !== null | ||
| 331 | && $this->request->request->get('page_name_path_hidden') === ''){ | ||
| 332 | return PageManagementController::updatePageMenuPath($this->entityManager, $this->request->request->get('page_menu_path')); | ||
| 333 | } | ||
| 334 | // ajout d'un bloc dans une page | ||
| 335 | elseif($this->request->request->get('bloc_title') !== null | ||
| 336 | && $this->request->request->get('bloc_select') !== null | ||
| 337 | && $this->request->request->get('bloc_title_hidden') === ''){ // contrôle anti-robot avec input hidden | ||
| 338 | return PageManagementController::addBloc($this->entityManager, $this->request); | ||
| 339 | } | ||
| 340 | // suppression d'un bloc de page | ||
| 341 | elseif($this->request->request->get('delete_bloc_id') !== null | ||
| 342 | && $this->request->request->get('delete_bloc_hidden') === ''){ // contrôle anti-robot avec input hidden | ||
| 343 | return PageManagementController::deleteBloc($this->entityManager, $this->request); | ||
| 344 | } | ||
| 345 | |||
| 346 | |||
| 347 | /* -- page Menu et chemins -- */ | ||
| 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")){ | ||
| 350 | return MenuAndPathsController::newUrlMenuEntry($this->entityManager); | ||
| 351 | } | ||
| 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 | ||
| 354 | return MenuAndPathsController::deleteUrlMenuEntry($this->entityManager); | ||
| 355 | } | ||
| 356 | |||
| 357 | |||
| 358 | /* -- page Mon compte -- */ | ||
| 359 | elseif($this->request->query->get('action') === 'update_username'){ | ||
| 360 | return UserController::updateUsername($this->entityManager); | ||
| 361 | } | ||
| 362 | elseif($this->request->query->get('action') === 'update_password'){ | ||
| 363 | return UserController::updatePassword($this->entityManager); | ||
| 364 | } | ||
| 365 | |||
| 366 | /* -- page Maintenance -- */ | ||
| 367 | elseif($this->request->query->get('action') === 'restore_database' && $this->request->get('hidden') === '' && $this->request->request->has('selected_sql')){ | ||
| 368 | return MaintenanceController::handleBackupSelection($this->entityManager, $this->request); | ||
| 369 | } | ||
| 370 | |||
| 371 | // redirection page d'accueil | ||
| 372 | return new RedirectResponse((string)new URL(['error' => 'paramètres inconnus'])); | ||
| 373 | } | ||
| 374 | |||
| 375 | // requêtes XMLHttpRequest | ||
| 376 | elseif($this->request->isXmlHttpRequest()){ | ||
| 377 | return new JsonResponse(['success' => false]); // noyer le poisson en laissant penser que le site gère les requêtes XHR | ||
| 378 | } | ||
| 379 | |||
| 380 | // POST admin ne matchant pas | ||
| 381 | return new Response('bad parameters', Response::HTTP_BAD_REQUEST); // code 400 | ||
| 382 | } | ||
| 383 | |||
| 384 | // POST non-admin ne matchant pas | ||
| 385 | return new Response('bad parameters', Response::HTTP_BAD_REQUEST); // code 400 | ||
| 386 | } | ||
| 387 | |||
| 388 | // méthode HTTP inconnue | ||
| 389 | else{ | ||
| 390 | return new RedirectResponse((string)new URL(['error' => 'tu fais quoi là mec?'])); | ||
| 391 | } | ||
| 392 | } | ||
| 393 | } \ No newline at end of file | ||
diff --git a/src/Service/Security.php b/src/Service/Security.php new file mode 100644 index 0000000..0fbd8b7 --- /dev/null +++ b/src/Service/Security.php | |||
| @@ -0,0 +1,112 @@ | |||
| 1 | <?php | ||
| 2 | // src/Service/Security.php | ||
| 3 | // | ||
| 4 | // htmlawed nettoie les entrées de l'utilisateur, en particulier le html de l'éditeur | ||
| 5 | |||
| 6 | declare(strict_types=1); | ||
| 7 | |||
| 8 | namespace App\Service; | ||
| 9 | |||
| 10 | class Security | ||
| 11 | { | ||
| 12 | private static $configHtmLawed = array( | ||
| 13 | 'safe'=>1, // protection contre les élements et attributs dangereux | ||
| 14 | |||
| 15 | // liste blanche d'éléments HTML | ||
| 16 | 'elements'=> 'h1, h2, h3, h4, h5, h6, p, br, 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, iframe, small', | ||
| 17 | |||
| 18 | // liste noire d'attributs HTML | ||
| 19 | 'deny_attribute'=> 'id, class' // on garde 'style' | ||
| 20 | ); | ||
| 21 | // faire qu'un certain élément puisse n'avoir que certains attributs, regarder la doc | ||
| 22 | private static $specHtmLawed = ''; | ||
| 23 | |||
| 24 | // obtenir du HTML non dangereur sans appliquer htmlspecialchars | ||
| 25 | public static function secureHTML(string $chaine): string | ||
| 26 | { | ||
| 27 | return trim(htmLawed($chaine, self::$configHtmLawed, self::$specHtmLawed)); | ||
| 28 | } | ||
| 29 | |||
| 30 | public static function secureFileName(string $chaine): string | ||
| 31 | { | ||
| 32 | // sécuriser un nom avec chemin avec basename? | ||
| 33 | //$chaine = basename($chaine); | ||
| 34 | |||
| 35 | /* | ||
| 36 | - caractères interdits sous windows / \ : * ? " < > | | ||
| 37 | - mac autorise les / | ||
| 38 | - mac interdit : | ||
| 39 | - linux autorise tout sauf les / | ||
| 40 | - imagemagick ne supporte pas les : | ||
| 41 | |||
| 42 | - 'espace' fonctionne | ||
| 43 | - / remplacé par firefox en : | ||
| 44 | - \ retire ce qui est devant le \ | ||
| 45 | - * fonctionne | ||
| 46 | - ? permet le téléchargement mais pas l'affichage | ||
| 47 | - " ne fonctionne pas, remplacé par %22, filtrer %22 | ||
| 48 | - < > fonctionnent | ||
| 49 | - | fonctionne | ||
| 50 | - = fonctionne, mais je filtre parce qu'on en trouve dans une URL | ||
| 51 | - ' ` fonctionnent | ||
| 52 | - % fonctionne | ||
| 53 | - (){}[] fonctionnent | ||
| 54 | - ^ fonctionne | ||
| 55 | - # ne fonctionne pas | ||
| 56 | - ~ fonctionne | ||
| 57 | - & fonctionne | ||
| 58 | - ^ pas encore testé | ||
| 59 | */ | ||
| 60 | |||
| 61 | // => on remplace tout par des _ | ||
| 62 | // filtrer / et \ semble inutile | ||
| 63 | |||
| 64 | /*$cibles = [' ', '/', '\\', ':', '*', '?', '<', '>', '|', '=', "'", '`', '"', '%22', '#']; | ||
| 65 | $chaine = str_replace($cibles, '_', $chaine); // nécéssite l'extension mbstring | ||
| 66 | $chaine = mb_strtolower($chaine); | ||
| 67 | return($chaine);*/ | ||
| 68 | |||
| 69 | $chaine = preg_replace('/[^a-zA-Z0-9_-]/', '_', $chaine); // ne garder que les lettres, chiffres, tirets et underscores | ||
| 70 | $chaine = preg_replace('/_+/', '_', $chaine); // doublons d'underscores | ||
| 71 | return trim($chaine, '_'); | ||
| 72 | |||
| 73 | // les problèmes avec \ persistent !! | ||
| 74 | // => javascript | ||
| 75 | // malheureusement document.getElementById('upload').files[0].name = chaine; ne marche pas! interdit! | ||
| 76 | // javascript ne doit pas pouvoir accéder au système de fichiers | ||
| 77 | // solutions: | ||
| 78 | // - au lieu de fournir une chaine (le chemin du fichier), donner un objet à files[0].name | ||
| 79 | // - créer une copie du fichier et l'envoyer à la place | ||
| 80 | // - envoyer le fichier en AJAX | ||
| 81 | // - envoyer le nom du fichier à part puis renommer en PHP | ||
| 82 | } | ||
| 83 | } | ||
| 84 | |||
| 85 | // lien sans http:// | ||
| 86 | function fixLinks($data) | ||
| 87 | { | ||
| 88 | // 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:// | ||
| 90 | // tomber ainsi sur une page d'erreur est parfaitement déroutant | ||
| 91 | |||
| 92 | // regex pour détecter les balises <a> et ajouter http:// au début des liens si nécessaire | ||
| 93 | $pattern = '#(<a[^>]+href=")((?!https?://)[^>]+>)#'; | ||
| 94 | //$data = preg_replace($pattern, '$1http://$2', $data); | ||
| 95 | |||
| 96 | // 2/ | ||
| 97 | // cas où la regex fait mal son boulot: | ||
| 98 | // l'erreur 404 est gérée par le .htaccess | ||
| 99 | // et le visiteur est redirigé à la page "menu" | ||
| 100 | // (ça ne règle pas le problème mais c'est mieux) | ||
| 101 | |||
| 102 | // 3/ | ||
| 103 | // quand l'éditeur est ouvert (avant de valider l'article), | ||
| 104 | // le lien qu'on vient de créer apparaît dans l'infobulle, | ||
| 105 | // cliquer dessus ouvre un onglet sur une erreur 404 | ||
| 106 | // solution partielle avec le .htaccess | ||
| 107 | // | ||
| 108 | // solution? fermer ce nouvel onglet avec echo '<SCRIPT>javascript:window.close()</SCRIPT>'; | ||
| 109 | // comment déclencher le JS? en faisant qu'une erreur 404 causée pour cette raison soit particulière? | ||
| 110 | |||
| 111 | return($data); | ||
| 112 | } | ||
diff --git a/src/Service/URL.php b/src/Service/URL.php new file mode 100644 index 0000000..035f572 --- /dev/null +++ b/src/Service/URL.php | |||
| @@ -0,0 +1,90 @@ | |||
| 1 | <?php | ||
| 2 | // src/Service/URL.php | ||
| 3 | |||
| 4 | declare(strict_types=1); | ||
| 5 | |||
| 6 | namespace App\Service; | ||
| 7 | |||
| 8 | class URL implements \Stringable | ||
| 9 | { | ||
| 10 | static private string $protocol = 'http://'; | ||
| 11 | static private string $host = ''; | ||
| 12 | static private string $port; | ||
| 13 | static private string $path = '/index.php'; | ||
| 14 | private array $params; | ||
| 15 | private string $anchor = ''; | ||
| 16 | |||
| 17 | public function __construct(array $gets = [], string $anchor = ''){ | ||
| 18 | $this->params = $gets; | ||
| 19 | if($anchor != ''){ | ||
| 20 | $this->setAnchor($anchor); | ||
| 21 | } | ||
| 22 | } | ||
| 23 | |||
| 24 | // setters statiques | ||
| 25 | static public function setProtocol(string $protocol = 'http'): void | ||
| 26 | { | ||
| 27 | self::$protocol = $protocol === 'https' ? 'https://' : 'http://'; | ||
| 28 | } | ||
| 29 | static public function setPort(int|string $port = 80): void | ||
| 30 | { | ||
| 31 | if((int)$port === 443){ | ||
| 32 | self::$protocol = 'https://'; | ||
| 33 | self::$port = ''; | ||
| 34 | } | ||
| 35 | elseif((int)$port === 80){ | ||
| 36 | self::$protocol = 'http://'; | ||
| 37 | self::$port = ''; | ||
| 38 | } | ||
| 39 | else{ | ||
| 40 | self::$port = ':' . (string)$port; | ||
| 41 | } | ||
| 42 | } | ||
| 43 | static public function setHost(string $host): void | ||
| 44 | { | ||
| 45 | self::$host = $host; | ||
| 46 | } | ||
| 47 | static public function setPath(string $path): void | ||
| 48 | { | ||
| 49 | self::$path = '/' . ltrim($path, '/'); | ||
| 50 | } | ||
| 51 | |||
| 52 | //setters normaux | ||
| 53 | public function addParams(array $gets): void | ||
| 54 | { | ||
| 55 | // array_merge est préféré à l'opérateur d'union +, si une clé existe déjà la valeur est écrasée | ||
| 56 | $this->params = array_merge($this->params, $gets); | ||
| 57 | } | ||
| 58 | public function setAnchor(string $anchor = ''): void | ||
| 59 | { | ||
| 60 | if($anchor != ''){ | ||
| 61 | $this->anchor = '#' . ltrim($anchor, '#'); | ||
| 62 | } | ||
| 63 | else{ | ||
| 64 | $this->anchor = ''; | ||
| 65 | } | ||
| 66 | } | ||
| 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 | ||
| 87 | { | ||
| 88 | return self::$protocol . self::$host . self::$port . self::$path . $this->makeParams() . $this->anchor; | ||
| 89 | } | ||
| 90 | } \ No newline at end of file | ||
diff --git a/src/Service/User.php b/src/Service/User.php new file mode 100644 index 0000000..bd4c315 --- /dev/null +++ b/src/Service/User.php | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | <?php | ||
| 2 | // src/Service/User.php | ||
| 3 | |||
| 4 | declare(strict_types=1); | ||
| 5 | |||
| 6 | namespace App\Service; | ||
| 7 | |||
| 8 | use Doctrine\ORM\EntityManager; | ||
| 9 | |||
| 10 | class User{ | ||
| 11 | static public function existUsers(EntityManager $entityManager): bool | ||
| 12 | { | ||
| 13 | if(!$entityManager // table vide | ||
| 14 | ->createQuery("SELECT u FROM App\Entity\User u") | ||
| 15 | ->setMaxResults(1) | ||
| 16 | ->getOneOrNullResult()) | ||
| 17 | { | ||
| 18 | unset($_SESSION['user']); | ||
| 19 | return false; | ||
| 20 | } | ||
| 21 | else{ | ||
| 22 | return true; | ||
| 23 | } | ||
| 24 | } | ||
| 25 | } \ No newline at end of file | ||
diff --git a/src/Service/session.php b/src/Service/session.php new file mode 100644 index 0000000..39c6ba2 --- /dev/null +++ b/src/Service/session.php | |||
| @@ -0,0 +1,94 @@ | |||
| 1 | <?php | ||
| 2 | // src/Service/session.php | ||
| 3 | |||
| 4 | // à voir si c'est mieux avec: | ||
| 5 | //use Symfony\Component\HttpFoundation\Session\Session; | ||
| 6 | |||
| 7 | // note: session_regenerate_id(true) se trouve dans UserController::connect | ||
| 8 | |||
| 9 | declare(strict_types=1); | ||
| 10 | |||
| 11 | use Doctrine\ORM\EntityManager; | ||
| 12 | use App\Service\URL; | ||
| 13 | use App\Controller\UserController; | ||
| 14 | |||
| 15 | function startSession(EntityManager $entityManager): void | ||
| 16 | { | ||
| 17 | ini_set('session.cookie_samesite', 'Strict'); | ||
| 18 | ini_set('session.cookie_httponly', 'On'); | ||
| 19 | ini_set('session.use_strict_mode', 'On'); | ||
| 20 | ini_set('session.cookie_secure', 'On'); | ||
| 21 | session_start(); | ||
| 22 | validateSession($entityManager); | ||
| 23 | } | ||
| 24 | |||
| 25 | function validateSession(EntityManager $entityManager): void | ||
| 26 | { | ||
| 27 | if(defined('IS_ADMIN')){ | ||
| 28 | return; | ||
| 29 | } | ||
| 30 | |||
| 31 | $is_admin = false; | ||
| 32 | |||
| 33 | if(isset($_SESSION['user']['id'])){ | ||
| 34 | $user = UserController::getUserById($_SESSION['user']['id'], $entityManager); | ||
| 35 | |||
| 36 | // visiteur normal | ||
| 37 | if(!$user){ | ||
| 38 | session_unset(); | ||
| 39 | session_destroy(); | ||
| 40 | header('Location: ' . new URL(['message' => 'session_invalide'])); | ||
| 41 | die; | ||
| 42 | } | ||
| 43 | |||
| 44 | // MAJ de la session avec CERTAINES données | ||
| 45 | $_SESSION['user']['username'] = $user->getLogin(); | ||
| 46 | $_SESSION['user']['role'] = $user->getRole(); | ||
| 47 | |||
| 48 | $is_admin = $user->getRole() === 'admin'; | ||
| 49 | } | ||
| 50 | |||
| 51 | define('IS_ADMIN', $is_admin); | ||
| 52 | |||
| 53 | // si on a un jour besoin d'une variable globale au lieu d'une constante | ||
| 54 | //$GLOBALS['is_admin'] = $is_admin; // version modifiable 1 | ||
| 55 | /*function isAdmin(): bool { // version modifiable 2 | ||
| 56 | return $_SESSION['user']['role'] ?? null === 'admin'; | ||
| 57 | }*/ | ||
| 58 | |||
| 59 | |||
| 60 | // => système de cache à ajouter pour ne pas lire la BDD à chaque fois | ||
| 61 | //remplacer ce qui est en haut | ||
| 62 | /*$user = $_SESSION['user'] ?? null; | ||
| 63 | if (!$user) { | ||
| 64 | // visiteur | ||
| 65 | } | ||
| 66 | // Vérification périodique (ex: toutes les 5 minutes) | ||
| 67 | if (time() - $user['last_check'] > 300) { | ||
| 68 | $user = UserController::getUserById($user['id'], $entityManager); | ||
| 69 | if (!$user) { | ||
| 70 | session_destroy(); | ||
| 71 | header('Location: /login.php'); | ||
| 72 | exit; | ||
| 73 | } | ||
| 74 | // cache pour ne pas avoir à lire la BDD à chaque page | ||
| 75 | $_SESSION['user'] = [ | ||
| 76 | 'id' => $user['id'], | ||
| 77 | 'role' => $user['role'], | ||
| 78 | 'username' => $user['username'], | ||
| 79 | 'last_check' => time() | ||
| 80 | ]; | ||
| 81 | $user = $_SESSION['user']; | ||
| 82 | } | ||
| 83 | $is_admin = ($user['role'] === 'admin');*/ | ||
| 84 | |||
| 85 | |||
| 86 | // améliorations possibles: ajouter expiration automatique + protection contre vol de session (IP / user-agent) sans casser ton app. | ||
| 87 | } | ||
| 88 | |||
| 89 | // nettoyage complet | ||
| 90 | /*function cleanSession(){ | ||
| 91 | unset($_SESSION['user']); // mémoire vive | ||
| 92 | session_destroy(); // fichier côté serveur | ||
| 93 | setcookie('PHPSESSID', '', time() - 86400, '/'); // cookie de session | ||
| 94 | }*/ \ No newline at end of file | ||
