Licence CC BY

Intéragir avec les utilisateurs

Dernière mise à jour :

Lorsque vous développez votre site, vous allez devoir demander à vos utilisateurs d’entrer des données.

Par exemple, vous allez lui demander quelle page d’une liste il veut afficher, quels sont son pseudonyme et son mot de passe, le texte d’un commentaire ou d’un article…

Ce chapitre vous permettra de percer les secrets des formulaires, d’envoyer des fichiers tels que des images et même afficher les articles de votre blog.

Nous parlerons aussi de sécurité parce que, en informatique, il y a une règle d’or : Never Trust User Input, c’est-à-dire ne faites jamais confiance à ce que les utilisateurs saisissent.

Préparation aux cas d'étude

Dans les sections suivantes, nous allons échanger des données avec nos utilisateurs. Pour cela, quatre cas d’études sont prévus :

  • afficher vos articles ;
  • la création d’un article ;
  • l’envoi d’une image ;
  • la sélection d’une page parmi plusieurs (la pagination).

Pour que ces cas d’études se passent de la meilleure des manières, il va falloir préparer votre espace de travail.

Comme nous allons manipuler des "articles", il faudra commencer par définir ce qu’est un article. Pour faire simple, nous allons supposer qu’un article possède les propriétés suivantes :

  • le pseudo de son auteur ;
  • le titre de l’article ;
  • le texte de l’article.

Dans le dossier Models, créez la classe suivante :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace Blog.Models
{
    /// <summary>
    /// Notre modèle de base pour représenter un article
    /// </summary>
    public class Article
    {
        /// <summary>
        /// Le pseudo de l'auteur
        /// </summary>
        public string Pseudo { get; set; }
        /// <summary>
        /// Le titre de l'article
        /// </summary>
        public string Titre { get; set; }
        /// <summary>
        /// Le contenu de l'article
        /// </summary>
        public string Contenu { get; set; }
    }
}

Pour aller plus vite, je vous propose un exemple de liste d’articles au format JSON. Ce fichier nous permet de stocker nos articles, nous utiliserons une base de données dans le prochain chapitre.

Téléchargez le fichier, et placez-le dans le dossier App_Data.
Puis, pour qu’il apparaisse dans Visual Studio, faites un clic droit sur le dossier App_Data, et "Ajouter un élément existant". Sélectionnez le fichier.

Il est extrêmement facile de lire un fichier de ce type, contrairement à un fichier texte. JSON est un format de données textuelles, tout comme le XML. Il est très répandu dans le monde du développement web. Le contenu d’un fichier JSON ressemble à cela :

{
    "post": {
        "pseudo": { "value": "Clem" },
        "titre": { "value": "mon article" },
        "contenu": { "value": "Du texte, du texte, du texte" }
    }
}

Maintenant, nous allons utiliser un outil qui est capable de lire le JSON. Pour cela, nous allons utiliser un package : faites un clic droit sur le nom du projet puis "Gérer les packages nuget". Cherchez JSON.NET et installez-le.

Pour ceux qui veulent savoir ce qu’est NuGet, c’est simplement une bibliothèque de packages, qui regroupe de nombreuses librairies et DLL pour la plateforme de développement sous Microsoft.

Installez JSON.NET

Ensuite, dans le dossier Models créez le fichier ArticleJSONRepository.cs et entrez ce code :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Newtonsoft.Json;
using System.IO;

namespace Blog.Models
{
    /// <summary>
    /// permet de gérer les articles qui sont enregistrés dans un fichier JSON
    /// </summary>
    public class ArticleJSONRepository
    {
        /// <summary>
        /// Représente le chemin du fichier JSON
        /// </summary>
        private readonly string _savedFile;

        /// <summary>
        /// Construit le gestionnaire d'article à partir du nom d'un fichier JSON
        /// </summary>
        /// <param name="fileName">nom du fichier json</param>
        public ArticleJSONRepository(string fileName)
        {
            _savedFile = fileName;
        }

        /// <summary>
        /// Obtient un unique article à partir de sa place dans la liste enregistrée
        /// </summary>
        /// <param name="id">la place de l'article dans la liste (commence de 0)</param>
        /// <returns>L'article désiré</returns>
        public Article GetArticleById(int id)
        {
            using (StreamReader reader = new StreamReader(_savedFile))
            {
                List<Article> list = JsonConvert.DeserializeObject<List<Article>>(reader.ReadToEnd());

                if (list.Count < id || id < 0)
                {
                    throw new ArgumentOutOfRangeException("Id incorrect");
                }
                return list[id];
            }

        }

        /// <summary>
        /// Obtient une liste d'article
        /// </summary>
        /// <param name="start">Premier article sélectionné</param>
        /// <param name="count">Nombre d'article sélectionné</param>
        /// <returns></returns>
        public IEnumerable<Article> GetListArticle(int start = 0, int count = 10)
        {
            using (StreamReader reader = new StreamReader(_savedFile))
            {
                List<Article> list = JsonConvert.DeserializeObject<List<Article>>(reader.ReadToEnd());
                return list.Skip(start).Take(count);
            }
        }

