Skip to content

Coupon - partie 2

Cette leçon fait suite à la leçon "03 - coupon1" et nécessite que l'application développée en partie 1 soit fonctionnelle. Reportez-vous donc $ la première partie si vous ne l'avez pas encore fait.

Durant cette leçon, nous allons apporter différentes retouches à l'application pour étendre la description, améliorer l'interface de saisie ainsi que la recherche et mettre en place une validation de données.

Modèle "Magasin"

Notre description ne comporte actuellement qu'un seul modèle "coupon". Nous allons ajouter un nouveau modèle "magasin" et faire en sorte que l'on puisse référencer un magasin depuis un coupon.

👉 Éditez le fichier schema.xml et ajoutez la définition de table suivante en-dessous de la table Coupon:

1
2
3
4
5
<table name="Magasin">
  <column name="idMagasin" type="int" isPrimaryKey="true"/>
  <column name="Nom" type="varchar(100)"/>
  <column name="SiteInternet" type="varchar(250)"/>
</table>

La nouvelle table est très simple et se passe de commentaire. Il s'agit à présent de remplacer notre colonne Magasin dans la table Coupon par une référence vers la nouvelle table. Le but recherché est d'avoir une liste déroulante de magasins à sélectionner lorsque l'on crée un coupon. Pour ce faire:

👉 Dans le même fichier au niveau de la table Coupon, remplacez la ligne

<column name="Magasin" type="varchar(100)"/>

par

<column name="idMagasin" type="int" foreignReference="Magasin"/>

L'attribut foreignReference indique quelle table, voire quel champ référence notre champ. La valeur de l'attribut peut avoir deux formes: TABLE.COLUMN ou TABLE. Dans le premier cas, la référence est explicite. Dans la seconde, le moteur va automatiquement baser la référence sur la clé primaire de la table indiquée, à condition que cette dernière possède une clé primaire basée sur une seule colonne. Dans le cas où la table utilise une clé primaire composée de plusieurs colonnes, la notation TABLE.COLUMN est obligatoire. Dans notre exemple, la colonne idMagasin sera une référence vers la colonne idMagasin de la table Magasin.

La mise à jour de la description est également relativement aisée à comprendre.

👉 Éditez le fichier descript.xml et ajoutez le modèle suivant à la suite du modèle coupon.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<model name="magasin" maingroup="base">
  <groups>
    <group name="base" table="Magasin" type="single" mainfield="idMagasin">
      <fields>
        <field name="idMagasin" type="hidden" column="idMagasin"/>
        <field name="nom" type="text" column="Nom"/>
        <field name="siteInternet" type="text" column="SiteInternet"/>
      </fields>
    </group>
  </groups>
  <views>
    <view name="default" style="documents">
      <group name="base"/>
    </view>
  </views>
</model>

👉 Dans le même fichier au niveau du modèle coupon, remplacez la ligne

<field name="nomMagasin" type="text" column="Magasin"/>

par

1
2
3
4
5
<field name="idMagasin" type="select" column="idMagasin">
  <options mode="sql">
    <value>select idMagasin,Nom from Magasin order by Nom</value>
  </options>
</field>

Ce champ apporte une nouveauté. Ce champ annonce un type "select". Le type est indique à la feuille de style qu'elle doit afficher une liste déroulante. Pour alimenter la liste déroulante, on s'appuie sur une requête SQL.

👉 Ouvrez l'application depuis un navigateur via l'URL http://localhost:8080/ewt/web/coupon. Cliquez sur Fichier > Reset pour forcer un rechargement du schéma et de la description.

👉 Cliquez sur Admin > Générer un canevas de langue. Sélectionner les nouvelles lignes du canevas, c'est-à-dire celles qui se trouvent à partir de # model magasin, collez-les dans le fichier i18n/descript_fr.properties de l'application et ajoutez les libellés.

1
2
3
4
5
6
7
8
9
[...]

# model magasin
model.magasin.label = 🛒 Magasin

group.magasin.base.label = Données de base
field.magasin.base.idMagasin.label = Identifiant magasin
field.magasin.base.nom.label = Nom
field.magasin.base.siteInternet.label = Site internet

Dans l'exemple ci-dessus, nous avons décidé d'ajouter une emoji sur le modèle "Magasin". Vous pouvez adapter le libellé à votre guise.

👉 Remplacez la ligne ayant pour clé field.coupon.base.nomMagasin.label par

