Objectifs
L’objectif de cette activité est de travailler sur la couche vue / présentation d’une application MVC, et d’expérimenter :
- le rendu HTML côté serveur (SSR) ;
- l’amélioration de l’expérience utilisateur grâce au fil d’ariane ;
- les techniques de mise en tampon qui permettent d’améliorer l’organisation du code ;
- les techniques de mise en cache pour optimiser les performances et économiser les ressources processeur et mémoire du serveur.
De plus, cette activité prend en compte les problématiques de cyber-sécurité pour prévenir les failles XSS et les fuites de données.
Enfin, l’utilisation d’un cache est l’occasion de réviser les concepts de processus, d’utilisateur et les droits POSIX.
Travail à faire
- Déployer le code de départ.
- Remplacer la route par défaut.
- Créer et utiliser les vues.
- Mettre en place le cache.
Étude
- Étudier le cours sur la vue, le tampon et le cache.
- Analyser le code de la classe
Cachedu cadriciel TeachFrame :- justifier en particulier l’importance du
chmod; - repérer la récursivité et indiquer le cas d’arrêt.
- justifier en particulier l’importance du
- Analyser le code des méthodes
indexetrenderde la classeview\IndexViewpour comprendre comment le contenu spécifique est injecté dans le code HTML commun aux différentes pages. - Faire une trace du code permettant de générer le fil d’ariane, lorsque la
méthode
renderde la classeview\IndexViewest appelée avec en paramètre :$title = '/Archipels/Îles de la Société'$uri = '/archipelago/1'
- Expliquer comment la protection contre les fuites de données (accès direct
aux fichiers du cache) a été mise en œuvre, alors que le chargement des ressources
(fichiers
cssentre autres) est possible.
- Créer un nouveau dossier
vueet y extraire le code de départ, de sorte à ce que l’URL de l’application soit de la formehttps://web.sio.local/p.nom4/vue. - Corriger la directive
RewriteBasedu fichier.htaccess. - Adapter la configuration du fichier
config.yaml. - Ajouter la classe
Cachedu cadriciel TeachFrame. - Ajouter les ressources :
- celles en provenance de tiers vont dans le dossier
lib/static: feuille de stylew3.css, fichiers pour la police d’icones Fontello, et drapeau de la Polynésie (favicon.png) ; - le fichier
styles.cssva dans le dossierview/static.
- celles en provenance de tiers vont dans le dossier
- Vérifier le bon fonctionnement de l’application avant de continuer.
Objectifs
La route par défaut — qui déclenchait une redirection vers ‘/islands’ — doit dorénavant afficher une page d’accueil, avec des liens de navigation pour accéder aux îles et archipels.
De plus, pour améliorer l’expérience utilisateur, toutes les pages de l’application vont être dotée d’un fil d’ariane (breadcrumb) ; ce code est commun à l’ensemble des vues.
Travail à faire
- Créer la classe
IndexView(cf code en annexe). - Relire les objectifs, analyser le code de la classe
IndexView, réfléchir, et corriger la route par défaut. - Vérifier le bon fonctionnement de l’application avec la route par défaut ;
il ne doit pas y avoir d’erreur
404(Not Found) lors du chargement des ressources — cf onglet “réseau” des outils de développement.
Annexe - code de la classe IndexView
<?php
namespace view;
use teachframe\Router;
class IndexView {
public function render(string $title, string $uri, string $content): string {
$uriParts = explode('/', $uri);
$titleParts = explode('/', $title);
ob_start(); ?>
<!DOCTYPE html>
<html lang="fr"><head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="stylesheet" href="<?=Router::getURL('/lib/static/w3.css')?>">
<link rel="stylesheet" href="<?=Router::getURL('/lib/static/fontello.css')?>">
<link rel="icon" type="image/png" href="<?=Router::getURL('/lib/static/favicon.png')?>"/>
<link rel="stylesheet" href="<?=Router::getURL('/view/static/styles.css')?>">
<title><?=htmlspecialchars($title)?></title>
</head><body>
<header class="w3-indigo w3-container"><ul class="w3-ul w3-large w3-bar">
<li class="w3-bar-item"><a href="<?=Router::getURL("/")?>"><span title="Accueil" class="icon-home-1"><i> ¤ </i></span></a></li>
<?php for ($i = 1; $i < count($titleParts); $i++): ?>
<li class="w3-bar-item icon-right-open"><i> > </i></li>
<?php if (isset($uriParts[$i])): ?>
<?php $uriPart = Router::getURL(implode('/', array_slice($uriParts, 0, $i+1))); ?>
<li class="w3-bar-item"><a href="<?=$uriPart?>"><?=htmlspecialchars($titleParts[$i])?></a></li>
<?php else: ?>
<li class="w3-bar-item"><?=htmlspecialchars($titleParts[$i])?></li>
<?php endif; ?>
<?php endfor; ?>
</ul></header>
<main class="w3-margin-top">
<?=$content?>
</main>
</body></html>
<?php return ob_get_clean();
}
public function index() { //route par défaut
ob_start(); ?>
<h1 class="w3-center w3-xlarge">PolyApp</h1>
<div class="w3-container w3-margin-top"><ul class="w3-ul w3-card">
<li><a href="<?=Router::getURL("/island")?>">Îles</a></li>
<li><a href="<?=Router::getURL("/archipelago")?>">Archipels</a></li>
</ul></div>
<?php $content = ob_get_clean();
echo $this->render('PolyApp', '', $content);
}
}
Explications
La fonction ob_start active la mise en tampon (attente) de ce qui aurait
sinon été envoyé au navigateur ; la fonction ob_get_clean renvoie le contenu
du tampon, qui peut ensuite être injecté dans le reste du document HTML.
Travail à faire
Créer la classe
IslandView(cf code en annexe).Adapter le code du contrôleur
IslandControllerpour qu’il utilise la classeIslandViewpour le rendu HTML ; exemple :
class IslandController {
function getAll() {
$islands = ORM::getAll(Island::class); //extraction des données
$html = new IslandView()->renderAll($islands); //rendu HTML
echo $html;
}
Vérifier la résistance de l’application au XSS en insérant en base de données l’île "Huahine<script>alert(‘XSS’);</script>”, 74 km², 6313 habitants, (archipel des îles de la Société).
Créer la classe
ArchipelagoViewet adapter le controllerArchipelagoController.
Code de la classe IslandView
<?php
namespace view;
use model\Island;
use teachframe\Router;
class IslandView extends IndexView {
public function renderAll(array $islands): string {
ob_start(); ?>
<ul class="w3-ul">
<?php foreach ($islands as $island): ?>
<li><a href="<?=Router::getURL('/island/' . $island->getId())?>"><?=htmlspecialchars($island->getName())?></a></li>
<?php endforeach; ?>
</ul>
<?php $content = ob_get_clean();
return $this->render('/Îles', '/island', $content);
}
public function renderOne(Island $island): string {
$archipelago = $island->getArchipelago();
ob_start(); ?>
<div class="w3-margin-top w3-container">
<dl class="w3-card w3-padding w3-grid">
<dt>Île :</dt><dd><?=htmlspecialchars($island->getName())?></dd>
<dt>Population :</dt><dd><?=$island->getPopulation()?> habitants</dd>
<dt>Surface :</dt><dd><?=$island->getArea()?> km²</dd>
<dt>Archipel :</dt><dd><a href="<?=Router::getURL('/archipelago/' . $archipelago->getId())?>"><?=htmlspecialchars($archipelago->getName())?></a></dd>
</dl>
</div>
<?php $content = ob_get_clean();
return $this->render('/Îles/' . $island->getName(), '/island/' . $island->getId(), $content);
}
}
Explications
Le code des classes de présentation reprend celui qu’il y avait initialement dans le contrôleur, et utilise le cadriciel W3.CSS pour soigner la présentation.
De même que les contrôleurs ont des méthodes getAll et getOne, les vues ont
des méthodes renderAll et renderOne. Ces méthodes reçoivent des données
en paramètre et renvoient le code HTML généré au contrôleur (elle n’envoient
rien au navigateur).
Toutes les chaînes de caractères en provenance de la base de données, potentiellement
non sûres, sont encodées avec la fonction htmlspecialchar au moment du rendu HTML.
Objectifs
A ce stade, le serveur extrait les données et effectue le rendu HTML à chaque requête HTTP. Pour le soulager et améliorer les performances de l’application, le code HTML généré va être mis en cache dans un fichier de façon à pouvoir être réutilisé.
Problèmes à résoudre
Les fichiers de cache sont écrits par le serveur, qui doit pour cela disposer des droits POSIX nécessaires. Il y a pour cela plusieurs possibilités :
- accorder à tout le monde le droit d’écrire dans le dossier
cache:chmod 777 cache; - changer le propriétaire ou le groupe d’appartenance du dossier
cachepour accorder les droits d’écriture uniquement au processus sous l’identité duquel s’exécute le serveur web (Apache) ; cette solution n’est pas possible dans le cas de cet hébergement mutualisé car les commandeschownetchgrpdoivent être exécutées par l’utilisateurroot.
De plus, les sous-dossiers et fichiers de cache seront crées par le serveur, et lui appartiendront. Le système de cache doit permettre au développeur de les supprimer…
Travail à faire
- Créer à la racine de l’application (dans le dossier
vue) un dossiercache. - Vérifier l’utilisateur sous l’identité duquel s’exécute le serveur web —
(cf commandes
ss -pltnetps aux), puis les droits du dossierscache(cf commandels -l). - Attribuer les droits nécessaires au dossier
cache. - Adapter le code des contrôleurs
IslandControlleretArchipelagoControllerpour qu’ils mettent en œuvre le cache. (cf exemple en annexe). - Vérifier que les fichiers de cache sont générés (actualiser le dossier
cache). - Vérifier que les fichiers de cache ne sont pas accessibles directement (en indiquant leur URL dans la barre d’adresse du navigateur).
Exemple de contrôleur
use teachframe\Cache;
class IslandController {
function getAll() {
$html = Cache::get('/islands'); //chargement du cache
if (null == $html) { //si cache absent
$islands = ORM::getAll(Island::class); //extraction des données
$html = new IslandView()->renderAll($islands); //rendu HTML
Cache::set('/islands', $html); //enregistrement du cache
}
echo $html;
}
Pour la méthode getOne, la ressource est ‘/island/1’, ‘/island/2’… avec
“island” au singulier (car il n’est pas possible d’avoir un dossier et un
fichier de même nom) : $html = Cache::get('/island/' . $id);