        /// <summary>
        /// Obtient une liste de tout les articles
        /// </summary>
        public IEnumerable<Article> GetAllListArticle()
        {
            using (StreamReader reader = new StreamReader(_savedFile))
            {
                List<Article> list = JsonConvert.DeserializeObject<List<Article>>(reader.ReadToEnd());
                return list;
            }
        }

        /// <summary>
        /// Ajoute un article à la liste
        /// </summary>
        /// <param name="newArticle">Le nouvel article</param>
        public void AddArticle(Article newArticle)
        {
            List<Article> list;
            using (StreamReader reader = new StreamReader(_savedFile))
            {
                list = JsonConvert.DeserializeObject<List<Article>>(reader.ReadToEnd());
            }
            using (StreamWriter writter = new StreamWriter(_savedFile, false))
            {
                list.Add(newArticle);
                writter.Write(JsonConvert.SerializeObject(list));
            }
        }
    }
}

Avec le code ci-dessus nous allons pouvoir récupérer nos articles qui se trouvent dans notre ficher JSON, et pouvoir en créer. Les méthodes parlent d’elles-mêmes : nous lisons et écrivons dans le fichier JSON.

Cas d'étude numéro 1 : afficher les articles

Ce premier cas d’étude vous permettra de mettre en œuvre :

  • la création d’un contrôleur qui nous retourne une liste d’articles ;
  • la création d’une vue avec un modèle fortement typé ;
  • l’utilisation du layout.

S’organiser pour répondre au problème

Comme c’est notre premier cas d’étude, je vais vous faire un pas à pas détaillé de ce qu’il faut faire puis vous coderez vous-mêmes les choses nécessaires.

  1. Dans le contrôleur Article, créez une méthode List public ActionResult List()
  2. Remplir la liste en utilisant la classe ArticleJSONRepository.cs
  3. Ajouter une vue que le contrôleur va retourner, et entrez les paramètres décrits dans le tableau

paramètres

valeur

Nom

List

Modèle

List

Classe de modèle

Article (votre classe de modèle)

Utiliser une page de disposition

Oui

Tableau : Les paramètres à entrer

Liste des paramètres<-

Voici le code qui permet de récupérer le chemin de votre fichier qui se trouve dans le dossier App_Data :

string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data", "liste_article_tuto_full.json");

Bien sur vous rajouterez un lien dans votre menu qui va permettre d’afficher votre liste d’articles.

Correction

Comme vous l’avez remarqué, c’est rapide de créer une vue qui affiche nos articles. Bien sur comme d’habitude, je vous invite à lancer votre application pour voir le résultat et le code source généré.

using Blog.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace Blog.Controllers
{
    public class ArticleController : Controller
    {
       /// <summary>
       /// Champ qui va permettre d'appeler des méthodes pour faire des actions sur notre fichier
       /// </summary>
        private readonly ArticleJSONRepository _repository;

        /// <summary>
        /// Constructeur par défaut, permet d'initialiser le chemin du fichier JSON
        /// </summary>
        public ArticleController()
        {
            string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data", "liste_article_tuto_full.json");
            _repository = new ArticleJSONRepository(path);
        }

        // GET: List
        public ActionResult List()
        {
            try
            {
                List<Article> liste = _repository.GetAllListArticle().ToList();
                return View(liste);
            }
            catch
            {
                return View(new List<Article>());
            }
        }
    }
}
Le contrôleur
@model IEnumerable<Blog.Models.Article>

@{
    ViewBag.Title = "Blog";
}

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.Pseudo)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Titre)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Contenu)
        </th>
        <th></th>
    </tr>

@foreach (var item in Model) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.Pseudo)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Titre)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Contenu)
        </td>
        <td>
            @Html.ActionLink("Edit", "Edit", new { /* id=item.PrimaryKey */ }) |
            @Html.ActionLink("Details", "Details", new { /* id=item.PrimaryKey */ }) |
            @Html.ActionLink("Delete", "Delete", new { /* id=item.PrimaryKey */ })
        </td>
    </tr>
}

</table>
La vue

Jetons un coup d’œil à ce que nous a généré la vue :

  • un tableau qui affiche les propriétés publiques de notre modèle Article ;
  • différents liens qui pointent sur des méthodes du contrôleur : Create / Edit / Details / Delete.

Cas d'étude numéro 2 : publier un article

Les étapes de la publication

Dans ce cas d’étude, nous ne parlerons pas des autorisations, nous les mettrons en place plus tard, en partie III et IV.

Pour autant, il faut bien comprendre que la publication d’un article ne se fait pas au petit bonheur la chance. Il va falloir respecter certaines étapes qui sont nécessaires.

Comme pour tout ajout de fonctionnalité à notre site, nous allons devoir créer une action. Par convention (ce n’est pas une obligation, mais c’est tellement mieux si vous respectez ça), l’action de créer du contenu s’appelle Create, celle d’éditer Edit, celle d’afficher le contenu seul (pas sous forme de liste) Details.

Le seul problème, c’est que la création se déroule en deux étapes :

  • afficher le formulaire ;
  • traiter le formulaire et enregistrer l’article ainsi posté.

Il va donc bel et bien falloir créer deux actions. Pour cela, nous allons utiliser la capacité de C# à nommer de la même manière deux méthodes différentes du moment qu’elles n’ont pas les mêmes arguments.

