Objectifs d’apprentissage
L’objectif de cette séquence qui s’inscrit dans le cadre du développement d’une application MVC est de :
- réviser le mécanisme du routage et des URL ReST ;
- étudier le rôle d’un contrôleur concernant la vérification de la validité des données reçues / en entrée ;
- s’initier aux expressions rationelles ;
- réviser les formulaires HTML ;
- consolider la maîtrise d’une architure MVC, notamment pour le partage des “responsabilités” entre vues et contrôleurs.
Travail à faire
L’objectif de la mission est de finaliser l’application de gestion des îles et archipels en implémentant le reste des opérations CRUD.
Préparation :
- récupérer le code de départ ;
- renommer le dossier
vueenctrl; - adapter les fichiers
.htaccessetconfig.yaml; - ajouter la classe
IOdu cadriciel TeachFrame ; - vérifier le bon fonctionnement de l’application.
Il est recommandé de désactiver le cache pendant la mise au point de l’application ;
pour cela, ajouter dans index.php l’instruction Cache::$disabled = true;
(avant dispatch).
Réalisation : adapter les routes, les vues et les contrôleurs pour prendre en compte les contraintes. Les indications sont volontairement restreintes afin de susciter la réflexion.
Analyse : une analyse détaillée du code est nécessaire pour développer les compétences. Les questions de réflexion abordent les points essentiels.
Écrans et contrôles à ajouter
- écran d’édition (du nom) d’un archipel ;
- bouton pour créer un archipel depuis la liste des archipels ;
- bouton pour éditer un archipel depuis le détail d’un archipel ;
- si l’archipel n’a pas d’île, bouton de suppression d’un archipel depuis le détail d’un archipel ;
- écran d’édition d’une île — le choix de l’archipel doit s’effectuer à partir d’une liste déroulante ;
- boutons pour créer une île :
- depuis la liste des îles ;
- depuis le détail d’un archipel — à pré-selectionner sur l’écran d’édition ;
- bouton pour éditer une île depuis le détail d’une île ;
- bouton de suppression d’une île depuis le détail d’une île,
Rappel d’ergonomie : lors de l’édition d’un objet (ici un archipel ou une île), les contrôles (champs de saisie, boutons radios, cases à cocher, listes déroulantes…) doivent être initialisés avec les données de l’objet.
Routes
Dans le respect du style d’architecture ReST, quatre routes doivent être ajoutées
- pour afficher l’écran d’édition d’un archipel :
GET /archipelago/id/edit—id = 0dans le cas d’un nouvel archipel ; - pour créer, modifier ou supprimer un archipel :
POST /archipelago/id: - création lorsqueid = 0,- modification lorsque
id != 0, - suppression lorsque
id != 0et qu’aucune données n’est envoyée ;
- modification lorsque
- pour afficher l’écran d’édition d’une île :
GET /island/id/edit; - pour créer, modifier ou supprimer une île :
POST /island/id.
Rappel de sécurité : les opérations qui changent l’état du système (typiquement
l’ajout, la modification ou la suppression de données) doivent impérativement
se faire avec la méthode HTTP POST.
Important : dans le cas d’une application réelle, il faut toujours demander confirmation avant d’effectuer la suppression d’un objet. Pour cela, un écran supplémentaire de confirmation (avec une route associés) est nécessaire.
Persistance d’une île
Lors d’une opération impactant l’état du système, le contrôleur doit effectuer
certaines vérifications en considérant que l’utilisateur est malveillant et
détourne l’utilisation de l’application. En effet, lors d’une utilisation
normale, aucune instructions die ne devrait s’exécuter.
function save(string $id) {
if (0 == $id) {
$island = new Island(0, '', -1, -1, new Archipelago(0, ''));
} else {
$island = ORM::getOne(Island::class, $id);
if (null == $island) {
http_response_code(404);
die('error: \'/island/' . $id . '\' not found');
}
}
if (IO::hasInput()) {
$inputs = IO::getInput();
IO::validateInput(['name:*s', 'population:*i', 'area:*i', 'idArchipelago:*i'], $inputs);
$archipelago = ORM::getOne(Archipelago::class, $inputs->idArchipelago, false);
if (null == $archipelago) {
http_response_code(400);
die('error: integrity constraint failed (idArchipelago = ' . $inputs->idArchipelago . ')');
}
$island->setName($inputs->name);
$island->setPopulation($inputs->population);
$island->setArea($inputs->area);
$island->setArchipelago($archipelago);
ORM::persist($island);
} else if (0 != $id) { //sans données, c'est une suppression
ORM::delete($island);
} else {
http_response_code(400);
die('error: cannot delete \'/island/0\'');
}
Cache::resetAll();
header('Location: ' . Router::getURL('/island'));
}
Suppression d’un archipel
Les boutons de suppressions doivent déclencher la soumission d’un formulaire
sans données, avec la méthode POST ; une solution consiste :
- à utiliser un formulaire vide (donc sans affichage), mais avec un identifiant ;
- à utiliser un bouton, associé au formulaire.
En raison des contraintes d’intégrité référentielle, la suppression d’un archipel n’est possible que si ce dernier n’a pas d’île ; plusieurs approches sont possibles :
- lors de la suppression d’un archipel, toutes ses îles sont également supprimées ;
- la suppression d’un archipel avec des îles est interdite, approche retenue ici.
L’ajout du bouton de suppression est conditionné à l’absence d’îles pour cet
archipel (cf count($archipelago->getIslands()) == 0) :
<form id="delete" method="POST" action="<?=Router::getURL('/archipelago/' . $archipelago->getId())?>"></form>
<p class="w3-bar">
<a class="w3-bar-item w3-button w3-indigo w3-right" href="<?=Router::getURL('/archipelago/' . $archipelago->getId() . '/edit')?>"> Éditer </a>
<?php if (count($archipelago->getIslands()) == 0): ?>
<button form="delete" class="w3-bar-item w3-button w3-red">Effacer</button>
<?php endif; ?>
</p>
Du côté du contrôleur, il faut prendre en considération l’utilisation malveillante
de l’application : un utilisateur pourrait forger une requête HTTP avec curl
ou une extension comme Rester,
même en l’absence de bouton sur l’interface de l’application. Lors d’une opération
de suppression, le contrôleur doit donc vérifier l’absence d’îles :
if (0 != count($archipelago->getIslands())) {
http_response_code(403);
die('error: \'/archipelago/' . $id . '\' still has islands');
}
Édition d’un archipel
L’édition d’un archipel ne présente pas de difficulté, hé hé hé ;-)
public function renderEdit(Archipelago $archipelago): string {
ob_start();
?>
<div class="w3-margin-top w3-container">
<form method="POST" action="<?=Router::getURL('/archipelago/' . $archipelago->getId())?>" class="w3-card w3-padding">
<p>
<label class="w3-bold" for="name">Archipel :</label>
<input required="required" id="name" name="name" type="text"
value="<?=$archipelago->getName()?>" class="w3-input w3-border"/>
</p>
<p class="w3-bar"><button class="w3-bar-item w3-right w3-button w3-green">Enregistrer</button></p>
</form>
</div>
<?php $content = ob_get_clean();
$name = '' == $archipelago->getName() ? 'Nouveau' : $archipelago->getName();
$path = 0 == $archipelago->getId() ? '/archipelago' : '/archipelago/' . $archipelago->getId();
return $this->render('/Archipels/' . $name, $path, $content);
}
Édition d’une île
Pour éditer une île, il faut sélectionner un archipel :
- c’est le contrôleur qui récupère la liste des archipels avec l’ORM ;
- le contrôleur transmet à la vue l’île et la liste des archipels ;
- la vue pré-sélectionne l’archipel dans la liste.
Code du contrôleur :
function edit (string $id) {
if (0 == $id) { //pour éditer une nouvelle île
$referer = explode('/', Router::getReferer()); //présélection de l'archipel
//var_dump($referer);
$idArchipelago = 'archipelago' == $referer[1] && 3 <= count($referer) ? intval($referer[2]) : 0;
$island = new Island(0, '', -1, -1, new Archipelago($idArchipelago, ''));
} else { //pour éditer une île existante
$island = ORM::getOne(Island::class, $id);
if (null == $island) { //si l'île n'existe pas
http_response_code(404);
die('error: \'/island/' . $id . '\' not found');
}
}
$html = new IslandView()->renderEdit($island, ORM::getAll(Archipelago::class)); //rendu du formulaire
echo $html;
}
Code de la vue (extrait) :
<p>
<label class="w3-bold" for="idArchipelago">Archipel :</label>
<select class="w3-select" id="idArchipelago" name="idArchipelago">
<?php foreach ($archipelagos as $archipelago): ?>
<?php $selected = $archipelago->getId() == $island->getArchipelago()->getId() ? 'selected="selected"' : '';?>
<option <?=$selected?> value="<?=$archipelago->getId()?>"><?=htmlspecialchars($archipelago->getName())?></option>
<?php endforeach; ?>
</select>
</p>
Attention, la valeur associée à la liste déroulante est l’id de l’archipel…
Analyser le mécanisme de présélection de l’archipel :
- décommenter le
var_dumpet noter ce qu’il affiche lors de l’ajout d’une île :- depuis la liste des îles,
- depuis un archipel,
- par saisie directe de l’URL dans la barre d’adresse ;
- se documenter sur ce qu’est le “referer” dans le protocole HTTP ;
- faire une trace du code du contrôleur et du routeur ;
- expliquer pourquoi l’archipel associé à l’île n’a pas besoin d’être chargé
par l’ORM (cf
new Archipelago(…).
XSS
Les champs de saisie name sont vulnérables aux attaques de type XSS :
- vérifier que c’est bien le cas en réussissant une attaque ;
- corriger le code pour éliminer la faille de sécurité.
Détournement
Modifier dynamiquement le contenu du document (pour supprimer un attribut
requiredpar exemple) — cf outils de développements.Utiliser l’utilitaire en ligne de commande
curlet une interface graphique telle que Rester pour forger des requêtes HTTP valides et invalides ; exemple :curl -k -X POST -d “name=Test” https://web.sio.local/f.endr/ws/archipelago/0
L’option -k permet d’accepter les certificats SSL auto-signés.
Hameçonnage
Envoyer (à soi même) un courriel contenant un formulaire pour créer, modifier ou détruire une île…
Cela est par exemple possible avec Thunderbird en ajoutant une extension telle que HTML Source Editor.
Un bon client de messagerie, à la réception de ce message devrait afficher
un avertissement de sécurité, et remplacer la méthode POST par GET.
Méthode HTTP
Rappeler pourquoi la méthode HTTP GET est “interdite” (du point de vue de
la sécurité) pour tout changement d’état du système.
- Repérer le code redondant dans les contrôleurs et le factoriser.
- Optimiser la gestion du cache pour n’effacer que le nécessaire…
- Rendre les champs population et surface (
area) de l’île facultatifs. - Ajouter un écran de confirmation des suppressions ; exemple : code pour remplacer le bouton de suppression d’un archipel :
<p class="w3-bar">
<a class="w3-bar-item w3-button w3-indigo w3-right" href="<?=Router::getURL('/archipelago/' . $archipelago->getId() . '/edit')?>"> Éditer </a>
<?php if (count($archipelago->getIslands()) == 0): ?>
<a class="w3-bar-item w3-button w3-red" href="<?=Router::getURL('/archipelago/' . $archipelago->getId() . '/delete')?>"> Effacer </a>
<?php endif; ?>
</p>