Présentation de l’activité

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 Cache du cadriciel TeachFrame :
    • justifier en particulier l’importance du chmod ;
    • repérer la récursivité et indiquer le cas d’arrêt.
  • Analyser le code des méthodes index et render de la classe view\IndexView pour 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 render de la classe view\IndexView est 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 css entre autres) est possible.

I - Déployer le code de départ

  • Créer un nouveau dossier vue et y extraire le code de départ, de sorte à ce que l’URL de l’application soit de la forme https://web.sio.local/p.nom4/vue.
  • Corriger la directive RewriteBase du fichier .htaccess.
  • Adapter la configuration du fichier config.yaml.
  • Ajouter la classe Cache du cadriciel TeachFrame.
  • Ajouter les ressources :
    • celles en provenance de tiers vont dans le dossier lib/static : feuille de style w3.css, fichiers pour la police d’icones Fontello, et drapeau de la Polynésie (favicon.png) ;
    • le fichier styles.css va dans le dossier view/static.
  • Vérifier le bon fonctionnement de l’application avant de continuer.

II - Remplacer la route par défaut

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>&nbsp;¤&nbsp;</i></span></a></li>
    <?php for ($i = 1; $i < count($titleParts); $i++): ?>
    <li class="w3-bar-item icon-right-open"><i>&nbsp;&gt;&nbsp;</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.

III - Créer et utiliser les vues

Travail à faire

  • Créer la classe IslandView (cf code en annexe).

  • Adapter le code du contrôleur IslandController pour qu’il utilise la classe IslandView pour 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 ArchipelagoView et adapter le controller ArchipelagoController.

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.

IV - Mettre en place le cache

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 cache pour 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 commandes chown et chgrp doivent être exécutées par l’utilisateur root.

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 dossier cache.
  • Vérifier l’utilisateur sous l’identité duquel s’exécute le serveur web — (cf commandes ss -pltn et ps aux), puis les droits du dossiers cache (cf commande ls -l).
  • Attribuer les droits nécessaires au dossier cache.
  • Adapter le code des contrôleurs IslandController et ArchipelagoController pour 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);