La méthode pour afficher le formulaire s’appellera public ActionResult Create().

Lorsque vous allez envoyer vos données, ASP.NET est capable de construire seul l’objet Article à partir de ce qui a été envoyé.
De ce fait, vous allez pouvoir appeler la méthode qui traitera le formulaire public ActionResult Create(Article article). Elle devra comporter deux annotations qui seront détaillées ci-dessous :

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Article article)
{
   //Votre code
}
Protection contre la faille CSRF

C’est pour cela que de base, lorsque vous créerez la vue Create.cshtml, vous allez choisir comme modèle Article et comme template Create. Ce template génère un formulaire complet qui possède deux capacités :

  • envoyer un article quand vous le remplissez ;
  • afficher les erreurs si jamais l’utilisateur a envoyé des données incomplètes ou mal formées.

Maintenant que vous avez vos deux actions et votre formulaire, nous allons pouvoir ajouter de la logique à notre application.

Principalement, cette logique sera toujours la même :

Logique pour la création de contenu
Logique pour la création de contenu

Le jeu sera donc de savoir :

  • comment on valide ;
  • comment on fait une redirection.

Je vous conseille de lancer l’application pour voir le rendu du formulaire.

Faites un clic-droit sur "afficher le code source de la page", je vous conseille de comparer le code HTML généré avec votre code create.cshtml pour voir le rendu des différents éléments de la page.

Il y a trois helpers à retenir (et uniquement trois !) :

  • Html.LabelFor(model=>model.Propriete) pour le label (exemple Html.LabelFor(model => model.Titre)) ;
  • Html.EditorFor(model=>model.Propriete) pour le champ (exemple Html.EditorFor(model => model.Titre)) ;
  • Html.ValidationMessageFor(model=>model.Propriete) pour le message d’erreur(exemple Html.ValidationMessageFor(model => model.Titre)).

À noter, vous pourrez aussi trouver, au début du formulaire, Html.ValidationSummary() qui sert pour la sécurité.

Comme pour le formulaire, vous pouvez customiser le résultat de chacun des trois helpers en utilisant une version de la fonction qui a un argument appelé htmlattributes.

@Html.LabelFor(model => model.Titre, htmlAttributes: new { @class = "control-label col-md-2" })
Avec htmlattributes

En fin de tuto, vous pouvez retrouver un glossaire qui montre les différents contrôles Razor et leur résultat en HTML.

Mais avant d’aller plus loin, un petit point sur les formulaires s’impose.

Explication formulaire

Le formulaire est la façon la plus basique d’interagir avec l’utilisateur. Derrière le mot "interagir", se cachent en fait deux actions : l’utilisateur vous demande des choses ou bien l’utilisateur vous envoie du contenu ou en supprime.

Ces actions sont distinguées en deux verbes :

  • GET : l’utilisateur veut juste afficher le contenu qui existe déjà. Il est important de comprendre qu’une requête qui dit "GET" ne doit pas modifier d’informations dans votre base de données (sauf si vous enregistrez des statistiques) ;
  • POST : l’utilisateur agit sur les données ou en crée. Vous entendrez peut être parler des verbes PUT et DELETE. Ces derniers ne vous seront utiles que si vous utilisez le JavaScript1 ou bien si vous développez une API REST. En navigation basique, seul POST est supporté.

Vous avez sûrement remarqué : je n’ai pas encore parlé de sécurité. La raison est simple, GET ou POST n’apportent aucune différence du point de vue de la sécurité. Si quelqu’un vous dit le contraire, c’est qu’il a sûrement mal sécurisé son site.

Pour rappel, un formulaire en HTML ressemble à ceci :

<!-- permet de créer un formulaire qui ouvrira la page zestedesavoir.com/rechercher
l'attribut method="get", permet de dire qu'on va "lister" des choses
-->
<form id="search_form" class="clearfix search-form" action="/rechercher" method="get">
    <!-- permet de créer un champ texte, ici prérempli avec "zep".-->
    <input id="id_q" type="search" value="zep" name="q"></input>
    <button type="submit"></button><!-- permet de valider le formulaire-->

</form>
Un formulaire de recherche sur ZdS2

En syntaxe Razor, la balise <form> donne HTML.BeginForm, il existe différents paramètres pour définir si on est en "mode" GET ou POST, quel contrôleur appeler etc.. Bien sûr, on peut très bien écrire le formulaire en HTML comme ci-dessus.

Particularités d’un formulaire GET

Comme vous devez lister le contenu, le formulaire en mode GET possède quelques propriétés qui peuvent s’avérer intéressantes :

  • toutes les valeurs se retrouvent placées à la fin de l’URL sous la forme : url_de_base/?nom_champ_1=valeur1&nom_champ2=valeur2 ;
  • l’URL complète (URL de base + les valeurs) ne doit pas contenir plus de 255 caractères ;
  • si vous copiez/collez l’URL complète dans un autre onglet ou un autre navigateur : vous aurez accès au même résultat (sauf si la page demande à ce que vous soyez enregistré !)3.

Particularités d’un formulaire POST