field.coupon.base.idMagasin.label = Magasin`

👉 Revenez sur l'application, cliquez sur Admin > Reset

Notre nouveau modèle est prêt. Il reste à préparer la base de données.

👉 Cliquez sur Admin > Créer la base de données (alter)

La commande devrait générer passablement d'erreurs dans le log car certains requêtes vont chercher à recréer des éléments qui existent déjà. Vous pouvez ignorer ces erreurs.

Vérifions à présent que notre nouveau modèle fonctionne.

👉 Cliquez sur Fichier > Nouveau > 🛒 Magasin. Vous pouvez saisir un nom de magasin et une adresse internet.

👉 Créez un second magasin de la même façon.

Multi-dossiers

Si vous avez créé le nouveau dossier alors que le premier était encore ouvert, vous noterez que les deux dossiers sont ouverts en même temps.

img-04-01.png

En effet, par défaut Ewt garde ouvert les dossiers jusqu'à ce qu'ils soient explicitement fermés (par l'utilisateur ou par un script) ou que la session expire. C'est le fonctionnement par défaut du moteur.

Il est possible de demander au moteur de gérer les dossiers de façon unitaire. Pour cela, il faut définir la propriété suivante dans le bloc admin:

<documentMode>single</documentMode>

Dans la version actuelle du moteur, la propriété peut prendre les valeurs singleou multi. Le mode par défaut est le mode single.

On notera au passage que la ligne d'entête de l'interface affiche une liste déroulante avec la mention "2 dossiers ouverts". Le menu déroulant permet de ré-afficher les dossiers (par exemple s'ils ne sont plus visibles à l'écran, ce qui arrive par exemple si on clique sur le nom d'application dans le coin supérieur gauche) ou de les fermer tous.

Voyons à présent ce qu'il en est du modèle coupon.

👉 Fermez les dossiers de magasins et lancez une recherche. Ouvrez un dossier du modèle coupon.

On remarque que le nouveau champ "Magasin" affiche à présent une liste déroulante qui reprend les magasins créés précédemment.

img-04-02.png

Champ "Statut"

Nous allons à présent nous concentrer sur le champ "Statut". Ce champ est censé enregistrer le statut du coupon et indiquer s'il est disponible ou utilisé.

Type "switch"

Nous allons travailler plusieurs éléments sur ce champ. Commençons par sa forme. Le champ de type texte actuel n'est pas pratique et nous allons le remplacer par un autre type de contrôle.

👉 Éditez le fichier descript.xml et modifiez le type du champ statut de text à switch.

👉 Revenez sur l'application et cliquez sur Fichier > Reset. Le champ devrait aborder une nouvelle forme:

img-04-03.png

Lorsque l'on crée un nouveau coupon, on aimerait que celui-ci soit immédiatement disponible. Par conséquent le statut par défaut devrait être 1 et non pas 0. Il est possible de demander à Ewt de renseigner une valeur par défaut.

Valeur par défaut

👉 Modifiez la description du champ Statut pour lui donner la forme ci-dessous. Effectuez un nouveau Reset puis créer un nouveau coupon via le menu Fichier.

1
2
3
4
5
<field name="statut" type="switch" column="Statut">
  <default mode="text">
    <value>1</value>
  </default>
</field>

Le coupon devrait à présent avoir un statut "actif" dès la création.

Données binaires

On aimerait à présent pouvoir rattacher à notre coupon une copie numérique du coupon. Pour cela, nous allons ajouter un champ de type blob.

👉 Modifiez le fichier schema.xml et ajoutez les descriptions de colonnes suivantes dans la table Coupon:

1
2
3
4
<column name="Fichier" type="blob"/>
<column name="NomFichier" type="varchar(100)"/>
<column name="TailleFichier" type="integer"/>
<column name="MimeTypeFichier" type="varchar(100)"/>

Ces colonnes permettront de sauvegarder le fichier, son nom, sa taille et son mime-type. Le mime-type permet de décrire le type de fichier. Ainsi, un fichier pdf sera décrit au moyen du mime-type application/pdf, un fichier jpg au moyen de image/jpeg, etc. Une liste des mime-types principaux est disponible sur https://developer.mozilla.org/fr/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types.

👉 Modifiez le fichier descript.xml et ajoutez les lignes suivantes dans le modèle coupon:

1
2
3
4
5
6
7
<field name="fichier" type="file" column="Fichier">
  <metadata>
    <attribute name="file:name" column="NomFichier"/>
    <attribute name="file:size" column="TailleFichier"/>
    <attribute name="file:mimetype" column="MimeTypeFichier"/>
  </metadata>
</field>

Le champ fichier présente plusieurs particularités:

  • Il est du type file. Le moteur s'appuie sur ce type pour savoir que le champ est un champ binaire.
    Il indique au moteur et à la feuille de style que le champ est de type binaire. Le moteur a besoin de savoir que le champ est binaire pour des questions d'optimisation.
  • Il contient des métadonnées. Les métadonnées sont des valeurs associées au champ lui-même et stockées dans des colonnes indépendantes du champ; les noms de ces métadonnées utilisent des préfixes file:, ce qui indique au moteur comment il peut interpréter ces métadonnées et les lier à l'objet "File" qu'il va se construire en mémoire.

👉 Ajoutez un libellé de champ dans le fichier i18n/descript_fr.properties comme nous l'avons déjà fait auparavant

👉 Lancez à nouveau un "Reset" puis utilisez la fonction Admin > Créer la base de données (alter) pour forcer la création des champs en base de données.

L'interface du modèle "Coupon" affiche à présent un champ "Fichier" ayant la forme suivante:

img-04-03.png

Validations

La dernière notion que nous allons aborder dans cette leçon est la notion de validation. La validation consiste à spécifier des règles de contrôle des valeurs. Ces règles remplissent plusieurs objectifs : vérifier que les valeurs ont une forme convenable, vérifier que les valeurs sont valides et qu'elles sont sûres, c'est-à-dire qu'elles ne représentent pas un risque de sécurité.

Le moteur effectue un contrôle automatique du format de valeur après formatage. Par exemple qu'un champ lié à une colonne "integer" contient bien une valeur entière. Il est toutefois possible de spécifier des règles de validation personnalisées.

Contrôle (check)

Nous allons mettre en place un contrôle qui vérifie qu'un libellé est bien saisi pour le champ "Nom" du modèle "Magasin". Il existe plusieurs façons de réaliser ce type de test. Nous allons passer en revue les différentes variantes possibles.

👉 Implémentez l'une des variantes ci-dessous dans votre application puis effectuez un reset pour prendre en compte la modification.

i) Règle "MANDATORY"

👉 Éditez le fichier descript.xml et modifiez le champ nom du modèle magasin ainsi:

1
2
3
<field name="nom" type="text" column="Nom">
  <validation method="rule">MANDATORY</validation>
</field>

Ici, on utilise une règle interne du moteur pour vérifier que le champ n'est pas vide.

ii) Expression régulière

👉 Éditez le fichier descript.xml et modifiez le champ nom du modèle magasin ainsi:

1
2
3
<field name="nom" type="text" column="Nom">
  <validation method="regex" source="raw">^.+$</validation>
</field>

Ici, on utilise une expression régulière sur la valeur brute (source="raw"). La valeur brute est la valeur telle que reçue du formulaire html. Dans certains cas, cette valeur brute peut différer de la valeur enregistrée en base de données (que l'on appellera "valeur standardisée").

Prenons le cas d'une date. La valeur brute pourrait être "31.12.2023" dans le cas d'une locale "fr-CH" ou "12.31.2023" dans le cas d'une locale "en-US". Le moteur traite cette valeur brute en fonction de la locale et en produit une valeur standardisée au format iso 8601 "2023-12-31".

La validation s'effectuera donc sur la base d'une valeur différente selon la source indiquée:

  • raw : valeur brute
  • std : valeur standardisée

Pour les tests de valeur, il est recommandé de s'appuyer sur la valeur standardisée car les règles de validation n'ont pas à se soucier des différentes locales possibles.

Dans le cas ci-dessus, nous référençons la source raw car le test de validation que nous effectuons n'est pas influencé par la forme de la valeur.

iii) Script

👉 Éditez le fichier descript.xml et modifiez le champ nom du modèle magasin ainsi:

1
2
3
<field name="nom" type="text" column="Nom">
  <validation method="script" source="raw" action="check">return $$.data != "";</validation>
</field>

Ici, on utilise le moteur de script pour effectuer le test de validation. La valeur brute est disponible au niveau de la variable data. Le script doit retourner la valeur true pour indiquer que la validation réussi et false pour indiquer qu'elle échoue.

Nettoyage (sanitize)

Nous allons ajouter une règle de validation pour nettoyer le champ "Remarque" du modèle "Coupon".

👉 Éditez le fichier descript.xml et modifiez le champ remarque du modèle coupon ainsi:

1
2
3
<field name="remarque" type="textarea" column="Remarque">
  <validation method="rule" action="sanitize" source="raw">FORMATTING|BLOCKS|STYLES|LINKS|TABLES|IMAGES</validation>
</field>

Ici, on applique une règle de nettoyage sur la valeur pour lui enlever tous les éléments susceptibles de contenir une injection XSS, à l'exception de certains éléments (éléments de formatage, éléments de blocs, etc.)

Les règles d'autorisation correspondent aux sanitizers définis par la librairie Owasp: https://www.javadoc.io/doc/com.googlecode.owasp-java-html-sanitizer/owasp-java-html-sanitizer/20191001.1/org/owasp/html/Sanitizers.html

👉 Effectuez un reset pour prendre en compte la modification.

👉 Ouvrez ou créez une fiche "Coupon" puis essayez de saisir la valeur suivante dans le champ "Remarque":

hello <script>alert('xss')</script> world

👉 Cliquez sur "Enregistrer"

Vous pouvez alors constater que la partie <script>...</script> de la remarque a été retirée et qu'un avertissement a été émis par l'application:

img-04-03.png

Le libellé du message d'avertissement est un message prédéfini dans le moteur. Il est possible de le personnaliser.

👉 Modifiez la règle de validation pour lui ajouter un attribut label="field.coupon.base.remarque.validation.warning"

👉 Éditez le fichier de langue i18n/descript_fr.properties et ajoutez une entrée pour la clé que nous avons référencée:

field.coupon.base.remarque.validation.warning = Vous ne pouvez pas intégrer \
  d'élément html dans la remarque

👉 Effectuez un reset puis essayez à nouveau de renseigner la valeur contenant la partie <script>...</script> dans le champ. Cliquez sur "Enregistrer".

Vous noterez que la valeur a été nettoyée mais que cette fois l'avertissement correspond à celui que vous avez défini.

Use case de validation

Ce paragraphe est optionnel. Il approfondit le use case de validation de type sanitize.

Le contexte de validation sanitize que nous avons mise en œuvre ici n'est pas représentatif d'un cas réaliste, car nous avons appliqué une règle de nettoyage html à un texte simple. Le but ici n'était pas tant de démontrer le fonctionnement du sanitize en condition réelle, mais de voir quel impact la règle de validation sanitize peut avoir sur une valeur.

D'ailleurs l'utilisation du sanitize dans ce cas de figure représente un risque de corruption de données. Saisissez par exemple la valeur hello < world dans la remarque puis cliquez sur "Enregistrer". Vous constaterez que la valeur est changée en hello &lt; world. Cela vient du fait que le sanitize a pour vocation de générer un code réutilisable en tant que html et non pas en tant que texte.

La mise en place d'un use case plus réaliste demande plus d'efforts car le jeu de styles que nous utilisons est déjà construit pour éviter le plus possible le risque de Cross Site Scripting (XSS). En effet, pour qu'une injection XSS s'exprime, il faut que la valeur soit intégrée dans la page html au moyen d'une instruction XSL du genre

<xsl:value-of select="data" disable-output-escaping="yes"/>

or le jeu de style que nous utilisons n'en effectue aucun pour des données provenant de champs (à l'exception du type de champ tinymce, mais le contrôle utilisé dans ce cas intègre son propre mécanisme de nettoyage). Pour mettre en place un use case représentatif, il faut donc construire un scénario qui provoque volontairement une faille permettant l'injection XSS (ce qui n'est évidemment pas une bonne chose pour une application). Pour ce faire, nous pouvons procéder ainsi:

👉 Créez un script _test.script avec le contenu suivant:

$msg.info(#coupon[1].info.remarque, null, { disableOutputEscaping: "yes" });

👉 Adaptez le numéro de dossier (le "1" entre crochets dans l'exemple ci-dessus) au numéro de votre dossier. Le numéro de dossier apparaît à droite du libellé "Coupon", au-dessus des boutons "Enregistrer", "Fermer" et "Supprimer" de la fiche "Coupon".

Ce script reprend la valeur du champ "Remarque" du dossier coupon ayant l'identifiant "1" (que vous devrez adapter) dans un message de type "info", en indiquant explicitement de désactiver l'échappement. Ainsi, le texte de la remarque sera interprété comme du html.

👉 Désactivez la règle de validation (en la mettant en commentaire dans le fichier descript.xml), effectuez un reset puis saisissez la valeur suivante dans le champ "Remarque":

hello <script>alert('xss')</script> <b>world</b>

À l'enregistrement, le champ "Remarque" devrait afficher une valeur inchangée. Cliquez à présent dans le menu "Fichier > Test", ce qui aura pour effet d'exécuter le script _test.xml que nous avons construit. Un popup d'alerte avec le texte "xss" devrait s'afficher après l'enregistrement. Cela signifie que l'application est à présent sujette à l'injection XSS. Notez au passage que le mot "world" apparaît en gras dans le message d'information.

👉 Réactivez alors la règle de validation, effectuez un reset puis cliquez à nouveau sur "Fichier > Test".

Le popup affichant "css" ne devrait plus apparaître car la valeur de remarque a été nettoyée. Par contre le mot "world" devrait continuer de s'afficher en gras si la règle FORMATTING a bien été reprise dans la règle de validation.

Cette illustration montre que le rôle du sanitize est de nettoyer un texte pour qu'il puisse être affiché de manière sure sous forme HTML.

Conclusion

Dans cette leçon, nous avons abordé différentes notions avancées de définitions de champs. Cela n'est de loin pas exhaustif et d'autres notions seront abordées dans les prochaines leçons.

La prochaine leçon sera orientée sur la gestion des permissions au moyen de policies. Cela nous permettra de mieux gérer l'affichage des champs en fonction des états et des droits d'accès.