Les formulaires POST sont fait pour envoyer des contenus nouveaux. Ils sont donc beaucoup moins limités :

  • il n’y a pas de limite de nombre de caractères ;
  • il n’y a pas de limite de caractères spéciaux (vous pouvez mettre des accents, des smileys…) ;
  • il est possible d’envoyer un ou plusieurs fichiers, seul le serveur limitera la taille du fichier envoyé.

Comme pour les formulaires GET, les données sont entrées sous la forme de clef=>valeur. À ceci près que les formulaires POST envoient les données dans la requête HTTP elle-même et donc sont invisibles pour l’utilisateur lambda.

Autre propriété intéressante : si vous utilisez HTTPS, les données envoyées par POST sont cryptées. C’est pour ça qu’il est fortement conseillé d’utiliser ce protocole (quand on a un certificat4) dès que vous demandez un mot de passe à votre utilisateur.

Validation du formulaire

Les formulaires Razor sont très intelligents et vous permettent d’afficher rapidement :

  • le champ adapté à la donnée, par exemple si vous avez une adresse mail, il vous enverra un <input type="email"/> ;
  • l’étiquette de la donnée (la balise <label>) avec le bon formatage et le bon texte ;
  • les erreurs qui ont été détectées lors de l’envoi grâce à JavaScript (attention, c’est un confort, pas une sécurité) ;
  • les erreurs qui ont été détectées par le serveur (là, c’est une sécurité).

Pour que Razor soit capable de définir le bon type de champ à montrer, il faut que vous lui donniez cette indication dans votre classe de modèle. Cela permet aussi d’indiquer ce que c’est qu’un article valide.

Voici quelques attributs que l’on rencontre très souvent :

Nom de l’attribut

Effet

Exemple

System.ComponentModel .DataAnnotations.RequiredAttribute

Force la propriété à être toujours présente

[Required(AllowEmptyStrings=false)]

System.ComponentModel .DataAnnotations .StringLengthAttribute

Permet de limiter la longueur d’un texte

[StringLength(128)]

System.ComponentModel .DataAnnotations.RangeAttriute

Permet de limiter un nombre à un intervalle donné

[Range(128,156)]

System.ComponentModel .PhoneAnnotations

Permet d’indiquer que la chaîne de caractères est un numéro de téléphone

[Phone]

Les attributs communs

Il en existe beaucoup d’autres, pour gérer les dates, les expressions régulières… Vous pouvez vous-mêmes en créer si cela vous est nécessaire.

Avec ces attributs, décrivons notre classe Article. Par exemple de cette manière :

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web;

namespace Blog.Models
{
    /// <summary>
    /// Notre modèle de base pour représenter un article
    /// </summary>
    public class Article
    {
        /// <summary>
        /// Le pseudo de l'auteur
        /// </summary>
        [Required(AllowEmptyStrings=false)]
        [StringLength(128)]
        [RegularExpression(@"^[^,\.\^]+$")]
        [DataType(DataType.Text)]
        public string Pseudo { get; set; }
        /// <summary>
        /// Le titre de l'article
        /// </summary>
        [Required(AllowEmptyStrings = false)]
        [StringLength(128)]
        [DataType(DataType.Text)]
        public string Titre { get; set; }
        /// <summary>
        /// Le contenu de l'article
        /// </summary>
        [Required(AllowEmptyStrings=false)]
        [DataType(DataType.MultilineText)]
        public string Contenu { get; set; }
    }
}

Maintenant, au sein de la méthode Create(Article article), nous allons demander de valider les données.

Pour cela, il faut utiliser ModelState.IsValid.

           if (ModelState.IsValid)
            {
                _repository.AddArticle(article);
                return RedirectToAction("List", "Article"); // Voir explication dessous
            }
            return View(article);
Validation des données et enregistrement

Redirections

Comme nous l’avons vu dans le diagramme de flux, nous allons devoir rediriger l’utilisateur vers la page adéquate dans deux cas :

  • erreur fatale ;
  • réussite de l’action.

Dans le premier cas, il faut se rendre compte que, de base, ASP.NET MVC, enregistre des "filtres d’exception" qui, lorsqu’une erreur se passe, vont afficher une page "customisée". Le comportement de ce filtre est celui-ci :

  • si l’exception attrapée hérite de HttpException, le moteur va essayer de trouver une page "personnalisée" correspondant au code d’erreur ;
  • sinon, il traite l’exception comme une erreur 500.

Pour "customiser" la page d’erreur 500, il faudra attendre un peu, ce n’est pas nécessaire maintenant, au contraire, les informations de débogage que vous apportent la page par défaut sont très intéressantes.

Ce qui va nous intéresser, c’est plutôt la redirection "quand tout va bien".

Il existe deux méthodes pour rediriger selon vos besoin. La plupart du temps, vous utiliserez RedirectToAction. Cette méthode retourne un objet ActionResult.

Pour une question de lisibilité, je vous propose d’utiliser RedirectToAction avec deux (ou trois selon les besoins) arguments :

RedirectToAction("NomDeL'action", "Nom du Contrôleur");//une redirection simple 
RedirectToAction("List","Article",new {page= 0});///Une redirection avec des paramètres pour l'URL

Le code final

À chaque partie le code final est donnée, mais il ne faut pas le copier bêtement. Pour bien comprendre les choses, il faut regarder pas à pas les différentes actions, mettre des points des arrêts dans les contrôleurs et regarder les différents objets.

Mon point d’arrêt
Mon point d’arrêt

L’entité Article

public class Article
    {
        /// <summary>
        /// Le pseudo de l'auteur
        /// </summary>
        [Required(AllowEmptyStrings=false)]
        [StringLength(128)]
        [RegularExpression(@"^[^,\.\^]+$")]
        [DataType(DataType.Text)]
        public string Pseudo { get; set; }
        /// <summary>
        /// Le titre de l'article
        /// </summary>
        [Required(AllowEmptyStrings = false)]
        [StringLength(128)]
        [DataType(DataType.Text)]
        public string Titre { get; set; }
        /// <summary>
        /// Le contenu de l'article
        /// </summary>
        [Required(AllowEmptyStrings=false)]
        [DataType(DataType.MultilineText)]
        public string Contenu { get; set; }
    }

La vue Create

@model Blog.Models.Article

@{
    ViewBag.Title = "Create";
}

<h2>Create</h2>


@using (Html.BeginForm()) 
{
    @Html.AntiForgeryToken()
    
    <div class="form-horizontal">
        <h4>Article</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.Pseudo, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Pseudo, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Pseudo, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Titre, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Titre, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Titre, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Contenu, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.TextAreaFor(model => model.Contenu, new { htmlAttributes = new { @class = "form-control" }, cols = 35, rows = 10 })
                @Html.ValidationMessageFor(model => model.Contenu, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}

<div>
    @Html.ActionLink("Back to List", "List")
</div>

<script src="~/Scripts/jquery-1.10.2.min.js"></script>
<script src="~/Scripts/jquery.validate.min.js"></script>
<script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>

Le contrôleur

        //GET : Create
        public ActionResult Create()
        {
            return View();
        }

        //POST : Create
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create(Article article)
        {
            if (ModelState.IsValid)
            {
                _repository.AddArticle(article);
                return RedirectToAction("List", "Article");
            }
            return View(article);
        }

  1. Une technique avancée mais néanmoins courante dans le web moderne est l’utilisation de JavaScript avec l’objet XMLHttpRequest. L’acronyme qui désigne cette utilisation est AJAX.

  2. La liste complète des types de champs HTML se trouve ici.

  3. On parle d'indempotence.

  4. Le protocole HTTPS garantit la confidentialité (i.e ce qui est transmis est secret) et l’authenticité (i.e que le site est bien qui il prétend être) et pour cela il faut acheter un certificat.

Cas d'étude numéro 3 : envoyer une image pour illustrer l'article

Notre prochain cas d’étude sera le cas de l’ajout d’une image thumbnail (miniature) pour nos articles.

Comme nous allons lier un nouvel objet à notre modèle d’article, il nous faut modifier notre classe Article de manière à ce qu’elle soit prête à recevoir ces nouvelles informations.

En fait la difficulté ici, c’est que vous allez transmettre un fichier. Un fichier, au niveau de ASP.NET, c’est représenté par l’objet HttpPostedFileBase. Cet objet ne va pas être sauvegardé avec JSON ou SQL. Ce que vous allez sauvegarder, c’est le chemin vers le fichier.

Du coup, la première idée qui vous vient à l’esprit, c’est de rajouter une propriété dans votre classe Article :

        /// <summary>
        /// Le chemin vers le fichier image
        /// </summary>
        public string ImageName { get; set; }

Alors comment faire pour qu’un fichier soit téléchargé ?

Une technique utilisable et que je vous conseille pour débuter, c’est de créer une nouvelle classe qu’on appelle souvent un modèle de vue2. Ce modèle de vue est une classe normale, elle va simplement être adaptée au formulaire que nous désirons mettre en place.

Créez un fichier ArticleViewModels.cs et à l’intérieur créez une classe "ArticleCreation" :

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web;

namespace TutoBlog.Models
{
    public class ArticleCreation
    {
        /// <summary>
        /// Le pseudo de l'auteur
        /// </summary>
        [Required(AllowEmptyStrings = false)]
        [StringLength(128)]
        [RegularExpression(@"^[^,\.\^+$")]
        [DataType(DataType.Text)]
        public string Pseudo { get; set; }
        /// <summary>
        /// Le titre de l'article
        /// </summary>
        [Required(AllowEmptyStrings = false)]
        [StringLength(128)]
        [DataType(DataType.Text)]
        public string Titre { get; set; }
        /// <summary>
        /// Le contenu de l'article
        /// </summary>
        [Required(AllowEmptyStrings = false)]
        [DataType(DataType.Text)]
        public string Contenu { get; set; }

        /// <summary>
        /// Le fichier image
        /// </summary>
        [DisplayName("Illustration")]
        public HttpPostedFileBase Image{ get; set; }
    }
}

Dans notre contrôleur, nous allons réécrire la méthode Create. Et au lieu de demander un Article, nous allons demander un ArticleCreation. Il faut aussi modifier notre vue Create pour prendre un ArticleCreation en entrée.

Il nous faudra aussi ajouter un champ au formulaire, qui accepte de télécharger un fichier :

        <div class="form-group">
            @Html.LabelFor(model => model.Image, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.TextBoxFor(model => model.Image, new { type = "file", accept = "image/jpeg,image/png" })
                @* ou  <input type="file" name="Image"/>*@
                @Html.ValidationMessageFor(model => model.Image, "", new { @class = "text-danger" })
            </div>
        </div>

Un formulaire, à la base, n’est pas fait pour transporter des données aussi complexes que des fichiers. Du coup, il faut les encoder. Et pour cela, il faut préciser un attribut de la balise form : enctype="multipart/form-data". Si vous avez bien suivi les bases de Razor, vous avez compris qu’il faut modifier l’appel à la méthode Html.BeginForm.

Nous allons donc enregistrer le fichier une fois que ce dernier est reçu - surtout quand il est reçu.
La méthode SaveAs sert à cela.
Par convention, ce téléchargement se fait dans le dossier Content ou un de ses sous-dossiers. Ensuite nous allons retenir le chemin du fichier.

Attention, pour des raisons de sécurité, il est impératif d’imposer des limites à ce qui est téléchargeable. Il est toujours conseillé de procéder à des vérifications dans le contrôleur :

  • une image = des fichiers jpeg, png ou gif ;
  • dans tous les cas, il faut limiter la taille du téléchargement (sur ZdS, la limite est par exemple à 1024 Ko).

Voici ce que cela peut donner :

        private static string[] AcceptedTypes = new string[] { "image/jpeg", "image/png" };
        private static string[] AcceptedExt = new string[] { "jpeg", "jpg", "png", "gif" };

        //POST : Create
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create(ArticleCreation articleCreation)
        {
            if (!ModelState.IsValid)
            {
                return View(articleCreation);
            }

            string fileName = "";
            if (articleCreation.Image != null)
            {
                bool hasError = false;
                if (articleCreation.Image.ContentLength > 1024 * 1024)
                {
                    ModelState.AddModelError("Image", "Le fichier téléchargé est trop grand.");
                    hasError = true;
                }

                if (!AcceptedTypes.Contains(articleCreation.Image.ContentType)
                       || AcceptedExt.Contains(Path.GetExtension(articleCreation.Image.FileName).ToLower()))
                {
                    ModelState.AddModelError("Image", "Le fichier doit être une image.");
                    hasError = true;
                }

                try
                {
                    fileName = Path.GetFileName(articleCreation.Image.FileName);
                    string imagePath = Path.Combine(Server.MapPath("~/Content/Upload"), fileName);
                    articleCreation.Image.SaveAs(imagePath);
                }
                catch
                {
                    fileName = "";
                    ModelState.AddModelError("Image", "Erreur à l'enregistrement.");
                    hasError = true;
                }
                if (hasError)
                    return View(articleCreation);
            }

            Article article = new Article
            {
                Contenu = articleCreation.Contenu,
                Pseudo = articleCreation.Pseudo,
                Titre = articleCreation.Titre,
                ImageName = fileName
            };

            _repository.AddArticle(article);
            return RedirectToAction("List", "Article");
        }

Nous souhaitons maintenant afficher l’image dans notre liste d’article, pour cela il n’y a pas de helper magique dans Razor. Il faudra utiliser simplement la balise <img />.

Les problèmes qui se posent

Je vous présente ce cas d’étude avant de passer à la base de données pour une raison simple : un fichier, qu’il soit une image ou non, ne doit presque jamais être stocké dans une base de données.

Si vous ne suivez pas cette règle, vous risquez de voir les performances de votre site baisser de manière affolantes.

Un fichier, ça se stocke généralement dans un système de fichiers. Pour faire plus simple, on le rangera dans un dossier.

Doublons

Essayons de donner à deux articles différents des images qui ont le même nom. Que se passe-t-il actuellement ?

L’image déjà présente est écrasée.

Pour éviter ce genre de problèmes, il faut absolument s’assurer que le fichier qui est créé lors d’un téléchargement est unique.
Vous trouverez deux écoles pour rendre unique un nom de fichier, ceux qui utilisent un marquage temporel (le nombre de secondes depuis 1970) et ceux qui utilisent un identifiant.

Comme nous n’utilisons pas encore de base de données, notre identifiant, c’est nous qui allons devoir le générer. Et une des meilleurs manières de faire un bon identifiant, c’est d’utiliser un Global Unique Identifier connu sous l’acronyme GUID.

Le code pour générer un tel identifiant est simple :

Guid id = Guid.NewGuid();
string imageFileName = id.toString()+"-"+model.Image.FileName;

Les caractères spéciaux

Nouveau problème : nous désirons afficher l’image. Pour cela, nous allons modifier le fichier de vue List pour qu’à chaque article, on ajoute une image.

Typiquement, la balise <img> s’attend à ce que vous lui donniez un lien. Comme nous avons sauvegardé notre image dans le dossier Content/Uploads, la balise ressemblera à : <img src="/Content/Uploads/135435464-nomdufichier.jpg/>.

C’est là qu’arrive le problème : vos visiteurs n’utiliseront pas forcément des noms de fichiers qui donnent des URL sympathiques. En effet, les systèmes d’exploitation modernes savent utiliser les accents, les caractères chinois… mais ce n’est pas le cas des URL.

Pour régler ce problème, nous allons utiliser ce qu’on appelle un slug.

Les slugs sont très utilisés dans les liens des sites de presse, car ils ont souvent un impact positif sur le SEO1. Prenons un exemple :
nextinpact.com/news/89405-gps-europeen-deux-satellites-galileo-bien-lances-mais-mal-positionnes.htm

Vous avez un lien en trois parties :

  • nextinpact.com : le nom de domaine ;
  • /news/ : l’équivalent pour nous du segment "{controller}" ;
  • 89405-gps-europeen-deux-satellites-galileo-bien-lances-mais-mal-positionnes.htm : le slug, qui permet d’écrire une URL sans accents, espaces blancs… Et pour bien faire, ils ont même un identifiant unique en amont.

Pour utiliser les slugs, une possibilité qui s’offre à vous est d’utiliser un package nommé slugify.

Dans l’explorateur de solutions, faites un clic-droit sur le nom du projet puis sur "gérer les packages" et installez slugify.

Gérer les packages
Gérer les packages
Installer slugify
Installer slugify

Pour utiliser slugify, cela se passe ainsi :

string fileSlug = new SlugHelper().GenerateSlug(filePath);

  1. Search Engine Optimisation : un ensemble de techniques qui permettent d’améliorer votre classement dans les résultats des grands moteurs de recherche.

  2. En anglais on dit ViewModel.

Cas d'étude numéro 4 : la pagination

Ce qu’est la pagination

La pagination, c’est une technique très utile quand vous devez afficher des listes très longues de contenu.
Sur un blog, vous aurez - à partir d’un moment - une liste assez importante d’articles. Disons pour faire simple que vous en avez 42.

Pensez-vous qu’afficher les 42 articles sur la page d’accueil soit une bonne chose ?

Autant d’articles, c’est long, c’est lourd. Cela ralentirait le chargement de votre page et ça forcerait vos visiteurs à utiliser l’ascenseur vertical un trop grand nombre de fois.

De ce fait, vous allez par exemple décider de n’afficher que 5 articles, et de mettre les 5 suivants dans une autre page et ainsi de suite.

Votre but sera donc d’afficher la liste des articles puis de mettre à disposition de vos utilisateurs un bouton "page précédente", un autre "page suivante", et un formulaire permettant de se rendre à une page quelconque.

Voici un petit aperçu du contrôle proposé :

Aperçu formulaire pagination

S’organiser pour répondre au problème

Comme c’est notre dernier cas d’étude pour ce chapitre, vous allez utiliser toutes vos connaissances apprises jusqu’ici :

  1. dans le contrôleur Article, modifiez la méthode List en public ActionResult List(int page = 0) ;
  2. utilisez la bonne méthode pour récupérer vos articles, voir ArticleJSONRepository ;
  3. ajoutez les liens Précédent/Suivant dans la vue, @Html.ActionLink() ;
  4. déterminez si vous devez utiliser un formulaire GET ou POST et créez-le (URL de type url/Article/List?page=0).
        public readonly static int ARTICLEPERPAGE = 5;

        // GET: List
        public ActionResult List(int page = 0)
        {
            try
            {
                List<Article> liste = _repository.GetListArticle(page * ARTICLEPERPAGE, ARTICLEPERPAGE).ToList();
                ViewBag.Page = page;
                return View(liste);
            }
            catch
            {
                return View(new List<Article>());
            }
        }

Code : le contrôleur

@model IEnumerable<Blog.Models.Article>

@{
    ViewBag.Title = "Blog";
}

<section>
    <p>
        @Html.ActionLink("Create New", "Create")
    </p>
    <table class="table">
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Pseudo)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Titre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contenu)
            </th>
            <th></th>
        </tr>

        @foreach (var item in Model)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Pseudo)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Titre)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Contenu)
                </td>
                <td>
                    @{
                       if (!string.IsNullOrWhiteSpace(item.ImageName))
                       {
                         <img src="~/Content/Upload/@item.ImageName" alt="@Path.GetFileNameWithoutExtension(item.ImageName)" />
                       }
                    }
                </td>
                <td>
                    @Html.ActionLink("Edit", "Edit", new { /* id=item.PrimaryKey */ }) |
                    @Html.ActionLink("Details", "Details", new { /* id=item.PrimaryKey */ }) |
                    @Html.ActionLink("Delete", "Delete", new { /* id=item.PrimaryKey */ })
                </td>
            </tr>
        }
    </table>

    <footer class="pagination-nav">
        <ul>
            <li>
                @if ((int)ViewBag.Page > 0)
                {
                    @Html.ActionLink("Précédent", "List", "Article", new { page = ViewBag.Page - 1 }, new { @class = "button" })
                }
            </li>
            <li>
                @if (Model.Count() == Blog.Controllers.ArticleController.ARTICLEPERPAGE || ViewBag.Page > 0)
                {
                    using (Html.BeginForm("List", "Article", FormMethod.Get))
                    {
                        @Html.TextBox("page", 0, new { type = "number" })
                        <button type="submit">Aller à</button>
                    }
                }
            </li>
            <li>
                @if (Model.Count() == Blog.Controllers.ArticleController.ARTICLEPERPAGE)
                {
                    @Html.ActionLink("Suivant", "List", "Article", new { page = ViewBag.Page + 1 }, new { @class = "button" })
                }
            </li>
        </ul>
    </footer>
</section>

Code : La vue

Se protéger contre les failles de sécurité

Un site internet est un point d’entrée important pour pas mal d’attaques. Certaines de ces attaques sont brutales et pour vous protéger, vous aurez besoin d’une infrastructure importante, ce qui est souvent la responsabilité de votre hébergeur.

D’autres, à l’opposé, sont plus pernicieuses, car elles se servent de failles présentes au sein-même de votre code.
Nous allons voir quelques-unes des failles les plus connues sur le web, et surtout que ASP.NET vous aide énormément à vous en protéger.

La faille CSRF

Pour vous souhaiter la bienvenue dans le vaste monde des failles de sécurité, voici votre premier acronyme : CSRF, pour Cross Site Request Forgery1.

Cette faille s’attaque aux formulaires qui demandent authentification. Elle tire profit du fait que créer une requête HTTP à partir de rien est très facile.

Surtout, elle s’aide beaucoup du social engineering et du réflexe pavlovien des gens : cliquer sur tous les liens qu’ils voient…

Cette attaque consiste à usurper l’identité d’un membre qui a le droit de gérer le contenu (créer, éditer, supprimer) et à envoyer un formulaire avec les données adéquates au site.

Faisons un petit schéma pour bien comprendre.

Au départ nous avons deux entités séparées :

  • un utilisateur qui est connecté sur le vrai site avec ses identifiants ;
  • un pirate qui va créer un site ou un email frauduleux pour piéger cet utilisateur.
Préparation de l'attaque
Préparation de l'attaque

Le fameux lien sur lequel l’utilisateur va cliquer va en fait être un faux site qui ne fera qu’une chose : envoyer une requête pour supprimer tel ou tel contenu.

Heureusement, il existe une parade.

Premièrement il est absolument primordial que vous utilisiez des formulaires POST quand vous voulez modifier des données dans votre base. En effet si vous laissez un contrôleur supprimer des données alors que tout passe en GET, une simple balise image du type <img src="http://votre.site.fr/supprimer/objet/1/?param=valeur"/> suffira à vous "piéger". L’image ne s’affichera jamais mais votre navigateur interprétera automatiquement cette balise comme "envoie une requête au site" et vous allez silencieusement supprimer des données.

Le formulaire POST n’est pas suffisant, mais il permet d’utiliser une astuce pour se protéger définitivement contre cette faille.

En session, vous allez enregistrer un nombre généré aléatoirement à chaque fois que vous affichez une page.

Sur la page honnête, donc sur le vrai site, vous allez ajouter à tous les formulaires un champ caché qui s’appellera "csrf_token" par exemple et qui aura pour valeur ce nombre impossible à deviner.

Puis, dans votre contrôleur, vous allez vérifier que ce nombre est bien le même que celui enregistré en session.

Comme ça, tout autre site extérieur qui essaierait de créer une requête sera immédiatement rejeté et vous vous en rendrez compte.

Comme mettre en place tout ça risquerait d’être long à coder à chaque fois, ASP.NET vous aide.

Lorsque vous créez un formulaire de type POST, il suffit d’ajouter deux choses :

  • dans le formulaire Razor, ajoutez : @Html.AntiForgeryToken();
  • au niveau de la méthode Create, Edit ou Delete de votre contrôleur, ajoutez l’attribut [ValidateAntiForgeryToken].

Voilà, vous êtes protégés !

La faille XSS

La faille "XSS" est une faille de type "injection de code malveillant", et elle est une des failles les plus connues et les plus faciles à corriger.

Une faille XSS survient lorsqu’une personne essaie d’envoyer du code HTML dans votre formulaire.

Imaginons qu’il envoie <strong>Je suis important</strong>. Si vous ne vous protégez pas contre la faille XSS, vous verrez sûrement apparaître la phrase "Je suis important" en gras.
Maintenant, cas un peu plus pernicieux, la personne vous envoie "</body>je suis important". Et c’est le design complet de la page qui tombe.

Malheureusement, ça n’est pas tout. La personne peut envoyer un script JavaScript qui redirigera vos visiteurs vers un site publicitaire, ou bien illégal. C’est de ce comportement-là que vient le nom XSS : Cross Site Scripting.

Comme je vous l’ai dit, la parade est simple : il suffit de remplacer les caractères ><" par leur équivalent HTML (&gt; &lt; &quot;). Ainsi les balises HTML sont considérées comme du texte simple et non plus comme des balises.

Et comme c’est un comportement qui est très souvent désiré, c’est le comportement par défaut de Razor quand vous écrivez @votrevariable.

Pour changer ce comportement, il faut créer un helper qui va par exemple autoriser les balises bien construites (<strong></strong>), interdire les balises <script> et les attributs spéciaux (onclick par exemple).


  1. Plus d’informations sur wikipédia


Notre blog commence à ressembler à quelque chose. C’est bien, mais la suite est encore longue.