Introduction par l'exemple à PHP 4

Serge Tahé

Ce cours reprend la structure du document "Introduction à la programmation Web en Java" [http://tahe.developpez.com/java/web/] et le traite avec PHP. Permet la comparaison des deux technologies.

Téléchargement fichier PDF de l'article (216 pages, 4 Mo) : ftp (plus rapide) http (plus lent)

Téléchargement fichier ZIP de l'étude de cas (1,25 Mo) : ftp (plus rapide) http (plus lent)

Autres ressources PHP

Introduction à PHP5 par l'exemple (Juillet 2011)
Programmation web avec PHP et architecture MVC

- Titre : Introduction par l'exemple à PHP 4

- Auteur : Serge Tahé

- Parution : 4 avril 2001

- Mise à jour : 4 avril 2001

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2001 Serge Tahé. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Remerciements

- II. Les bases

- II-A. Les composantes d'une application Web

- II-B. Les échanges de données dans une application web avec formulaire

- II-C. Quelques ressources

- II-D. Notations

- II-E. Pages Web statiques, Pages Web dynamiques

- II-E-1. Page statique HTML (HyperText Markup Language)

- II-E-2. Une page ASP (Active Server Pages)

- II-E-3. Un script PERL (Practical Extracting and Reporting Language)

- II-E-4. Un script PHP (Personal Home Page, HyperText Processor)

- II-E-5. Un script JSP (Java Server Pages)

- II-E-6. Conclusion

- II-F. Scripts côté navigateur

- II-F-1. Une page Web avec un script Vbscript, côté navigateur

- II-F-2. Une page Web avec un script Javascript, côté navigateur

- II-G. Les échanges client-serveur

- II-G-1. Le modèle OSI

- II-G-2. Le modèle TCP/IP

- II-G-3. Le protocole HTTP

- II-G-3-a. La réponse d'un serveur HTTP

- II-G-3-b. La demande d'un client HTTP

- II-H. Le langage HTML

- II-H-1. Un exemple

- II-H-1-a. Le formulaire

- II-H-1-b. Champ de saisie

- II-H-1-c. Champ de saisie multilignes

- II-H-1-d. Boutons radio

- II-H-1-e. Cases à cocher

- II-H-1-f. Liste déroulante (combo)

- II-H-1-g. Liste à sélection unique

- II-H-1-h. Liste à sélection multiple

- II-H-1-i. Bouton de type button

- II-H-1-j. Bouton de type submit

- II-H-1-k. Bouton de type reset

- II-H-1-l. Champ caché

- II-H-2. Envoi à un serveur web par un client web des valeurs d'un formulaire

- II-H-2-a. Méthode GET

- II-H-2-b. Méthode POST

- II-H-2-c. Récupération des valeurs d'un formulaire Web

- III. Introduction à la programmation web en PHP

- III-A. Programmation PHP

- III-B. Le fichier de configuration de l'interpréteur PHP

- III-C. Configurer PHP à l'exécution

- III-D. Contexte d'exécution des exemples

- III-E. Un premier exemple

- III-F. Récupérer les paramètres envoyés par un client web

- III-F-1. par un POST

- III-F-2. par un GET

- III-G. Récupérer les entêtes http envoyés par un client web

- III-H. Récupérer des informations d'environnement

- III-I. Exemples

- III-I-1. Génération dynamique de formulaire - 1

- III-I-2. Génération dynamique de formulaire - 2

- III-I-3. Génération dynamique de formulaire - 3

- III-I-4. Génération dynamique de formulaire - 4

- III-I-5. Récupérer les valeurs d'un formulaire

- III-J. Suivi de session

- III-J-1. Le problème

- III-J-2. L'API PHP pour le suivi de session

- III-J-3. Exemple 1

- III-J-4. Exemple 3

- III-J-5. Exemple 4

- III-J-6. Exemple 5

- IV. Exemples

- IV-A. Application Impôts : Introduction

- IV-B. Application Impôts : la classe ImpotsDSN

- IV-C. Application Impôts : version 1

- IV-D. Application Impôts : version 2

- IV-E. Application Impôts : version 3

- IV-F. Application Impôts : version 4

- IV-G. Application Impôts : Conclusion

- V. XML et PHP

- V-A. Fichiers XML et feuilles de style XSL

- V-B. Application impôts : version 5

- V-B-1. Les fichiers XML et feuilles de style XSL de l'application impôts

- V-B-2. L'application xmlsimulations

- V-C. Analyse d'un document XML en PHP

- V-D. Application impôts : version 6

- V-E. Conclusion

- VI. A suivre…

- VII. Compléments

- VII-A. Les outils du développement web

- VII-A-1. Serveurs Web, Navigateurs, Langages de scripts

- VII-A-2. Où trouver les outils

- VII-A-3. EasyPHP

- VII-A-3-a. Administration PHP

- VII-A-3-b. Administration Apache

- VII-A-3-c. Le fichier de configuration d'Apache httpd.conf

- VII-A-3-d. Administration de MySQL avec PhpMyAdmin

- VII-A-4. PHP

- VII-A-5. PERL

- VII-A-6. Vbscript, Javascript, Perlscript

- VII-A-7. JAVA

- VII-A-8. Serveur Apache

- VII-A-8-a. Configuration

- VII-A-8-b. Lien PHP - Apache

- VII-A-8-c. Lien PERL-APACHE

- VII-A-9. Le serveur PWS

- VII-A-9-a. Installation

- VII-A-9-b. Premiers tests

- VII-A-9-c. Lien PHP - PWS

- VII-A-10. Tomcat : servlets Java et pages JSP (Java Server Pages)

- VII-A-10-a. Installation

- VII-A-10-b. Démarrage/Arrêt du serveur Web Tomcat

- VII-A-11. Jbuilder

- VII-B. Code source de programmes

- VII-B-1. Le client TCP générique

- VII-B-2. Le serveur Tcp générique

- VII-C. Javascript

- VII-C-1. Récupérer les informations d'un formulaire

- VII-C-1-a. Le formulaire

- VII-C-1-b. Le code

- VII-C-2. Les expressions régulières en Javascript

- VII-C-2-a. La page de test

- VII-C-2-b. Le code de la page

- VII-C-3. Gestion des listes en JavaScript

- VII-C-3-a. Le formulaire

- VII-C-3-b. Le code

- VII-D. Gestion d'une base d'articles sur le web

- VII-D-1. Introduction

- VII-D-2. La base de données

- VII-D-3. Les contraintes du projet

- VII-D-4. La classe articles

- VII-D-5. La structure de l'application WEB

- VII-D-5-a. La page type de l'application

- VII-D-5-b. Le traitement type d'une demande d'un client

- VII-D-5-c. Le fichier de configuration

- VII-D-5-d. La feuille de style associée à la page type

- VII-D-5-e. Le module d'entrée de l'application

- VII-D-5-f. La page d'erreurs

- VII-D-5-g. La page d'informations

- VII-D-6. Le fonctionnement de l'application

- VII-D-6-a. L'authentification

- VII-D-6-b. Ajout d'un article

- VII-D-6-c. Consultation d'articles

- VII-D-6-d. Modification d'articles

- VII-D-6-e. Suppression d'un article

- VII-D-6-f. Émissions de requêtes administrateur

- VII-D-6-g. Travail à faire

- VII-D-7. Faire évoluer l'application

- VII-D-7-a. Changer le type de la base de données

- VII-D-7-b. Améliorer la sécurité

- VII-D-7-c. Faire évoluer le "look"

- VII-D-7-d. Améliorer les performances

- VII-D-8. Pour aller plus loin

- VII-E. PEAR DB: a unified API for accessing SQL-databases

- VII-E-1. DSN

- VII-E-2. Connect

- VII-E-3. Query

- VII-E-4. Fetch

- VII-E-4-a. Select the format of the fetched row

- VII-E-4-b. Set the format of the fetched row

- VII-E-4-c. Fetch rows by number

- VII-E-4-d. Freeing the result set

- VII-E-4-e. Quick data retrieving

- VII-E-4-f. Getting more information from query results

I - Remerciements

Je remercie les personnes suivantes :

- les bénévoles de [developpez.com] qui permettent aux auteurs d'articles de délivrer des documents d'excellente qualité à leurs lecteurs ;

- Joachim Tahé qui a mis le document initial au format [developpez.com].

II - Les bases

Dans ce chapitre, nous présentons les bases de la programmation Web. Il a pour but essentiel de faire découvrir les grands principes de la programmation Web avant de mettre ceux-ci en pratique avec un langage et un environnement particuliers. Il présente de nombreux exemples qu'il est conseillé de tester afin de "s'imprégner" peu à peu de la philosophie du développement web.

II-A - Les composantes d'une application Web

Numéro

Rôle

Exemples courants

1

OS Serveur

Linux, Windows

2

Serveur Web

Apache (Linux, Windows)
IIS (NT), PWS(Win9x)

3

Scripts exécutés côté serveur. Ils peuvent l'être par des modules du serveur ou par des programmes externes au serveur (CGI).

PERL (Apache, IIS, PWS)
VBSCRIPT (IIS,PWS)
JAVASCRIPT (IIS,PWS)
PHP (Apache, IIS, PWS)
JAVA (Apache, IIS, PWS)
C#, VB.NET (IIS)

4

Base de données - Celle-ci peut être sur la même machine que le programme qui l'exploite ou sur une autre via Internet.

Oracle (Linux, Windows)
MySQL (Linux, Windows)
Access (Windows)
SQL Server (Windows)

5

OS Client

Linux, Windows

6

Navigateur Web

Netscape, Internet Explorer

7

Scripts exécutés côté client au sein du navigateur. Ces scripts n'ont aucun accès aux disques du poste client.

VBscript (IE)
Javascript (IE, Netscape)
Perlscript (IE)
Applets JAVA

II-B - Les échanges de données dans une application web avec formulaire

Numéro

Rôle

1

Le navigateur demande une URL pour la 1re fois (http://machine/url). Auncun paramètre n'est passé.

2

Le serveur Web lui envoie la page Web de cette URL. Elle peut être statique ou bien dynamiquement générée par un script serveur (SA) qui a pu utiliser le contenu de bases de données (SB, SC). Ici, le script détectera que l'URL a été demandée sans passage de paramètres et génèrera la page WEB initiale.
Le navigateur reçoit la page et l'affiche (CA). Des scripts côté navigateur (CB) ont pu modifier la page initiale envoyée par le serveur. Ensuite par des interactions entre l'utilisateur (CD) et les scripts (CB) la page Web va être modifiée. Les formulaires vont notamment être remplis.

3

L'utilisateur valide les données du formulaire qui doivent alors être envoyées au serveur web. Le navigateur redemande l'URL initiale ou une autre selon les cas et transmet en même temps au serveur les valeurs du formulaire. Il peut utiliser pour ce faire deux méthodes appelées GET et POST. A réception de la demande du client, le serveur déclenche le script (SA) associé à l'URL demandée, script qui va détecter les paramètres et les traiter.

4

Le serveur délivre la page WEB construite par programme (SA, SB, SC). Cette étape est identique à l'étape 2 précédente. Les échanges se font désormais selon les étapes 2 et 3.

II-C - Quelques ressources

Ci-dessous on trouvera une liste de ressources permettant d'installer et d'utiliser certains outils permettant de faire du développement web. On trouvera en annexe, une aide à l'installation de ces outils.

Serveur Apache

http://www.apache.org
- Apache, Installation et Mise en œuvre, O'Reilly

Serveur IIS, PWS

http://www.microsoft.com

PERL

http://www.activestate.com
- Programmation en Perl, Larry Wall, O'Reilly
- Applications CGI en Perl, Neuss et Vromans, O'Reilly
- la documentation HTML livrée avec Active Perl

PHP

http://www.php.net
- Prog. Web avec PHP, Lacroix, Eyrolles
- Manuel d'utilisation de PHP récupérable sur le site de PHP

VBSCRIPT, ASP

http://msdn.microsoft.com/scripting/vbscript/download/vbsdoc.exe
http://msdn.microsoft.com/scripting/default.htm?/scripting/vbscript
- Interface entre WEB et Base de données sous WinNT, Alex Homer, Eyrolles

JAVASCRIPT

http://msdn.microsoft.com/scripting/jscript/download/jsdoc.exe
http://developer.netscape.com/docs/manuals/index.html

HTML

http://developer.netscape.com/docs/manuals/index.html

JAVA

http://www.sun.com
- JAVA Servlets, Jason Hunter, O'Reilly
- Programmation réseau avec Java, Elliotte Rusty Harold, O'Reilly
- JDBC et Java, George Reese, O'reilly

Base de données

http://www.mysql.com
http://www.oracle.com
- Le manuel de MySQL est disponible sur le site de MySQL
- Oracle 8i sous Linux, Gilles Briard, Eyrolles
- Oracle 8i sous NT, Gilles Briard, Eyrolles

II-D - Notations

Dans la suite, nous supposerons qu'un certain nombre d'outils ont été installés et adopterons les notations suivantes :

notation

signification

<apache>

racine de l'arborescence du serveur apache

<apache-DocumentRoot>

racine des pages Web délivrées par Apache. C'est sous cette racine que doivent se trouver les pages Web. Ainsi l'URL http://localhost/page1.htm correspond au fichier <apache-DocumentRoot>\page1.htm.

<apache-cgi-bin>

racine de l'arborescence lié à l'alias cgi-bin et où l'on peut placer des scripts CGI pour Apache. Ainsi l'URL http://localhost/cgi-bin/test1.pl correspond au fichier <apache-cgi-bin>\test1.pl.

<pws-DocumentRoot>

racine des pages Web délivrées par PWS. C'est sous cette racine que doivent se trouver les pages Web. Ainsi l'URL http://localhost/page1.htm correspond au fichier <pws-DocumentRoot>\page1.htm.

<perl>

racine de l'arborescence du langage Perl. L'exécutable perl.exe se trouve en général dans <perl>\bin.

<php>

racine de l'arborescence du langage PHP. L'exécutable php.exe se trouve en général dans <php>.

<java>

racine de l'arborescence de java. Les exécutables liés à java se trouvent dans <java>\bin.

<tomcat>

racine du serveur Tomcat. On trouve des exemples de servlets dans <tomcat>\webapps\examples\servlets et des exemples de pages JSP dans <tomcat>\webbapps\examples\jsp

On se reportera pour chacun de ces outils à l'annexe qui donne une aide pour leur installation.

II-E - Pages Web statiques, Pages Web dynamiques

Une page statique est représentée par un fichier HTML. Une page dynamique est, elle, générée "à la volée" par le serveur web. Nous vous proposons dans ce paragraphe divers tests avec différents serveurs web et différents langages de programmation afin de montrer l'universalité du concept web.

II-E-1 - Page statique HTML (HyperText Markup Language)

Considérons le code HTML suivant :

1. <html>

2. <head>

3. <title>essai 1 : une page statique</title>

4. </head>

5. <body>

6. <center>

7. <h1>Une page statique...</h1>

8. </body>

9. </html>

qui produit la page web suivante :

Les tests

- lancer le serveur Apache

- mettre le script essai1.html dans <apache-DocumentRoot>

- visualiser l'URL http://localhost/essai1.html avec un navigateur

- arrêter le serveur Apache

- lancer le serveur PWS

- mettre le script essai1.html dans <pws-DocumentRoot

- visualiser l'URL http://localhost/essai1.html avec un navigateur

II-E-2 - Une page ASP (Active Server Pages)

essai2.asp

1. <html>

2. <head>

3. <title>essai 1 : une page asp</title>

4. </head>

5. <body>

6. <center>

7. <h1>Une page asp générée dynamiquement par le serveur PWS</h1>

8. <h2>Il est <% =time %></h2>

9. <br>

10. A chaque fois que vous rafraîchissez la page, l'heure change.

11. </body>

12. </html>

produit la page web suivante :

Le test

- lancer le serveur PWS

- mettre le script essai2.asp dans <pws-DocumentRoot>

- demander l'URL http://localhost/essai2.asp avec un navigateur

II-E-3 - Un script PERL (Practical Extracting and Reporting Language)

essai3.pl

1. #!d:\perl\bin\perl.exe

3. ($secondes,$minutes,$heure)=localtime(time);

5. print <<HTML

6. Content-type: text/html

8. <html>

9. <head>

10. <title>essai 1 : un script Perl</title>

11. </head>

12. <body>

13. <center>

14. <h1>Une page générée dynamiquement par un script Perl</h1>

15. <h2>Il est $heure:$minutes:$secondes</h2>

16. <br>

17. A chaque fois que vous rafraîchissez la page, l'heure change.

18. </body>

19. </html>

21. HTML

22. ;

La première ligne est le chemin de l'exécutable perl.exe. Il faut l'adapter si besoin est. Une fois exécuté par un serveur Web, le script produit la page suivante :

Le test

- serveur Web : Apache

- pour information, visualisez le fichier de configuration srm.conf ou httpd.conf selon la version d'Apache dans <apache>\confs et rechercher la ligne parlant de cgi-bin afin de connaître le répertoire <apache-cgi-bin> dans lequel placer essai3.pl.

- mettre le script essai3.pl dans <apache-cgi-bin>

- demander l'url http://localhost/cgi-bin/essai3.pl

A noter qu'il faut davantage de temps pour avoir la page perl que la page asp. Ceci parce que le script Perl est exécuté par un interpréteur Perl qu'il faut charger avant qu'il puisse exécuter le script. Il ne reste pas en permanence en mémoire.

II-E-4 - Un script PHP (Personal Home Page, HyperText Processor)

essai4.php

1. <html>

2. <head>

3. <title>essai 4 : une page php</title>

4. </head>

5. <body>

6. <center>

7. <h1>Une page PHP générée dynamiquement</h1>

8. <h2>

9. <?

10. $maintenant=time();

11. echo date("j/m/y, h:i:s",$maintenant);

12. ?>

13. </h2>

14. <br>

15. A chaque fois que vous rafraîchissez la page, l'heure change.

16. </body>

17. </html>

Le script précédent produit la page web suivante :

Les tests

- consulter le fichier de configuration srm.conf ou httpd.conf d'Apache dans <Apache>\confs

- pour information, vérifier les lignes de configuration de php

- lancer le serveur Apache

- mettre essai4.php dans <apache-DocumentRoot>

- demander l'URL http://localhost/essai4.php

- lancer le serveur PWS

- pour information, vérifier la configuration de PWS à propos de php

- mettre essai4.php dans <pws-DocumentRoot>\php

- demander l'URL http://localhost/essai4.php

II-E-5 - Un script JSP (Java Server Pages)

heure.jsp

1. <%  //programme Java affichant l'heure %>

3. <%@ page import="java.util.*" %>

5. <%

6. // code JAVA pour calculer l'heure

7. Calendar calendrier=Calendar.getInstance();

8. int heures=calendrier.get(Calendar.HOUR_OF_DAY);

9. int minutes=calendrier.get(Calendar.MINUTE);

10. int secondes=calendrier.get(Calendar.SECOND);

11. // heures, minutes, secondes sont des variables globales

12. // qui pourront être utilisées dans le code HTML

13. %>

15. <% // code HTML %>

16. <html>

17. <head>

18. <title>Page JSP affichant l'heure</title>

19. </head>

20. <body>

21. <center>

22. <h1>Une page JSP générée dynamiquement</h1>

23. <h2>Il est <%=heures%>:<%=minutes%>:<%=secondes%></h2>

24. <br>

25. <h3>A chaque fois que vous rechargez la page, l'heure change</h3>

26. </body>

27. </html>

Une fois exécuté par le serveur web, ce script produit la page suivante :

Les tests

1. mettre le script heure.jsp dans <tomcat>\jakarta-tomcat\webapps\examples\jsp (Tomcat 3.x) ou dans <tomcat>\webapps\examples\jsp (Tomcat 4.x)

2. lancer le serveur Tomcat

3. demander l'URL http://localhost:8080/examples/jsp/heure.jsp

II-E-6 - Conclusion

Les exemples précédents ont montré que :

1. une page HTML pouvait être générée dynamiquement par un programme. C'est tout le sens de la programmation Web.

2. que les langages et les serveurs web utilisés pouvaient être divers. Actuellement on observe les grandes tendances suivantes :

1. les tandems Apache/PHP (Windows, Linux) et IIS/PHP (Windows)

2. la technologie ASP.NET sur les plate-formes Windows qui associent le serveur IIS à un langage .NET (C#, VB.NET…)

3. la technologie des servlets Java et pages JSP fonctionnant avec différents serveurs (Tomcat, Apache, IIS) et sur différentes plate-formes (Windows, Linux). C'est cette denière technologie qui sera plus particulièrement développée dans ce document.

II-F - Scripts côté navigateur

Une page HTML peut contenir des scripts qui seront exécutés par le navigateur. Les langages de script côté navigateur sont nombreux. En voici quelques-uns :

Langage

Navigateurs utilisables

Vbscript

IE

Javascript

IE, Netscape

PerlScript

IE

Java

IE, Netscape

Prenons quelques exemples.

II-F-1 - Une page Web avec un script Vbscript, côté navigateur

vbs1.html

1. <html>

2. <head>

3. <title>essai : une page web avec un script vb</title>

4. <script language="vbscript">

5. function reagir

6. alert "Vous avez cliqué sur le bouton OK"

7. end function

8. </script>

9. </head>

11. <body>

12. <center>

13. <h1>Une page Web avec un script VB</h1>

14. <table>

15. <tr>

16. <td>Cliquez sur le bouton</td>

17. <td><input type="button" value="OK" name="cmdOK" onclick="reagir"></td>

18. </tr>

19. </table>

20. </body>

21. </html>

La page HTML ci-dessus ne contient pas simplement du code HTML mais également un programme destiné à être exécuté par le navigateur qui aura chargé cette page. Le code est le suivant :

1. <script language="vbscript">

2. function reagir

3. alert "Vous avez cliqué sur le bouton OK"

4. end function

5. </script>

Les balises <script></script> servent à délimiter les scripts dans la page HTML. Ces scripts peuvent être écrits dans différents langages et c'est l'option language de la balise <script> qui indique le langage utilisé. Ici c'est VBScript. Nous ne chercherons pas à détailler ce langage. Le script ci-dessus définit une fonction appelée réagir qui affiche un message. Quand cette fonction est-elle appelée ? C'est la ligne de code HTML suivante qui nous l'indique :

1. <input type="button" value="OK" name="cmdOK" onclick="reagir">

L'attribut onclick indique le nom de la fonction à appeler lorsque l'utilisateur cliquera sur le bouton OK. Lorsque le navigateur aura chargé cette page et que l'utilisateur cliquera sur le bouton OK, on aura la page suivante :

Les tests

Seul le navigateur IE est capable d'exécuter des scripts VBScript. Netscape nécessite des compléments logiciels pour le faire. On pourra faire les tests suivants :

- serveur Apache

- script vbs1.html dans <apache-DocumentRoot>

- demander l'url http://localhost/vbs1.html avec le navigateur IE

- serveur PWS

- script vbs1.html dans <pws-DocumentRoot>

- demander l'url http://localhost/vbs1.html avec le navigateur IE

II-F-2 - Une page Web avec un script Javascript, côté navigateur

js1.html

1. <html>

2. <head>

3. <title>essai 4 : une page web avec un script Javascript</title>

4. <script language="javascript">

5. function reagir(){

6. alert ("Vous avez cliqué sur le bouton OK");

7. }

8. </script>

9. </head>

11. <body>

12. <center>

13. <h1>Une page Web avec un script Javascript</h1>

14. <table>

15. <tr>

16. <td>Cliquez sur le bouton</td>

17. <td><input type="button" value="OK" name="cmdOK" onclick="reagir()"></td>

18. </tr>

19. </table>

20. </body>

21. </html>

On a là quelque chose d'identique à la page précédente si ce n'est qu'on a remplacé le langage VBScript par le langage Javascript. Celui-ci présente l'avantage d'être accepté par les deux navigateurs IE et Netscape. Son exécution donne les mêmes résultats :

Les tests

- serveur Apache

- script js1.html dans <apache-DocumentRoot>

- demander l'url http://localhost/js1.html avec le navigateur IE ou Netscape

- serveur PWS

- script js1.html dans <pws-DocumentRoot>

- demander l'url http://localhost/js1.html avec le navigateur IE ou Netscape

II-G - Les échanges client-serveur

Revenons à notre schéma de départ qui illustrait les acteurs d'une application web :

Nous nous intéressons ici aux échanges entre la machine cliente et la machine serveur. Ceux-ci se font au travers d'un réseau et il est bon de rappeler la structure générale des échanges entre deux machines distantes.

II-G-1 - Le modèle OSI

Le modèle de réseau ouvert appelé OSI (Open Systems Interconnection Reference Model) défini par l'ISO (International Standards Organisation) décrit un réseau idéal où la communication entre machines peut être représentée par un modèle à sept couches :

Chaque couche reçoit des services de la couche inférieure et offre les siens à la couche supérieure. Supposons que deux applications situées sur des machines A et B différentes veulent communiquer : elles le font au niveau de la couche Application. Elles n'ont pas besoin de connaître tous les détails du fonctionnement du réseau : chaque application remet l'information qu'elle souhaite transmettre à la couche du dessous : la couche Présentation. L'application n'a donc à connaître que les règles d'interfaçage avec la couche Présentation. Une fois l'information dans la couche Présentation, elle est passée selon d'autres règles à la couche Session et ainsi de suite, jusqu'à ce que l'information arrive sur le support physique et soit transmise physiquement à la machine destination. Là, elle subira le traitement inverse de celui qu'elle a subi sur la machine expéditeur.

A chaque couche, le processus expéditeur chargé d'envoyer l'information, l'envoie à un processus récepteur sur l'autre machine apartenant à la même couche que lui. Il le fait selon certaines règles que l'on appelle le protocole de la couche. On a donc le schéma de communication final suivant :

Le rôle des différentes couches est le suivant :

Physique

Assure la transmission de bits sur un support physique. On trouve dans cette couche des équipements terminaux de traitement des données (E.T.T.D.) tels que terminal ou ordinateur, ainsi que des équipements de terminaison de circuits de données (E.T.C.D.) tels que modulateur/démodulateur, multiplexeur, concentrateur. Les points d'intérêt à ce niveau sont :
.le choix du codage de l'information (analogique ou numérique)
.le choix du mode de transmission (synchrone ou asynchrone).

Liaison de données

Masque les particularités physiques de la couche Physique. Détecte et corrige les erreurs de transmission.

Réseau

Gère le chemin que doivent suivre les informations envoyées sur le réseau. On appelle cela le routage : déterminer la route à suivre par une information pour qu'elle arrive à son destinataire.

Transport

Permet la communication entre deux applications alors que les couches précédentes ne permettaient que la communication entre machines. Un service fourni par cette couche peut être le multiplexage : la couche transport pourra utiliser une même connexion réseau (de machine à machine) pour transmettre des informations appartenant à plusieurs applications.

Session

On va trouver dans cette couche des services permettant à une application d'ouvrir et de maintenir une session de travail sur une machine distante.

Présentation

Elle vise à uniformiser la représentation des données sur les différentes machines. Ainsi des données provenant d'une machine A, vont être "habillées" par la couche Présentation de la machine A, selon un format standard avant d'être envoyées sur le réseau. Parvenues à la couche Présentation de la machine destinatrice B qui les reconnaîtra grâce à leur format standard, elles seront habillées d'une autre façon afin que l'application de la machine B les reconnaisse.

Application

A ce niveau, on trouve les applications généralement proches de l'utilisateur telles que la messagerie électronique ou le transfert de fichiers.

II-G-2 - Le modèle TCP/IP

Le modèle OSI est un modèle idéal. La suite de protocoles TCP/IP s'en approche sous la forme suivante :

- l'interface réseau (la carte réseau de l'ordinateur) assure les fonctions des couches 1 et 2 du modèle OSI

- la couche IP (Internet Protocol) assure les fonctions de la couche 3 (réseau)

- la couche TCP (Transfer Control Protocol) ou UDP (User Datagram Protocol) assure les fonctions de la couche 4 (transport). Le protocole TCP s'assure que les paquets de données échangés par les machines arrivent bien à destination. Si ce n'est pas les cas, il renvoie les paquets qui se sont égarés. Le protocole UDP ne fait pas ce travail et c'est alors au développeur d'applications de le faire. C'est pourquoi sur l'internet qui n'est pas un réseau fiable à 100%, c'est le protocole TCP qui est le plus utilisé. On parle alors de réseau TCP-IP.

- la couche Application recouvre les fonctions des niveaux 5 à 7 du modèle OSI.

Les applications web se trouvent dans la couche Application et s'appuient donc sur les protocoles TCP-IP. Les couches Application des machines clientes et serveur s'échangent des messages qui sont confiées aux couches 1 à 4 du modèle pour être acheminées à destination. Pour se comprendre, les couches application des deux machines doivent "parler" un même langage ou protocole. Celui des applications Web s'appelle HTTP (HyperText Transfer Protocol). C'est un protocole de type texte, c.a.d. que les machines échangent des lignes de texte sur le réseau pour se comprendre. Ces échanges sont normalisés, c.a.d. que le client dispose d'un certain nombre de messages pour indiquer exactement ce qu'il veut au serveur et ce dernier dispose également d'un certain nombre de messages pour donner au client sa réponse. Cet échange de messages a la forme suivante :

Client --> Serveur

Lorsque le client fait sa demande au serveur web, il envoie

1. des lignes de texte au format HTTP pour indiquer ce qu'il veut

2. une ligne vide

3. optionnellement un document

Serveur --> Client

Lorsque le serveur fait sa réponse au client, il envoie

1. des lignes de texte au format HTTP pour indiquer ce qu'il envoie

2. une ligne vide

3. optionnellement un document

Les échanges ont donc la même forme dans les deux sens. Dans les deux cas, il peut y avoir envoi d'un document même s'il est rare qu'un client envoie un document au serveur. Mais le protocole HTTP le prévoit. C'est ce qui permet par exemple aux abonnés d'un fournisseur d'accès de télécharger des documents divers sur leur site personnel hébergé chez ce fournisseur d'accès. Les documents échangés peuvent être quelconques. Prenons un navigateur demandant une page web contenant des images :

1. le navigateur se connecte au serveur web et demande la page qu'il souhaite. Les ressources demandées sont désignées de façon unique par des URL (Uniform Resource Locator). Le navigateur n'envoie que des entêtes HTTP et aucun document.

2. le serveur lui répond. Il envoie tout d'abord des entêtes HTTP indiquant quel type de réponse il envoie. Ce peut être une erreur si la page demandée n'existe pas. Si la page existe, le serveur dira dans les entêtes HTTP de sa réponse qu'après ceux-ci il va envoyer un document HTML (HyperText Markup Language). Ce document est une suite de lignes de texte au format HTML. Un texte HTML contient des balises (marqueurs) donnant au navigateur des indications sur la façon d'afficher le texte.

3. le client sait d'après les entêtes HTTP du serveur qu'il va recevoir un document HTML. Il va analyser celui-ci et s'apercevoir peut-être qu'il contient des références d'images. Ces dernières ne sont pas dans le document HTML. Il fait donc une nouvelle demande au même serveur web pour demander la première image dont il a besoin. Cette demande est identique à celle faite en 1, si ce n'est que la resource demandée est différente. Le serveur va traiter cette demande en envoyant à son client l'image demandée. Cette fois-ci, dans sa réponse, les entêtes HTTP préciseront que le document envoyé est une image et non un document HTML.

4. le client récupère l'image envoyée. Les étapes 3 et 4 vont être répétées jusqu'à ce que le client (un navigateur en général) ait tous les documents lui permettant d'afficher l'intégralité de la page.

II-G-3 - Le protocole HTTP

Découvrons le protocole HTTP sur des exemples. Que s'échangent un navigateur et un serveur web ?

II-G-3-a - La réponse d'un serveur HTTP

Nous allons découvrir ici comment un serveur web répond aux demandes de ses clients. Le service Web ou service HTTP est un service TCP-IP qui travaille habituellement sur le port 80. Il pourrait travailler sur un autre port. Dans ce cas, le navigateur client serait obligé de préciser ce port dans l'URL qu'il demande. Une URL a la forme générale suivante :

1. protocole://machine[:port]/chemin/infos

avec

protocole

http pour le service web. Un navigateur peut également servir de client à des services ftp, news, telnet, ..

machine

nom de la machine où officie le service web

port

port du service web. Si c'est 80, on peut omettre le n° du port. C'est le cas le plus fréquent

chemin

chemin désignant la ressource demandée

infos

informations complémentaires données au serveur pour préciser la demande du client

Que fait un navigateur lorsqu'un utilisateur demande le chargement d'une URL ?

1. il ouvre une communication TCP-IP avec la machine et le port indiqués dans la partie machine[:port] de l'URL. Ouvrir une communication TCP-IP, c'est créer un "tuyau" de communication entre deux machines. Une fois ce tuyau créé, toutes les informations échangées entre les deux machines vont passer dedans. La création de ce tuyau TCP-IP n'implique pas encore le protocole HTTP du Web.

2. le tuyau TCP-IP créé, le client va faire sa demande au serveur Web et il va la faire en lui envoyant des lignes de texte (des commandes) au format HTTP. Il va envoyer au serveur la partie chemin/infos de l'URL

3. le serveur lui répondra de la même façon et dans le même tuyau

4. l'un des deux partenaires prendra la décision de fermer le tuyau. Cela dépend du protocole HTTP utilisé. Avec le protocole HTTP 1.0, le serveur ferme la connexion après chacune de ses réponses. Cela oblige un client qui doit faire plusieurs demandes pour obtenir les différents documents constituant une page web à ouvrir une nouvelle connexion à chaque demande, ce qui a un coût. Avec le protocole HTTP/1.1, le client peut dire au serveur de garder la connexion ouverte jusqu'à ce qu'il lui dise de la fermer. Il peut donc récupérer tous les documents d'une page web avec une seule connexion et fermer lui-même la connexion une fois le dernier document obtenu. Le serveur détectera cette fermeture et fermera lui aussi la connexion.

Pour découvrir les échanges entre un client et un serveur web, nous allons utiliser un client TCP générique. C'est un programme qui peut être client de tout service ayant un protocole de communication à base de lignes de texte comme c'est le cas du protocole HTTP. Ces lignes de texte seront tapées par l'utilisateur au clavier. Cela nécessite qu'il connaisse le protocole de communication du service qu'il cherche à atteindre. La réponse du serveur est ensuite affichée à l'écran. Le programme a été écrit en Java et on le trouvera en annexe. On l'utilise ici dans une fenêtre Dos sous windows et on l'appelle de la façon suivante :

1. java clientTCPgenerique machine port

avec

machine

nom de la machine où officie le service à contacter

port

port où le service est délivré

Avec ces deux informations, le programme va ouvrir une connexion TCP-IP avec la machine et le port désignés. Cette connexion va servir aux échanges de lignes de texte entre le client et le serveur web. Les lignes du client sont tapées par l'utilisateur au clavier et envoyées au serveur. Les lignes de texte renvoyées par le serveur comme réponse sont affichées à l'écran. Un dialogue peut donc s'instaurer directement entre l'utilisateur au clavier et le serveur web. Essayons sur les exemples déjà présentés. Nous avions créé la page HTML statique suivante :

1. <html>

2. <head>

3. <title>essai 1 : une page statique</title>

4. </head>

5. <body>

6. <center>

7. <h1>Une page statique...</h1>

8. </body>

9. </html>

que nous visualisons dans un navigateur :

On voit que l'URL demandée est : http://localhost:81/essais/essai1.html. La machine du service web est donc localhost (=machine locale) et le port 81. Si on demande à voir le texte HTML de cette page Web (Affichage/Source) on retrouve le texte HTML initialement créé :

Maintenant utilisons notre client TCP générique pour demander la même URL :

1. Dos>java clientTCPgenerique localhost 81

3. Commandes :

4. GET /essais/essai1.html HTTP/1.0

6. <-- HTTP/1.1 200 OK

7. <-- Date: Mon, 08 Jul 2002 08:07:46 GMT

8. <-- Server: Apache/1.3.24 (Win32) PHP/4.2.0

9. <-- Last-Modified: Mon, 08 Jul 2002 08:00:30 GMT

10. <-- ETag: "0-a1-3d29469e"

11. <-- Accept-Ranges: bytes

12. <-- Content-Length: 161

13. <-- Connection: close

14. <-- Content-Type: text/html

15. <--

16. <-- <html>

17. <--   <head>

18. <--     <title>essai 1 : une page statique</title>

19. <--    </head>

20. <--    <body>

21. <--      <center>

22. <--      <h1>Une page statique...</h1>

23. <--    </body>

24. <-- </html>

Au lancement du client par la commande java clientTCPgenerique localhost 81 un tuyau a été créé entre le programme et le serveur web opérant sur la même machine (localhost) et sur le port 81. Les échanges client-serveur au format HTTP peuvent commencer. Rappelons que ceux-ci ont trois composantes :

- entêtes HTTP

- ligne vide

- données facultatives

Dans notre exemple, le client n'envoie qu'une demande:

GET /essais/essai1.html HTTP/1.0

Cette ligne a trois composantes :

GET

commande HTTP pour demander une ressource. Il en existe d'autres :
HEAD demande une ressource mais en se limitant aux entêtes HTTP de la réponse du serveur. La ressource elle-même n'est pas envoyée.
PUT permet au client d'envoyer un document au serveur

/essais/essai1.html

ressource demandée

HTTP/1.0

niveau du protocole HTTP utilisé. Ici le 1.0. Cela signifie que le serveur fermera la connexion dès qu'il aura envoyé sa réponse

Les entêtes HTTP doivent toujours être suivis d'une ligne vide. C'est ce qui a été fait ici par le client. C'est comme cela que le client ou le serveur sait que la partie HTTP de l'échange est terminé. Ici pour le client c'est terminé. Il n'a pas de document à envoyer. Commence alors la réponse du serveur composée dans notre exemple de toutes les lignes commençant par le signe <--. Il envoie d'abord une série d'entêtes HTTP suivie d'une ligne vide :

1. <-- HTTP/1.1 200 OK

2. <-- Date: Mon, 08 Jul 2002 08:07:46 GMT

3. <-- Server: Apache/1.3.24 (Win32) PHP/4.2.0

4. <-- Last-Modified: Mon, 08 Jul 2002 08:00:30 GMT

5. <-- ETag: "0-a1-3d29469e"

6. <-- Accept-Ranges: bytes

7. <-- Content-Length: 161

8. <-- Connection: close

9. <-- Content-Type: text/html

10. <--

HTTP/1.1 200 OK

le serveur dit

- qu'il comprend le protocole HTTP version 1.1

- qu'il a la ressource demandée (code 200, message OK)

Date: …

la date/heure de la réponse

Server:

le serveur s'identifie. Ici c'est un serveur Apache

Last-Modified:

date de dernière modification de la ressource demandée par le client

ETag:

…

Accept-Ranges: bytes

unité de mesure des données envoyées. Ici l'octet (byte)

Content-Length: 161

nombre de bytes du document qui va être envoyé après les entêtes HTTP. Ce nombre est en fait la taille en octets du fichier essai1.html :
E:\data\serge\web\essais>dir essai1.html
08/07/2002 10:00 161 essai1.html

Connection: close

le serveur dit qu'il fermera la connexion une fois le document envoyé

Content-type: text/html

le serveur dit qu'il va envoyer du texte (text) au format HTML (html).

Le client reçoit ces entêtes HTTP et sait maintenant qu'il va recevoir 161 octets représentant un document HTML. Le serveur envoie ces 161 octets immédiatement derrière la ligne vide qui signalait la fin des entêtes HTTP :

1. <-- <html>

2. <--   <head>

3. <--     <title>essai 1 : une page statique</title>

4. <--    </head>

5. <--    <body>

6. <--      <center>

7. <--      <h1>Une page statique...</h1>

8. <--    </body>

9. <-- </html>

On reconnaît là, le fichier HTML construit initialement. Si notre client était un navigateur, après réception de ces lignes de texte, il les interpréterait pour présenter à l'utilisateur au clavier la page suivante :

Utilisons une nouvelle fois notre client TCP générique pour demander la même ressource mais cette fois-ci avec la commande HEAD qui demande seulement les entêtes de la réponse :

1. Dos>java.bat clientTCPgenerique localhost 81

2. Commandes :

3. HEAD /essais/essai1.html HTTP/1.1

4. Host: localhost:81

6. <-- HTTP/1.1 200 OK

7. <-- Date: Mon, 08 Jul 2002 09:07:25 GMT

8. <-- Server: Apache/1.3.24 (Win32) PHP/4.2.0

9. <-- Last-Modified: Mon, 08 Jul 2002 08:00:30 GMT

10. <-- ETag: "0-a1-3d29469e"

11. <-- Accept-Ranges: bytes

12. <-- Content-Length: 161

13. <-- Content-Type: text/html

14. <--

Nous obtenons le même résultat que précédemment sans le document HTML. Notons que dans sa demande HEAD, le client a indiqué qu'il utilisait le protocole HTTP version 1.1. Cela l'oblige à envoyer un second entête HTTP précisant le couple machine:port que le client veut interroger : Host: localhost:81.

Maintenant demandons une image aussi bien avec un navigateur qu'avec le client TCP générique. Tout d'abord avec un navigateur :

Le fichier univ01.gif a 3167 octets :

dos>dir univ01.gif
14/04/2000  13:37                3 167 univ01.gif

Utilisons maintenant le client TCP générique :

1. dos>java clientTCPgenerique localhost 81

2. Commandes :

3. HEAD /images/univ01.gif HTTP/1.1

4. host: localhost:81

6. <-- HTTP/1.1 200 OK

7. <-- Date: Tue, 09 Jul 2002 13:53:24 GMT

8. <-- Server: Apache/1.3.24 (Win32) PHP/4.2.0

9. <-- Last-Modified: Fri, 14 Apr 2000 11:37:42 GMT

10. <-- ETag: "0-c5f-38f70306"

11. <-- Accept-Ranges: bytes

12. <-- Content-Length: 3167

13. <-- Content-Type: image/gif

14. <--

On notera les points suivants dans la réponse du serveur :

HEAD

nous ne demandons que les entêtes HTTP de la ressource. En effet, une image est un fichier binaire et non un fichier texte et son affichage à l'écran en tant que texte ne donne rien de lisible.

Content-Length: 3167

c'est la taille du fichier univ01.gif

Content-Type: image/gif

le serveur indique à son client qu'il va lui envoyer un document de type image/gif, c.a.d. une image au format GIF. Si l'image avait été au format JPEG, le type du document aurait été image/jpeg. Les types des documents sont standardisés et sont appelés des types MIME (Multipurpose Mail Internet Extension).

II-G-3-b - La demande d'un client HTTP

Maintenant, posons-nous la question suivante : si nous voulons écrire un programme qui "parle" à un serveur web, quelles commandes doit-il envoyer au serveur web pour obtenir une ressource donnée ? Nous avons dans les exemples précédents obtenu un début de réponse. Nous avons rencontré trois commandes :

GET ressource protocole

pour demander une ressource donnée selon une version donnée du protocole HTTP. Le serveur envoie une réponse au format HTTP suivie d'une ligne vide suivie de la ressource demandée

HEAD ressource protocole

idem si ce n'est qu'ici la réponse se limite aux entêtes HTTP et de la ligne vide

host: machine:port

pour préciser (protocole HTTP 1.1) la machine et le port du serveur web interrogé

Il existe d'autres commandes. Pour les découvrir, nous allons maintenant utiliser un serveur TCP générique. C'est un programme écrit en Java et que vous trouverez lui aussi en annexe. Il est lancé par : java serveurTCPgenerique portEcoute, où portEcoute est le port sur lequel les clients doivent se connecter. Le programme serveurTCPgenerique

- affiche à l'écran les commandes envoyées par les clients

- leur envoie comme réponse les lignes de texte tapées au clavier par un utilisateur. C'est donc ce dernier qui fait office de serveur. Dans notre exemple, l'utilisateur au clavier jouera le rôle d'un service web.

Simulons maintenant un serveur web en lançant notre serveur générique sur le port 88 :

Dos> java serveurTCPgenerique 88
Serveur générique lancé sur le port 88

Prenons maintenant un navigateur et demandons l'URL http://localhost:88/exemple.html. Le navigateur va alors se connecter sur le port 88 de la machine localhost puis demander la page /exemple.html :

Regardons maintenant la fenêtre de notre serveur qui affiche ce que le client lui a envoyé (certaines lignes spécifiques au fonctionnement du programme serveurTCPgenerique ont été omises par souci de simplification) :

1. Dos>java serveurTCPgenerique 88

2. Serveur générique lancé sur le port 88

3. ...

4. <-- GET /exemple.html HTTP/1.1

5. <-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/msword, */*

6. <-- Accept-Language: fr

7. <-- Accept-Encoding: gzip, deflate

8. <-- User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705; .NET CLR 1.0.2 914)

9. <-- Host: localhost:88

10. <-- Connection: Keep-Alive

11. <--

Les lignes précédées du signe <-- sont celles envoyées par le client. On découvre ainsi des entêtes HTTP que nous n'avions pas encore rencontrés :

Accept:

liste de types MIME de documents que le navigateur sait traiter.

Accept-language:

la langue acceptée de préférence pour les documents.

Accept-Encoding:

le type d'encodage des documents que le navigateur sait traiter

User-Agent:

identité du client

Connection:

Close : le serveur fermera la connexion après avoir donné sa réponse
Keep-Alive : la connexion restera ouverte après réception de la réponse du serveur. Cela permettra au navigateur de demander les autres documents nécessaires à la construction de la page sans avoir à recréer une connexion.

Les entêtes HTTP envoyés par le navigateur se terminent par une ligne vide comme attendu.

Elaborons une réponse à notre client. L'utilisateur au clavier est ici le véritable serveur et il peut élaborer une réponse à la main. Rappelons-nous la réponse faite par un serveur Web dans un précédent exemple :

1. <-- HTTP/1.1 200 OK

2. <-- Date: Mon, 08 Jul 2002 08:07:46 GMT

3. <-- Server: Apache/1.3.24 (Win32) PHP/4.2.0

4. <-- Last-Modified: Mon, 08 Jul 2002 08:00:30 GMT

5. <-- ETag: "0-a1-3d29469e"

6. <-- Accept-Ranges: bytes

7. <-- Content-Length: 161

8. <-- Connection: close

9. <-- Content-Type: text/html

10. <--

11. <-- <html>

12. <--   <head>

13. <--     <title>essai 1 : une page statique</title>

14. <--    </head>

15. <--    <body>

16. <--      <center>

17. <--      <h1>Une page statique...</h1>

18. <--    </body>

19. <-- </html>

Essayons d'élaborer à la main (au clavier) une réponse analogue. Les lignes commençant par --> : sont envoyées au client :

1. ...

2. <-- Host: localhost:88

3. <-- Connection: Keep-Alive

4. <--

5. --> : HTTP/1.1 200 OK

6. --> : Server: serveur tcp generique

7. --> : Connection: close

8. --> : Content-Type: text/html

9. --> :

10. --> : <html>

11. --> :   <head><title>Serveur generique</title></head>

12. --> :   <body>

13. --> :     <center>

14. --> :       <h2>Reponse du serveur generique</h2>

15. --> :     </center>

16. --> :    </body>

17. --> : </html>

18. fin

La commande fin est propre au fonctionnement du programme serveurTCPgenerique. Elle arrête l'exécution du programme et clôt la connexion du serveur au client. Nous nous sommes limités dans notre réponse aux entêtes HTTP suivants :

1. HTTP/1.1 200 OK

2. --> : Server: serveur tcp generique

3. --> : Connection: close

4. --> : Content-Type: text/html

5. --> :

Nous ne donnons pas la taille du fichier que nous allons envoyer (Content-Length) mais nous contentons de dire que nous allons fermer la connexion (Connection: close) après envoi de celui-ci. Cela est suffisant pour le navigateur. En voyant la connexion fermée, il saura que la réponse du serveur est terminée et affichera la page HTML qui lui a été envoyée. Cette dernière est la suivante :

1. --> : <html>

2. --> :   <head><title>Serveur generique</title></head>

3. --> :   <body>

4. --> :     <center>

5. --> :       <h2>Reponse du serveur generique</h2>

6. --> :     </center>

7. --> :    </body>

8. --> : </html>

Le navigateur affiche alors la page suivante :

Si ci-dessus, on fait View/Source pour voir ce qu'a reçu le navigateur, on obtient :

c'est-à-dire exactement ce qu'on a envoyé depuis le serveur générique.

II-H - Le langage HTML

Un navigateur Web peut afficher divers documents, le plus courant étant le document HTML (HyperText Markup Language). Celui-ci est un texte formaté avec des balises de la forme <balise>texte</balise>. Ainsi le texte <B>important</B> affichera le texte important en gras. Il existe des balises seules telles que la balise <hr> qui affiche une ligne horizontale. Nous ne passerons pas en revue les balises que l'on peut trouver dans un texte HTML. Il existe de nombreux logiciels WYSIWYG permettant de construire une page web sans écrire une ligne de code HTML. Ces outils génèrent automatiquement le code HTML d'une mise en page faite à l'aide de la souris et de contrôles prédéfinis. On peut ainsi insérer (avec la souris) dans la page un tableau puis consulter le code HTML généré par le logiciel pour découvrir les balises à utiliser pour définir un tableau dans une page Web. Ce n'est pas plus compliqué que cela. Par ailleurs, la connaissance du langage HTML est indispensable puisque les applications web dynamiques doivent générer elles-mêmes le code HTML à envoyer aux clients web. Ce code est généré par programme et il faut bien sûr savoir ce qu'il faut générer pour que le client ait la page web qu'il désire.

Pour résumer, il n'est nul besoin de connaître la totalité du langage HTML pour démarrer la programmation Web. Cependant cette connaissance est nécessaire et peut être acquise au travers de l'utilisation de logiciels WYSIWYG de construction de pages Web tels que Word, FrontPage, DreamWeaver et des dizaines d'autres. Une autre façon de découvrir les subtilités du langage HTML est de parcourir le web et d'afficher le code source des pages qui présentent des caractéristiques intéressantes et encore inconnues pour vous.

II-H-1 - Un exemple

Considérons l'exemple suivant, créé avec FrontPage Express, un outil gratuit livré avec Internet Explorer. Le code généré par Frontpage a été ici épuré. Cet exemple présente quelques éléments qu'on peut trouver dans un document web tels que :

- un tableau

- une image

- un lien

Un document HTML a la forme générale suivante :

1. <html>

2. <head>

3. <title>Un titre</title>

4. ...

5. </head>

6. <body attributs>

7. ...

8. </body>

9. </html>

L'ensemble du document est encadré par les balises <html>…</html>. Il est formé de deux parties :

1. <head>…</head> : c'est la partie non affichable du document. Elle donne des renseignements au navigateur qui va afficher le document. On y trouve souvent la balise <title>…</title> qui fixe le texte qui sera affiché dans la barre de titre du navigateur. On peut y trouver d'autres balises notamment des balises définissant les mots clés du document, mot clés utilisés ensuite par les moteurs de recherche. On peut trouver également dans cette partie des scripts, écrits le plus souvent en javascript ou vbscript et qui seront exécutés par le navigateur.

2. <body attributs>…</body> : c'est la partie qui sera affichée par le navigateur. Les balises HTML contenues dans cette partie indiquent au navigateur la forme visuelle "souhaitée" pour le document. Chaque navigateur va interpréter ces balises à sa façon. Deux navigateurs peuvent alors visualiser différemment un même document web. C'est généralement l'un des casse-têtes des concepteurs web.

Le code HTML de notre document exemple est le suivant :

1. <html>

3. <head>

4. <title>balises</title>

5. </head>

7. <body background="/images/standard.jpg">

8. <center>

9. <h1>Les balises HTML</h1>

10. <hr>

11. </center>

13. <table border="1">

14. <tr>

15. <td>cellule(1,1)</td>

16. <td valign="middle" align="center" width="150">cellule(1,2)</td>

17. <td>cellule(1,3)</td>

18. </tr>

19. <tr>

20. <td>cellule(2,1)</td>

21. <td>cellule(2,2)</td>

22. <td>cellule(2,3</td>

23. </tr>

24. </table>

26. <table border="0">

27. <tr>

28. <td>Une image</td>

29. <td><img border="0" src="/images/univ01.gif" width="80" height="95"></td>

30. </tr>

31. <tr>

32. <td>le site de l'ISTIA</td>

33. <td><a href="http://istia.univ-angers.fr">ici</a></td>

34. </tr>

35. </table>

36. </body>

37. </html>

Ont été mis en relief dans le code les seuls points qui nous intéressent :

Elément

balises et exemples HTML

titre du document

<title>balises</title>
balises apparaîtra dans la barre de titre du navigateur qui affichera le document

barre horizontale

<hr> : affiche un trait horizontal

tableau

<table attributs>….</table> : pour définir le tableau
<tr attributs>…</tr> : pour définir une ligne
<td attributs>…</td> : pour définir une cellule
exemples :
<table border="1">…</table> : l'attribut border définit l'épaisseur de la bordure du tableau
<td valign="middle" align="center" width="150">cellule(1,2)</td> : définit une cellule dont le contenu sera cellule(1,2). Ce contenu sera centré verticalement (valign="middle") et horizontalement (align="center"). La cellule aura une largeur de 150 pixels (width="150")

image

<img border="0" src="/images/univ01.gif" width="80" height="95"> : définit une image sans bordure (border=0"), de hauteur 95 pixels (height="95"), de largeur 80 pixels (width="80") et dont le fichier source est /images/univ01.gif sur le serveur web (src="/images/univ01.gif"). Ce lien se trouve sur un document web qui a été obtenu avec l'URL http://localhost:81/html/balises.htm. Aussi, le navigateur demandera-t-il l'URL http://localhost:81/images/univ01.gif pour avoir l'image référencée ici.

lien

<a href="http://istia.univ-angers.fr">ici</a> : fait que le texte ici sert de lien vers l'URL http://istia.univ-angers.fr.

fond de page

<body background="/images/standard.jpg"> : indique que l'image qui doit servir de fond de page se trouve à l'URL /images/standard.jpg du serveur web. Dans le contexte de notre exemple, le navigateur demandera l'URL http://localhost:81/images/standard.jpg pour obtenir cette image de fond.

On voit dans ce simple exemple que pour construire l'intéralité du document, le navigateur doit faire trois requêtes au serveur :

1. http://localhost:81/html/balises.htm pour avoir le source HTML du document

2. http://localhost:81/images/univ01.gif pour avoir l'image univ01.gif

3. http://localhost:81/images/standard.jpg pour obtenir l'image de fond standard.jpg

L'exemple suivant présente un formulaire Web créé lui aussi avec FrontPage.

Le code HTML généré par FrontPage et un peu épuré est le suivant :

1. <html>

3. <head>

4. <title>balises</title>

5. <script language="JavaScript">

6. function effacer(){

7. alert("Vous avez cliqué sur le bouton Effacer");

8. }//effacer

9. </script>

10. </head>

12. <body background="/images/standard.jpg">

14. <form method="POST" >

16. <table border="0">

17. <tr>

18. <td>Etes-vous marié(e)</td>

19. <td>

20. <input type="radio" value="Oui" name="R1">Oui

21. <input type="radio" name="R1" value="non" checked>Non

22. </td>

23. </tr>

24. <tr>

25. <td>Cases à cocher</td>

26. <td>

27. <input type="checkbox" name="C1" value="un">1

28. <input type="checkbox" name="C2" value="deux" checked>2

29. <input type="checkbox" name="C3" value="trois">3

30. </td>

31. </tr>

32. <tr>

33. <td>Champ de saisie</td>

34. <td>

35. <input type="text" name="txtSaisie" size="20" value="qqs mots">

36. </td>

37. </tr>

38. <tr>

39. <td>Mot de passe</td>

40. <td>

41. <input type="password" name="txtMdp" size="20" value="unMotDePasse">

42. </td>

43. </tr>

44. <tr>

45. <td>Boîte de saisie</td>

46. <td>

47. <textarea rows="2" name="areaSaisie" cols="20">

48. ligne1

49. ligne2

50. ligne3

51. </textarea>

52. </td>

53. </tr>

54. <tr>

55. <td>combo</td>

56. <td>

57. <select size="1" name="cmbValeurs">

58. <option>choix1</option>

59. <option selected>choix2</option>

60. <option>choix3</option>

61. </select>

62. </td>

63. </tr>

64. <tr>

65. <td>liste à choix simple</td>

66. <td>

67. <select size="3" name="lst1">

68. <option selected>liste1</option>

69. <option>liste2</option>

70. <option>liste3</option>

71. <option>liste4</option>

72. <option>liste5</option>

73. </select>

74. </td>

75. </tr>

76. <tr>

77. <td>liste à choix multiple</td>

78. <td>

79. <select size="3" name="lst2" multiple>

80. <option>liste1</option>

81. <option>liste2</option>

82. <option selected>liste3</option>

83. <option>liste4</option>

84. <option>liste5</option>

85. </select>

86. </td>

87. </tr>

88. <tr>

89. <td>bouton</td>

90. <td>

91. <input type="button" value="Effacer" name="cmdEffacer" onclick="effacer()">

92. </td>

93. </tr>

94. <tr>

95. <td>envoyer</td>

96. <td>

97. <input type="submit" value="Envoyer" name="cmdRenvoyer">

98. </td>

99. </tr>

100. <tr>

101. <td>rétablir</td>

102. <td>

103. <input type="reset" value="Rétablir" name="cmdRétablir">

104. </td>

105. </tr>

106. </table>

107. <input type="hidden" name="secret" value="uneValeur">

109. </form>

110. </body>

111. </html>

L'association contrôle visuel <--> balise HTML est le suivant :

Contrôle

balise HTML

formulaire

<form method="POST" >

champ de saisie

<input type="text" name="txtSaisie" size="20" value="qqs mots">

champ de saisie cachée

<input type="password" name="txtMdp" size="20" value="unMotDePasse">

champ de saisie multilignes

<textarea rows="2" name="areaSaisie" cols="20">
ligne1
ligne2
ligne3
</textarea>

boutons radio

<input type="radio" value="Oui" name="R1">Oui
<input type="radio" name="R1" value="non" checked>Non

cases à cocher

<input type="checkbox" name="C1" value="un">1
<input type="checkbox" name="C2" value="deux" checked>2
<input type="checkbox" name="C3" value="trois">3

Combo

<select size="1" name="cmbValeurs">
<option>choix1</option>
<option selected>choix2</option>
<option>choix3</option>
</select>

liste à sélection unique

<select size="3" name="lst1">
<option selected>liste1</option>
<option>liste2</option>
<option>liste3</option>
<option>liste4</option>
<option>liste5</option>
</select>

liste à sélection multiple

<select size="3" name="lst2" multiple>
<option>liste1</option>
<option>liste2</option>
<option selected>liste3</option>
<option>liste4</option>
<option>liste5</option>
</select>

bouton de type submit

<input type="submit" value="Envoyer" name="cmdRenvoyer">

bouton de type reset

<input type="reset" value="Rétablir" name="cmdRétablir">

bouton de type button

<input type="button" value="Effacer" name="cmdEffacer" onclick="effacer()">

Passons en revue ces différents contrôles.

II-H-1-a - Le formulaire

formulaire

<form method="POST" >

balise HTML

<form name="…" method="…" action="…">…</form>

attributs

name="frmexemple" : nom du formulaire
method="…" : méthode utilisée par le navigateur pour envoyer au serveur web les valeurs récoltées dans le formulaire
action="…" : URL à laquelle seront envoyées les valeurs récoltées dans le formulaire.
Un formulaire web est entouré des balises <form>…</form>. Le formulaire peut avoir un nom (name="xx"). C'est le cas pour tous les contrôles qu'on peut trouver dans un formulaire. Ce nom est utile si le document web contient des scripts qui doivent référencer des éléments du formulaire. Le but d'un formulaire est de rassembler des informations données par l'utilisateur au clavier/souris et d'envoyer celles-ci à une URL de serveur web. Laquelle ? Celle référencée dans l'attribut action="URL". Si cet attribut est absent, les informations seront envoyées à l'URL du document dans lequel se trouve le formulaire. Ce serait le cas dans l'exemple ci-dessus. Jusqu'à maintenant, nous avons toujours vu le client web comme "demandant" des informations à un serveur web, jamais lui "donnant" des informations. Comment un client web fait-il pour donner des informations (celles contenues dans le formulaire) à un serveur web ? Nous y reviendrons dans le détail un peu plus loin. Il peut utiliser deux méthodes différentes appelées POST et GET. L'attribut method="méthode", avec méthode égale à GET ou POST, de la balise <form> indique au navigateur la méthode à utiliser pour envoyer les informations recueillies dans le formulaire à l'URL précisée par l'attribut action="URL". Lorsque l'attribut method n'est pas précisé, c'est la méthode GET qui est prise par défaut.

II-H-1-b - Champ de saisie

champ de saisie

<input type="text" name="txtSaisie" size="20" value="qqs mots">
<input type="password" name="txtMdp" size="20" value="unMotDePasse">

balise HTML

<input type="…" name="…" size=".." value="..">
La balise input existe pour divers contrôles. C'est l'attribut type qui permet de différentier ces différents contrôles entre eux.

attributs

type="text" : précise que c'est un champ de saisie
type="password" : les caractères présents dans le champ de saisie sont remplacés par des caractères *. C'est la seule différence avec le champ de saisie normal. Ce type de contrôle convient pour la saisie des mots de passe.
size="20" : nombre de caractères visibles dans le champ - n'empêche pas la saisie de davantage de caractères
name="txtSaisie" : nom du contrôle
value="qqs mots" : texte qui sera affiché dans le champ de saisie.

II-H-1-c - Champ de saisie multilignes

champ de saisie multilignes

<textarea rows="2" name="areaSaisie" cols="20">
ligne1
ligne2
ligne3
</textarea>

balise HTML

<textarea …>texte</textarea>
affiche une zone de saisie multilignes avec au départ texte dedans

attributs

rows="2" : nombre de lignes
cols="'20" : nombre de colonnes
name="areaSaisie" : nom du contrôle

II-H-1-d - Boutons radio

boutons radio

<input type="radio" value="Oui" name="R1">Oui
<input type="radio" name="R1" value="non" checked>Non

balise HTML

<input type="radio" attribut2="valeur2" ….>texte
affiche un bouton radio avec texte à côté.

attributs

name="radio" : nom du contrôle. Les boutons radio portant le même nom forment un groupe de boutons exclusifs les uns des autres : on ne peut cocher que l'un d'eux.
value="valeur" : valeur affectée au bouton radio. Il ne faut pas confondre cette valeur avec le texte affiché à côté du bouton radio. Celui-ci n'est destiné qu'à l'affichage.
checked : si ce mot clé est présent, le bouton radio est coché, sinon il ne l'est pas.

II-H-1-e - Cases à cocher

cases à cocher

<input type="checkbox" name="C1" value="un">1
<input type="checkbox" name="C2" value="deux" checked>2
<input type="checkbox" name="C3" value="trois">3

balise HTML

<input type="checkbox" attribut2="valeur2" ….>texte
affiche une case à cocher avec texte à côté.

attributs

name="C1" : nom du contrôle. Les cases à cocher peuvent porter ou non le même nom. Les cases portant le même nom forment un groupe de cases associées.
value="valeur" : valeur affectée à la case à cocher. Il ne faut pas confondre cette valeur avec le texte affiché à côté du bouton radio. Celui-ci n'est destiné qu'à l'affichage.
checked : si ce mot clé est présent, le bouton radio est coché, sinon il ne l'est pas.

II-H-1-f - Liste déroulante (combo)

Combo

<select size="1" name="cmbValeurs">
<option>choix1</option>
<option selected>choix2</option>
<option>choix3</option>
</select>

balise HTML

<select size=".." name="..">
<option [selected]>…</option>
…
</select>
affiche dans une liste les textes compris entre les balises <option>…</option>

attributs

name="cmbValeurs" : nom du contrôle.
size="1" : nombre d'éléments de liste visibles. size="1" fait de la liste l'équivalent d'un combobox.
selected : si ce mot clé est présent pour un élément de liste, ce dernier apparaît sélectionné dans la liste. Dans notre exemple ci-dessus, l'élément de liste choix2 apparaît comme l'élément sélectionné du combo lorsque celui-ci est affiché pour la première fois.

II-H-1-g - Liste à sélection unique

liste à sélection unique

<select size="3" name="lst1">
<option selected>liste1</option>
<option>liste2</option>
<option>liste3</option>
<option>liste4</option>
<option>liste5</option>
</select>

balise HTML

<select size=".." name="..">
<option [selected]>…</option>
…
</select>
affiche dans une liste les textes compris entre les balises <option>…</option>

attributs

les mêmes que pour la liste déroulante n'affichant qu'un élément. Ce contrôle ne diffère de la liste déroulante précédente que par son attribut size>1.

II-H-1-h - Liste à sélection multiple

liste à sélection unique

<select size="3" name="lst2" multiple>
<option selected>liste1</option>
<option>liste2</option>
<option selected>liste3</option>
<option>liste4</option>
<option>liste5</option>
</select>

balise HTML

<select size=".." name=".." multiple>
<option [selected]>…</option>
…
</select>
affiche dans une liste les textes compris entre les balises <option>…</option>

attributs

multiple : permet la sélection de plusieurs éléments dans la liste. Dans l'exemple ci-dessus, les éléments liste1 et liste3 sont tous deux sélectionnés.

II-H-1-i - Bouton de type button

bouton de type button

<input type="button" value="Effacer" name="cmdEffacer" onclick="effacer()">

balise HTML

<input type="button" value="…" name="…" onclick="effacer()" ….>

attributs

type="button" : définit un contrôle bouton. Il existe deux autres types de bouton, les types submit et reset.
value="Effacer" : le texte affiché sur le bouton
onclick="fonction()" : permet de définir une fonction à exécuter lorsque l'utilisateur clique sur le bouton. Cette fonction fait partie des scripts définis dans le document web affiché. La syntaxe précédente est une syntaxe javascript. Si les scripts sont écrits en vbscript, il faudrait écrire onclick="fonction" sans les parenthèses. La syntaxe devient identique s'il faut passer des paramètres à la fonction : onclick="fonction(val1, val2,…)"
Dans notre exemple, un clic sur le bouton Effacer appelle la fonction javascript effacer suivante :
<script language="JavaScript">
function effacer(){
alert("Vous avez cliqué sur le bouton Effacer");
}//effacer
</script>
La fonction effacer affiche un message :

II-H-1-j - Bouton de type submit

bouton de type submit

<input type="submit" value="Envoyer" name="cmdRenvoyer">

balise HTML

<input type="submit" value="Envoyer" name="cmdRenvoyer">

attributs

type="submit" : définit le bouton comme un bouton d'envoi des données du formulaire au serveur web. Lorsque le client va cliquer sur ce bouton, le navigateur va envoyer les données du formulaire à l'URL définie dans l'attribut action de la balise <form> selon la méthode définie par l'attribut method de cette même balise.
value="Envoyer" : le texte affiché sur le bouton

II-H-1-k - Bouton de type reset

bouton de type reset

<input type="reset" value="Rétablir" name="cmdRétablir">

balise HTML

<input type="reset" value="Rétablir" name="cmdRétablir">

attributs

type="reset" : définit le bouton comme un bouton de réinitialisation du formulaire. Lorsque le client va cliquer sur ce bouton, le navigateur va remettre le formulaire dans l'état où il l'a reçu.
value="Rétablir" : le texte affiché sur le bouton

II-H-1-l - Champ caché

champ caché

<input type="hidden" name="secret" value="uneValeur">

balise HTML

<input type="hidden" name="…" value="…">

attributs

type="hidden" : précise que c'est un champ caché. Un champ caché fait partie du formulaire mais n'est pas présenté à l'utilisateur. Cependant, si celui-ci demandait à son navigateur l'affichage du code source, il verrait la présence de la balise <input type="hidden" value="…"> et donc la valeur du champ caché.
value="uneValeur" : valeur du champ caché.
Quel est l'intérêt du champ caché ? Cela peut permettre au serveur web de garder des informations au fil des requêtes d'un client. Considérons une application d'achats sur le web. Le client achète un premier article art1 en quantité q1 sur une première page d'un catalogue puis passe à une nouvelle page du catalogue. Pour se souvenir que le client a acheté q1 articles art1, le serveur peut mettre ces deux informations dans un champ caché du formulaire web de la nouvelle page. Sur cette nouvelle page, le client achète q2 articles art2. Lorsque les données de ce second formulaire vont être envoyées au serveur (submit), celui-ci va non seulement recevoir l'information (q2,art2) mais aussi (q1,art1) qui fait partie également partie du formulaire en tant que champ caché non modifiable par l'utilisateur. Le serveur web va alors mettre dans un nouveau champ caché les informations (q1,art1) et (q2,art2) et envoyer une nouvelle page de catalogue. Et ainsi de suite.

II-H-2 - Envoi à un serveur web par un client web des valeurs d'un formulaire

Nous avons dit dans l'étude précédente que le client web disposait de deux méthodes pour envoyer à un serveur web les valeurs d'un formulaire qu'il a affiché : les méthodes GET et POST. Voyons sur un exemple la différence entre les deux méthodes. Nous reprenons l'exemple précédent et le traitons de la façon suivante :

1. un navigateur demande l'URL de l'exemple à un serveur web

2. une fois le formulaire obtenu, nous le remplissons

3. avant d'envoyer les valeurs du formulaire au serveur web en cliquant sur le bouton Envoyer de type submit, nous arrêtons le serveur web et le remplaçons par le serveur TCP générique déjà utilisé précédemment. Rappelons que celui-ci affiche à l'écran les lignes de texte que lui envoie le client web. Ainsi nous verrons ce qu'envoie exactement le navigateur.

Le formulaire est rempli de la façon suivante :

L'URL utilisée pour ce document est la suivante :

II-H-2-a - Méthode GET

Le document HTML est programmé pour que le navigateur utilise la méthode GET pour envoyer les valeurs du formulaire au serveur web. Nous avons donc écrit :

<form method="GET" >

Nous arrêtons le serveur web et lançons notre serveur TCP générique sur le port 81 :

dos>java serveurTCPgenerique 81
Serveur générique lancé sur le port 81

Maintenant, nous revenons dans notre navigateur pour envoyer les données du formulaire au serveur web à l'aide du bouton Envoyer :

Voici alors ce que reçoit le serveur TCP générique :

1. <-- GET /html/balises.htm?R1=Oui&C1=un&C2=deux&txtSaisie=programmation+web&txtMdp=ceciestsecret&area

2. Saisie=les+bases+de+la%0D%0Aprogrammation+web&cmbValeurs=choix3&lst1=liste3&lst2=liste1&lst2=liste3&

3. cmdRenvoyer=Envoyer&secret=uneValeur HTTP/1.1

4. <-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/msword, application/vnd

5. .ms-powerpoint, application/vnd.ms-excel, */*

6. <-- Referer: http://localhost:81/html/balises.htm

7. <-- Accept-Language: fr

8. <-- Accept-Encoding: gzip, deflate

9. <-- User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705)

10. <-- Host: localhost:81

11. <-- Connection: Keep-Alive

12. <--

Tout est dans le premier entête HTTP envoyé par le navigateur :

1. <-- GET /html/balises.htm?R1=Oui&C1=un&C2=deux&txtSaisie=programmation+web&txtMdp=ceciestsecret&area

2. Saisie=les+bases+de+la%0D%0Aprogrammation+web&cmbValeurs=choix3&lst1=liste3&lst2=liste1&lst2=liste3&

3. cmdRenvoyer=Envoyer&secret=uneValeur HTTP/1.1

On voit qu'il est beaucoup plus complexe que ce qui avait été rencontré jusqu'à maintenant. On y retrouve la syntaxe GET URL HTTP/1.1 mais sous une forme particulière GET URL?param1=valeur1&param2=valeur2&… HTTP/1.1 où les parami sont les noms des contrôles du formulaire web et valeuri les valeurs qui leur sont associées. Examinons-les de plus près. Nous présentons ci-dessous un tableau à trois colonnes :

- colonne 1 : reprend la définition d'un contrôle HTML de l'exemple

- colonne 2 : donne l'affichage de ce contrôle dans un navigateur

- colonne 3 : donne la valeur envoyée au serveur par le navigateur pour le contrôle de la colonne 1 sous la forme qu'elle a dans la requête GET de l'exemple

contrôle HTML

visuel

valeur(s) renvoyée(s)

<input type="radio" value="Oui" name="R1">Oui
<input type="radio" name="R1" value="non" checked>Non

- la valeur de l'attribut value du bouton radio coché par l'utilisateur.

<input type="checkbox" name="C1" value="un">1
<input type="checkbox" name="C2" value="deux" checked>2
<input type="checkbox" name="C3" value="trois">3

C1=un
C2=deux
- valeurs des attributs value des cases cochées par l'utilisateur

<input type="text" name="txtSaisie" size="20" value="qqs mots">

txtSaisie=programmation+web
- texte tapé par l'utilisateur dans le champ de saisie. Les espaces ont été remplacés par le signe +

<input type="password" name="txtMdp" size="20" value="unMotDePasse">

txtMdp=ceciestsecret
- texte tapé par l'utilisateur dans le champ de saisie

<textarea rows="2" name="areaSaisie" cols="20">
ligne1
ligne2
ligne3
</textarea>

areaSaisie=les+bases+de+la%0D%0A
programmation+web
- texte tapé par l'utilisateur dans le champ de saisie. %OD%OA est la marque de fin de ligne. Les espaces ont été remplacés par le signe +

<select size="1" name="cmbValeurs">
<option>choix1</option>
<option selected>choix2</option>
<option>choix3</option>
</select>

cmbValeurs=choix3
- valeur choisie par l'utilisateur dans la liste à sélection unique

<select size="3" name="lst1">
<option selected>liste1</option>
<option>liste2</option>
<option>liste3</option>
<option>liste4</option>
<option>liste5</option>
</select>

lst1=liste3
- valeur choisie par l'utilisateur dans la liste à sélection unique

<select size="3" name="lst2" multiple>
<option selected>liste1</option>
<option>liste2</option>
<option selected>liste3</option>
<option>liste4</option>
<option>liste5</option>
</select>

lst2=liste1
lst2=liste3
- valeurs choisies par l'utilisateur dans la liste à sélection multiple

<input type="submit" value="Envoyer" name="cmdRenvoyer">

cmdRenvoyer=Envoyer
- nom et attribut value du bouton qui a servi à envoyer les données du formulaire au serveur

<input type="hidden" name="secret" value="uneValeur">

secret=uneValeur
- attribut value du champ caché

Refaisons la même chose mais cette fois-ci en gardant le serveur web pour élaborer la réponse et voyons quelle est cette dernière. La page renvoyée par le serveur Web est la suivante :

C'est exactement la même que celle reçue initialement avant le remplissage du formulaire. Pour comprendre pourquoi, il faut regarder de nouveau l'URL demandée par le navigateur lorsque l'utilisateur appuie sur le bouton Envoyer :

1. <-- GET /html/balises.htm?R1=Oui&C1=un&C2=deux&txtSaisie=programmation+web&txtMdp=ceciestsecret&area

2. Saisie=les+bases+de+la%0D%0Aprogrammation+web&cmbValeurs=choix3&lst1=liste3&lst2=liste1&lst2=liste3&

3. cmdRenvoyer=Envoyer&secret=uneValeur HTTP/1.1

L'URL demandée est /html/balises.htm. On passe de plus à cette URL des valeurs qui sont celles du formulaire. Pour l'instant, l'URL /html/balises.htm qui est une page statique n'utilise pas ces valeurs. Si bien que le GET précédent est équivalent à

1. <-- GET /html/balises.htm HTTP/1.1

et c'est pourquoi le serveur nous a renvoyé de nouveau la page initiale. On remarquera que le navigateur affiche bien lui l'URL complète qui a été demandée :

II-H-2-b - Méthode POST

Le document HTML est programmé pour que le navigateur utilise maintenant la méthode POST pour envoyer les valeurs du formulaire au serveur web :

1. <form method="POST" >

Nous arrêtons le serveur web et lançons le serveur TCP générique (déjà rencontré mais un peu modifié pour l'occasion) sur le port 81 :

1. dos>java serveurTCPgenerique2 81

2. Serveur générique lancé sur le port 81

Maintenant, nous revenons dans notre navigateur pour envoyer les données du formulaire au serveur web à l'aide du bouton Envoyer :

Voici alors ce que reçoit le serveur TCP générique :

1. <-- POST /html/balises.htm HTTP/1.1

2. <-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/msword, application/vnd

3. .ms-powerpoint, application/vnd.ms-excel, */*

4. <-- Referer: http://localhost:81/html/balises.htm

5. <-- Accept-Language: fr

6. <-- Content-Type: application/x-www-form-urlencoded

7. <-- Accept-Encoding: gzip, deflate

8. <-- User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705)

9. <-- Host: localhost:81

10. <-- Content-Length: 210

11. <-- Connection: Keep-Alive

12. <-- Cache-Control: no-cache

13. <--

14. <-- R1=Oui&C1=un&C2=deux&txtSaisie=programmation+web&txtMdp=ceciestsecret&areaSaisie=les+bases+de+la%0D%0Aprogrammation+web&cmbValeurs=choix3&lst1=liste3&lst2=liste1&lst2=liste3&cmdRenvoyer=Envoyer&secret=uneValeur

Par rapport à ce que nous connaissons déjà, nous notons les changements suivants dans la requête du navigateur :

- L'entête HTTP initial n'est plus GET mais POST. La syntaxe est POST URL HTTP/1.1 où URL est l'URL demandée par le navigateur. En même temps, POST signifie que le navigateur a des données à transmettre au serveur.

- La ligne Content-Type: application/x-www-form-urlencoded indique quel type de données va envoyer le navigateur. Ce sont des données de formulaire (x-www-form) codées (urlencoded). Ce codage fait que certains caractères des données transmises sont transformées afin d'éviter au serveur des erreurs d'interprétation. Ainsi, l'espace est remplacé par +, la marque de fin de ligne par %OD%OA,… De façon générale, tous les caractères contenus dans les données et susceptibles d'une interprétation erronée par le serveur (&, +, %…) sont transformés en %XX où XX est leur code hexadécimal.

- La ligne Content-Length: 210 indique au serveur combien de caractères le client va lui envoyer une fois les entêtes HTTP terminés, c.a.d. après la ligne vide signalant la fin des entêtes.

- Les données (210 caractères) : R1=Oui&C1=un&C2=deux&txtSaisie=programmation+web&txtMdp=ceciestsecret&areaSaisie=les+bases+de+la%0D%0Aprogrammation+web&cmbValeurs=choix3&lst1=liste3&lst2=liste1&lst2=liste3&cmdRenvoyer=Envoyer&secret=uneValeur

On remarque que les données transmises par POST le sont au même format que celle transmises par GET.

Y-a-t-il une méthode meilleure que l'autre ? Nous avons vu que si les valeurs d'un formulaire étaient envoyées par le navigateur avec la méthode GET, le navigateur affichait dans son champ Adresse l'URL demandée sous la forme URL?param1=val1&param2=val2&…. On peut voir cela comme un avantage ou un inconvénient :

1. un avantage si on veut permettre à l'utilisateur de placer cette URL paramétrée dans ses liens favoris

2. un inconvénient si on ne souhaite pas que l'utilisateur ait accès à certaines informations du formulaire tels, par exemple, les champs cachés

Par la suite, nous utiliserons quasi exclusivement la méthode POST dans nos formulaires.

II-H-2-c - Récupération des valeurs d'un formulaire Web

Une page statique demandée par un client qui envoie de plus des paramètres par POST ou GET ne peut en aucune façon récupérer ceux-ci. Seul un programme peut le faire et c'est lui qui se chargera alors de générer une réponse au client, une réponse qui sera dynamique et généralement fonction des paramètres reçus. C'est le domaine de la programmation web, domaine que nous abordons plus en détail dans le chapitre suivant avec la présentation des technologies Java de programmation web : les servlets et les pages JSP.

III - Introduction à la programmation web en PHP

III-A - Programmation PHP

Rappelons ici que PHP est un langage à part entière et que s'il est surtout utilisé dans le cadre du développelent d'applications pour le web, il peut être utilisé dans d'autres contextes. Montrons avec un exemple simple la procédure d'exécution d'un programme PHP sous windows. Le code suivant a été sauvegardé sous le nom coucou.php.

coucou.php

1. <?php

2. echo "coucou";

3. ?>

L'exécution de ce programme se fait dans une fenêtre Dos de Windows :

1. dos>"e:\program files\easyphp\php\php.exe" coucou.php

2. X-Powered-By: PHP/4.3.0-dev

3. Content-type: text/html

5. coucou

On remarquera que l'interpréteur PHP envoie par défaut :

1. l'interpréteur PHP est php.exe et se trouve normalement dans le répertoire <php> d'installation du logiciel.

2. des entêtes HTTP X-Powered-By et Content-type :

3. la ligne vide séparant les entêtes HTTP du reste du document

4. le document formé ici du texte écrit par la fonction echo

III-B - Le fichier de configuration de l'interpréteur PHP

Le comportement de l'interpréteur PHP est paramétré par un fichier de configuration appelé php.ini et rangé, sous windows, dans le répertoire de windows lui-même. C'est un fichier de taille conséquente puisque sous windows et pour la version 4.2 de PHP, il fait près de 1000 lignes dont heureusement les trois-quarts sont des commentaires. Examinons quelques-uns des attributs de configuration de PHP :

short_open_tag = On

permet d'inclure des instructions entre des balises <? >. A off, il faudrait les inclure entre <?php … >

asp_tags = Off

à on permet d'utiliser la syntaxe <% =variable %> utilisée par la technologie asp (Active Server Pages)

expose_php = On

permet l'envoi de l'entête HTTP X-Powered-By: PHP/4.3.0-dev. A off, cet entête est supprimé.

error_reporting = E_ALL & ~E_NOTICE

fixe l'étendue du suivi d'erreurs. Ici toutes les erreurs (E_ALL) sauf les avertissements en cours d'exécution (~E_NOTICE) seront signalées

display_errors = Off

à on, place les erreurs dans le flux HTML envoyé au client. Celles-ci sont donc visualisées dans le navigateur. Il est conseillé de mettre cette option à off.

log_errors = On

les erreurs seront mémorisées dans un fichier

track_errors = On

mémorise la dernière erreur survenue dans la variable $php_errormsg

error_log = E:\Program Files\EasyPHP\php\erreurs.log

fixe le fichier de mémorisation des erreurs (si log_errors=on)

register_globals = Off

à on, un certain nombre de variables deviennent globales. Considéré comme un trou de sécurité.

default_mimetype = "text/html"

génère par défaut l'entête HTTP : Content-type: text/html

include_path=".;E:\Program Files\EasyPHP\php\pear\"

la liste des répertoires qui seront explorés à la recherche des fichiers requis par des directives include ou require

session.save_path = /temp

le répertoire où seront sauvegardés les fichiers mémorisant les différentes sessions en cours. Le disque concerné est celui où a été installé PHP. Ici /temp désigne e:\temp

Ce fichier de configuration influe sur la portabilité du programme php écrit. En effet, si une application web doit récupérer la valeur d'un champ C d'un formulaire web, elle pourra le faire de diverses façons selon que la variable de configuration register_globals a la valeur on ou off :

- off : la valeur sera récupérée par $HTTP_GET_VARS["C"] ou _GET["C"] ou $HTTP_POST_VARS["C"] ou $_POST["C"] selon la méthode (GET/POST) utilisée par le client pour envoyer les valeurs du formulaire

- on : idem ci-dessus plus $C, car la valeur du champ C a été rendue globale dans une variable portant le même nom que le champ

Si un développeur écrit un programme en utilisant la notation $C car le serveur web/php qu'il utilise à la variable register_globals à on, ce programme ne fonctionnera plus s'il est porté sur un serveur web/php où cette même variable est à off. On cherchera donc à écrire des programmes en évitant d'utiliser des fonctionnalités dépendant de la configuration du serveur web/php.

III-C - Configurer PHP à l'exécution

Pour améliorer la portabilité d'un programme PHP, on peut fixer soi-même certaines des variables de configuration de PHP. Celles-ci sont modifiées le temps de l'exécution du programme et seulement pour lui. Deux fonctions sont utiles dans ce processus :

ini_get("confVariable")

rend la valeur de la variable de configuration confVariable

ini_set("confVariable","valeur")

fixe la valeur de la variable de configuration confVariable

Voici un exemple où on fixe la valeur de la variable de configuration track_errors :

1. <?php

2. // valeur de la variable de configuration track_errors

3. echo "track_errors=".ini_get("track_errors")."\n";

4. // changement de cette valeur

5. ini_set("track_errors","off");

6. // vérification

7. echo "track_errors=".ini_get("track_errors")."\n";

8. ?>

A l'exécution, on a les résultats suivants :

1. dos>"E:\Program Files\EasyPHP\php\php.exe" conf1.php

2. Content-type: text/html

4. track_errors=1

5. track_errors=off

La valeur de la variable de configuration track_errors était initialement 1 (~on). On la fait passer à off. On retiendra que si notre application doit s'appuyer sur certaines valeurs des variables de configuration, il est prudent d'initialiser celles-ci dans le programme lui-même.

III-D - Contexte d'exécution des exemples

Les exemples de ce polycopié seront exécutés avec la configuration suivante :

1. PC sous Windows 2000

2. serveur Apache 1.3

3. PHP 4.3

La configuration du serveur Apache est faite dans le fichier httpd.conf. Les lignes suivantes indiquent à Apache de charger PHP comme module intégré à Apache et de passer à l'interpréteur PHP tout requête visant un document ayant certains suffixes dont .php. C'est le suffixe par défaut que nous utiliserons pour nos programmes php.

httpd.conf

1. LoadModule php4_module "E:/Program Files/EasyPHP/php/php4apache.dll"

2. AddModule mod_php4.c

3. AddType application/x-httpd-php .phtml .pwml .php3 .php4 .php .php2 .inc

Par ailleurs, nous avons défini pour Apache, un alias poly :

httpd.conf

1. Alias "/poly/" "e:/data/serge/web/php/poly/"

2. <Directory "e:/data/serge/web/php/poly">

3. Options Indexes FollowSymLinks Includes

4. AllowOverride All

5. #Order allow,deny

6. Allow from all

7. </Directory>

Appelons <poly> le chemin e:/data/serge/web/php/poly. Si nous voulons demander avec un navigateur le document doc.php au serveur Apache, nous utiliserons l'URL http://localhost/poly/doc.php. Le serveur Apache reconnaîtra dans l'URL l'alias poly et associera alors l'URL /poly/doc.php au document <poly>\doc.php.

III-E - Un premier exemple

Écrivons une première application web/php. Le texte suivant est enregistré dans le fichier heure.php :

heure.php

1. <html>

2. <head>

3. <title>Une page php dynamique</title>

4. </head>

5. <body>

6. <center>

7. <h1>Une page PHP générée dynamiquement</h1>

8. <h2>

9. <?php

10. $maintenant=time();

11. echo date("j/m/y, h:i:s",$maintenant);

12. ?>

13. </h2>

14. <br>

15. A chaque fois que vous rafraîchissez la page, l'heure change.

16. </body>

17. </html>

Si nous demandons cette page avec un navigateur, nous obtenons le résultat suivant :

La partie dynamique de la page a été générée par du code PHP :

1. <?php

2. $maintenant=time();

3. echo date("j/m/y, h:i:s",$maintenant);

4. ?>

Que s'est-il passé exactement ? Le navigateur a demandé l'URL http://localhost/poly/intro/heure.php. Le serveur web (dans l'exemple Apache) a reçu cette demande et a détecté, à cause du suffixe .php du document demandé, qu'il devait faire suivre cette demande à l'interpréteur PHP. Celui-ci analyse alors le document heure.php et exécute toutes les portions de code situées entre les balises <?php > et remplace chacune d'elles par les lignes écrites par les instructions PHP echo ou print. Ainsi l'interpréteur PHP va exécuter la portion de code ci-dessus et la remplacer par la ligne écrite par l'instruction echo :

1. 2/10/02, 06:13:47

Une fois que toutes les portions de code PHP ont été exécutées, le document PHP est devenu un simple document HTML qui est alors envoyé au client.

On cherchera à éviter au maximum de mélanger code PHP et code HTML. Dans ce but, on pourrait réécrire l'application précédente de la façon suivante :

1. <!-- code PHP -->

2. <?php

3. // on récupère l'heure du moment

4. $maintenant=time();

5. $maintenant=date("j/m/y, h:i:s",$maintenant);

6. ?>

7. <!-- code HTML -->

8. <html>

9. <head>

10. <title>Une page php dynamique</title>

11. </head>

12. <body>

13. <center>

14. <h1>Une page PHP générée dynamiquement</h1>

15. <h2>

16. <?php echo  $maintenant ?>

17. </h2>

18. <br>

19. A chaque fois que vous rafraîchissez la page, l'heure change.

20. </body>

21. </html>

Le résultat obtenu dans le navigateur est identique :

La seconde version est meilleure que la première car il y a moins de code PHP dans le code HTML. La structure de la page est ainsi plus visible. On peut aller plus loin en mettant le code PHP et le code HTML dans deux fichiers différents. Le code PHP est stocké dans le fichier heure3.php :

heure3.php

1. <!-- code PHP -->

2. <?php

3. // on récupère l'heure du moment

4. $maintenant=time();

5. $maintenant=date("j/m/y, h:i:s",$maintenant);

6. // on affiche la réponse

7. include "heure3-page1.php";

8. ?>

Le code HTML est lui stocké dans le fichier heure3-page1.php :

heure3-page1.php

1. <!-- code HTML -->

2. <html>

3. <head>

4. <title>Une page php dynamique</title>

5. </head>

6. <body>

7. <center>

8. <h1>Une page PHP générée dynamiquement</h1>

9. <h2>

10. <?php echo  $maintenant ?>

11. </h2>

12. <br>

13. A chaque fois que vous rafraîchissez la page, l'heure change.

14. </body>

15. </html>

Lorsque le navigateur demandera le document heure3.php, celui sera chargé et analysé par l'interpréteur PHP. A la rencontre de la ligne

1. include "heure3-page1.php";

l'interpréteur va inclure le fichier heure3-page1.php dans le code source de heure3.php et l'exécuter. Ainsi tout se passe comme si on avait le code PHP suivant :

heure3-page1.php

1. <!-- code PHP -->

2. <?php

3. // on récupère l'heure du moment

4. $maintenant=time();

5. $maintenant=date("j/m/y, h:i:s",$maintenant);

6. ?>

7. <!-- code HTML -->

8. <html>

9. <head>

10. <title>Une page php dynamique</title>

11. </head>

12. <body>

13. <center>

14. <h1>Une page PHP générée dynamiquement</h1>

15. <h2>

16. <?php echo  $maintenant ?>

17. </h2>

18. <br>

19. A chaque fois que vous rafraîchissez la page, l'heure change.

20. </body>

21. </html>

Le résultat obtenu est le même qu'auparavant :

La solution de mettre les codes PHP et HTML dans des fichiers différents sera adoptée par la suite. Elle présente divers avantages :

- la structure des pages envoyées au client n'est pas noyée dans du code PHP. Ainsi elles peuvent être maintenues par un "web designer" ayant des compétences graphiques mais peu de compétences en PHP.

- le code PHP sert de "frontal" aux requêtes des clients. Il a pour but de calculer les données nécessaires à la page qui sera renvoyée en réponse au client.

La solution présente cependant un inconvénient : au lieu de nécessiter le chargement d'un seul document, elle nécessite le chargement de plusieurs documents d'où une possible perte de performances.

III-F - Récupérer les paramètres envoyés par un client web

III-F-1 - par un POST

Considérons le formulaire suivant où l'utilisateur doit donner deux informations : un nom et un age.

Lorsque l'utilisateur a rempli les champs Nom et Age, il appuie ensuite sur le bouton Envoyer qui est de type submit. Les valeurs du formulaire sont alors envoyées au serveur. Celui-ci renvoie le formulaire avec de plus un tableau listant les valeurs qu'il a reçues :

Le navigateur demande le formulaire à l'application nomage.php suivante :

nomage.php

1. <?php

2. // a-t-on les paramètres attendus ?

3. $post=isset($_POST["txtNom"]) && isset($_POST["txtAge"]);

4. if($post){

5. // on récupère les paramètres txtNom et txtAge "postés" par le client

6. $nom=$_POST["txtNom"];

7. $age=$_POST["txtAge"];

8. } else {

9. $nom="";

10. $age="";

11. }//if

12. // affichage de la page

13. include "nomage-p1.php";

14. ?>

Quelques explications :

- un champ de formulaire HTML appelé champ peut être envoyé au serveur par la méthode GET ou la méthode POST. S'il est envoyé par la méthode GET, le serveur peut le récupérer dans la variable $_GET["champ"] et dans la variable $_POST["champ"] s'il est envoyé par la méthode POST.

- l'existence d'une donnée peut être testée avec la fonction isset(donnée) qui rend true si la donnée existe, false sinon.

- l'application nomage.php construit trois variables : $nom pour le nom du formuaire, $age pour l'âge et $post pour indiquer s'il y a eu des valeurs "postées" ou non. Ces trois variables sont transmises à la page nomage-p1.php. On notera que si celle-ci participe à l'élaboration de la réponse au client, celui-ci n'en a pas connaissance. Pour lui, c'est l'aplication nomage.php qui lui répond.

- la première fois qu'un client demande l'aplication nomage.php on a $post à faux. En effet, lors de ce premier appel, aucune valeur de formulaire n'est transmise au serveur.

La page nomage-p1.php est la suivante :

nomage-p1.php

1. <html>

2. <head>

3. <title>Formulaire web</title>

4. </head>

6. <body>

7. <center>

8. <h3>Un formulaire Web</h3>

9. <h4>Récupération des valeurs des champs d'un formulaire</h4>

10. <hr>

11. <form name="frmPersonne" method="post">

12. <table>

13. <tr>

14. <td>Nom</td>

15. <td><input type="text" value="<?php echo $nom ?>" name="txtNom" size="20"></td>

16. <td>Age</td>

17. <td><input type="text" value="<?php echo $age ?>" name="txtAge" size="3"></td>

18. <tr>

19. </table>

20. <input type="submit" name="cmdEffacer" value="Envoyer">

21. </form>

22. </center>

23. <hr>

24. <?php

25. // y-at-il eu des valeurs postées ?

26. if ($post) {

27. ?>

28. <h4>Valeurs récupérées</h4>

29. <table border="1">

30. <tr>

31. <td>Nom</td><td><?php echo $nom ?></td>

32. <td width="10"></td>

33. <td>Age</td><td><?php echo $age ?></td>

34. <tr>

35. </table>

36. <?php } ?>

37. </body>

38. </html>

L'application nomage-p1.php présente le formulaire frmPersonne. Celui est défini par la balise :

1. <form name="frmPersonne" method="post">

L'attribut action de la balise n'étant pas définie, le navigateur transmettra les données du formulaire à l'URL qu'il a interrogée pour l'avoir, c.a.d. l'application nomage.php.

Distinguons les deux cas d'appel de l'application nomage.php :

1. C'est la première fois que l'utilisateur l'appelle. L'application nomage.php appelle donc l'application nomage-p1.php en lui fournissant les valeurs ($nom,$age,$post)=("","",faux). L'application nomage-p1.php affiche alors un formulaire vide.

2. L'utilisateur remplit le formulaire et utilise le bouton Envoyer (de type submit). Les valeurs du formulaire (txtNom, txtAge) sont alors "postées" (method="post" dans <form>) à l'application nomage.php (attribut action non défini dans <form>). L'application nomage.php calcule ($nom,$age,$post)=(txtNom,txtAge,vrai) et les transmet à l'application nomage-p1.php qui affiche alors un formulaire déjà rempli ainsi que le tableau des valeurs récupérées.

III-F-2 - par un GET

Dans le cas où les valeurs du formulaire sont transmises au serveur par un GET, l'application nomage.php devient l'application nomage2.php suivante :

nomage2.php

1. <?php

2. // a-t-on les paramètres attendus ?

3. $get=isset($_GET["txtNom"]) && isset($_GET["txtAge"]);

4. if($get){

5. // on récupère les paramètres txtNom et txtAge "GETTés" par le client

6. $nom=$_GET["txtNom"];

7. $age=$_GET["txtAge"];

8. } else {

9. $nom="";

10. $age="";

11. }//if

12. // affichage de la page

13. include "nomage-p2.php";

14. ?>

L'application nomage-p2.php est identique à l'application nomage-p1.php aux détails près suivants :

- la balise form est modifiée :

1. <form name="frmPersonne" method="get">

- l'application récupère maintenant une variable $get plutôt que $post :

php

1

1

1

<?php
// y-at-il eu des valeurs envoyées au serveur ?
if ($get) {
?>

A l'exécution, lorsque des valeurs sont saisies dans le formulaire et envoyées au serveur, le navigateur reflète dans son champ URL le fait que les valeurs ont été envoyées par la méthode GET :

III-G - Récupérer les entêtes http envoyés par un client web

Lorsqu'un navigateur fait une demande à un serveur web, il lui envoie un certain nombre d'entêtes HTTP. Il est parfois intéresant d'avoir accès à ceux-ci. On peut s'aider dans un premier temps du tableau associatif $_SERVER. Celui-ci contient diverses informations qui lui sont données par le serveur web, dont entre-autres les entêtes HTTP fournis par le client. Considérons le programme suivant qui affiche toutes les valeurs du tableau $_SERVER :

1. <?php

2. // affiche les variables liées au serveur web

3. // on envoie du texte simple

4. header("Content-type: text/plain");

5. // parcours du tableau associatif $_SERVER

6. reset($_SERVER);

7. while (list($clé,$valeur)=each($_SERVER)){

8. echo "$clé : $valeur\n";

9. }//while

10. ?>

Sauvegardons ce code dans headers.php et demandons cette URL avec un navigateur :

Nous récupérons un certain nombre d'informations dont les entêtes HTTP envoyés par le navigateur. Ceux-ci sont les valeurs associées aux clés commençant par HTTP. Détaillons certaines des informations obtenues ci-dessus :

HTTP_ACCEPT

types de documents acceptés par le client web

HTTP_ACCEPT_CHARSET

types de caractères acceptés dans les documents

HTTP_ACCEPT_ENCODING

types d'encodages acceptés pour les documents

HTTP_ACCEPT_LANGUAGE

types de langues acceptées pour les documents

HTTP_CONNECTION

type de connexion avec le serveur. Keep-Alive : le serveur doit maintenir la liaison ouverte lorsqu'il aura donné sa réponse

HTTP_KEEP_ALIVE

? durée maximale d'ouverture de la liaison

HOST

machine hôte interrogée par le client

HTTP_USER_AGENT

identité du client

REMOTE_ADDR

adresse IP du client

REMOTE_PORT

port de communication utilisé par le client

SERVER_PROTOCOL

protocole HTTP utilisé par le serveur

REQUEST_METHOD

méthode d'interrogation utilisée par le client (GET ou POST)

QUERY_STRING

requête ?param1=val1&param2=val2&… placée derrière l'URL demandée (méthode GET)

En modifiant légèrement le code du programme précédent, nous pouvons ne récupérer que les entêtes HTTP :

1. <?php

2. // affiche les variables liées au serveur web

3. // on envoie du texte simple

4. header("Content-type: text/plain");

5. // parcours du tableau associatif $_SERVER

6. reset($_SERVER);

7. while (list($clé,$valeur)=each($_SERVER)){

8. // entête HTTP ?

9. if(strtolower(substr($clé,0,4))=="http")

10. echo substr($clé,5)." : $valeur\n";

11. }//while

12. ?>

Le résultat obtenu dans le navigateur est le suivant :

Si on veut un entête HTTP précis, on écrira par exemple $_SERVER["HTTP_ACCEPT"].

III-H - Récupérer des informations d'environnement

Le serveur web/php s'exécute dans un environnement qu'on peut connaître via le tableau $_ENV qui mémorise diverses caractéristiques de l'environnement d'exécution. Considérons l'application env1.php suivante :

env1.php

1. <?php

2. // affiche les variables liées au serveur web

3. // on envoie du texte simple

4. header("Content-type: text/plain");

5. // parcours du tableau associatif $_ENV

6. reset($_ENV);

7. while (list($clé,$valeur)=each($_ENV)){

8. echo "$clé : $valeur\n";

9. }//while

10. ?>

Elle donne le résultat suivant dans un navigateur (vue partielle) :

On voit par exemple ci-dessus que le serveur web/php s'exécute sous l'OS Windows NT.

III-I - Exemples

III-I-1 - Génération dynamique de formulaire - 1

Nous prenons comme exemple la génération d'un formulaire n'ayant qu'un contrôle : un combo. Le contenu de ce combo est construit dynamiquement avec des valeurs prises dans un tableau. Dans la réalité, elles sont souvent prises dans une base de données. Le formulaire est le suivant :

Si sur l'exemple ci-dessus, on fait Envoyer, on obtient la réponse suivante :

Le code HTML du formulaire initial, une fois celui-ci généré, est le suivant :

1. <html>

2. <head>

3. <title>Génération de formulaire</title>

4. </head>

5. <body>

6. <h2>Choisissez un nombre</h2>

7. <hr>

8. <form name="frmvaleurs" method="post" action="valeurs.php">

9. <select name="cmbValeurs" size="1">

10. <option>un</option>

11. <option>deux</option>

12. <option>trois</option>

13. <option>quatre</option>

14. <option>cinq</option>

15. <option>six</option>

16. <option>sept</option>

17. <option>huit</option>

18. <option>neuf</option>

19. <option>dix</option>

20. </select>

21. <input type="submit" value="Envoyer" name="cmdEnvoyer">

22. </form>

23. </body>

24. </html>

L'application PHP est composée d'une page principale valeurs.php qui est appelée aussi bien pour obtenir le formulaire initial (la liste des valeurs) que pour traiter les valeurs de celui-ci et fournir la réponse (la valeur choisie). L'application génère deux pages différentes :

- celle du formulaire initial qui sera générée par le programme valeurs-p1.php

- celle de la réponse fournie à l'utilisateur qui sera générée par le programme valeurs-p2.php

L'application valeurs.php est la suivante :

valeurs.php

1. <?php

3. // le tableau des valeurs

4. $valeurs=array("un","deux","trois","quatre","cinq","six","sept","huit","neuf","dix");

6. // a-t-on les paramètres attendus

7. $requêteVide=! isset($_POST["cmbValeurs"]);

9. // on récupère le choix de l'utilisateur

10. if ($requêteVide){

11. // requête initiale

12. include "valeurs-p1.php";

13. }else{

14. // réponse à un POST

15. $choix=$_POST["cmbValeurs"];

16. include "valeurs-p2.php";

17. }

18. ?>

Elle définit le tableau des valeurs et appelle valeurs-p1.php pour générer le formulaire initial si la requête du client était vide ou valeurs-p2.php pour générer la réponse si on avait une requête valide. Le programme valeurs1-php est le suivant :

1. <html>

2. <head>

3. <title>Génération de formulaire</title>

4. </head>

5. <body>

6. <h2>Choisissez un nombre</h2>

7. <hr>

8. <form name="frmvaleurs" method="post" action="valeurs.php">

9. <select name="cmbValeurs" size="1">

10. <?php

11. for($i=0;$i<count($valeurs);$i++){

12. echo "<option>$valeurs[$i]</option>\n";

13. }//for

14. ?>

15. </select>

16. <input type="submit" value="Envoyer" name="cmdEnvoyer">

17. </form>

18. </body>

19. </html>

La liste des valeurs du combo est générée dynamiquement à partir du tableau $valeurs transmis par valeurs.php. Le programme valeurs-p2.php génère la réponse :

valeurs-p2.php

1. <html>

2. <head>

3. <title>réponse</title>

4. </head>

5. <body>

6. <h2>Vous avez choisi le nombre <?php echo $choix ?></h2>

7. </body>

8. </html>

Ici, on se contente d'afficher la valeur de la variable $choix là aussi transmise par valeurs.php.

III-I-2 - Génération dynamique de formulaire - 2

Nous reprenons l'exemple précédent en le modifiant de la façon suivante. Le formulaire proposé est toujours le même :

La réponse est elle différente :

Dans la réponse, on renvoie le formulaire, le nombre choisi par l'utilisateur étant indiqué dessous. Par ailleurs, ce nombre est celui qui apparaît comme sélectionné dans la liste affichée par la réponse.

Le code de valeurs.php est le suivant :

valeurs.php

1. <?php

3. // configuration

4. ini_set("register_globals","off");

6. // le tableau des valeurs

7. $valeurs=array("un","deux","trois","quatre","cinq","six","sept","huit","neuf","dix");

9. // on récupère l'éventuel choix de l'utilisateur

10. $choix=$_POST["cmbValeurs"];

12. // on affiche la réponse

13. include "valeurs-p1.php";

14. ?>

On remarquera qu'ici on a pris soin de configurer PHP pour qu'il n'y ait pas de variables globales. C'est en général une saine précaution car les variables globales posent des problèmes de sécurité. Une alternative est d'initialiser toutes les variables qu'on utilise. Cela aura pour effet "d'écraser" une éventuelle variable globale de même nom.

La page du formulaire est affichée par valeurs-p1.php :

valeurs-p1.php

1. <html>

2. <head>

3. <title>Génération de formulaire</title>

4. </head>

5. <body>

6. <h2>Choisissez un nombre</h2>

7. <hr>

8. <form name="frmvaleurs" method="post" action="valeurs.php">

9. <select name="cmbValeurs" size="1">

10. <?php

11. for($i=0;$i<count($valeurs);$i++){

12. // si option courante est égale au choix, on la sélectionne

13. if (isset($choix) && $choix==$valeurs[$i])

14. echo "<option selected>$valeurs[$i]</option>\n";

15. else echo "<option>$valeurs[$i]</option>\n";

16. }//for

17. ?>

18. </select>

19. <input type="submit" value="Envoyer" name="cmdEnvoyer">

20. </form>

21. <?php

22. // suite page

23. if(isset($choix)){

24. echo "<hr>\n";

25. echo "<h3>Vous avez choisi le nombre $choix</h3>\n";

26. }

27. ?>

28. </body>

29. </html>

Le programme générateur de la page s'appuie sur la variable $choix passée par le programme valeurs.php. Remarquons ici que la structure HTML de la page commence à être sérieusement "polluée" par du code PHP. Le frontal valeurs.php pourrait faire davantage de travail comme le montre la nouvelle version suivante :

valeurs.php

1. <?php

3. // configuration

4. ini_set("register_globals","off");

6. // le tableau des valeurs

7. $valeurs=array("un","deux","trois","quatre","cinq","six","sept","huit","neuf","dix");

9. // on récupère l'éventuel choix de l'utilisateur

10. $choix=$_POST["cmbValeurs"];

12. // on calcule la liste des valeurs à afficher

13. $HTMLvaleurs="";

14. for($i=0;$i<count($valeurs);$i++){

15. // si option courante est égale au choix, on la sélectionne

16. if (isset($choix) && $choix==$valeurs[$i])

17. $HTMLvaleurs.="<option selected>$valeurs[$i]</option>\n";

18. else $HTMLvaleurs.="<option>$valeurs[$i]</option>\n";

19. }//for

21. // on calcule la seconde partie de la page

22. $HTMLpart2="";

23. if(isset($choix)){

24. $HTMLpart2="<hr>\n";

25. $HTMLpart2.="<h3>Vous avez choisi le nombre $choix</h3>\n";

26. }//if

28. // on affiche la réponse

29. include "valeurs-p2.php";

30. ?>

La page est maintenant générée par le programme [valeurs-p2.php] suivant :

valeurs-p2.php

1. <html>

2. <head>

3. <title>Génération de formulaire</title>

4. </head>

5. <body>

6. <h2>Choisissez un nombre</h2>

7. <hr>

8. <form name="frmvaleurs" method="post" action="valeurs.php">

9. <select name="cmbValeurs" size="1">

10. <?php

11. // affichage liste de valeurs

12. echo $HTMLvaleurs;

13. ?>

14. </select>

15. <input type="submit" value="Envoyer" name="cmdEnvoyer">

16. </form>

17. <?php

18. // affichage partie 2

19. echo $HTMLpart2;

20. ?>

21. </body>

22. </html>

Le code HTML est maintenant débarrassé d'une bonne partie du code PHP. Rappelons cependant le but du découpage en un programme frontal qui analyse et traite la demande d'un client et des programmes simplement chargés d'afficher des pages paramétrées par des données transmises par le frontal : c'est de séparer le travail du développeur PHP de celui de l'infographiste. Le développeur PHP travaille sur le frontal, l'infographiste sur les pages web. Dans notre nouvelle version, l'infographiste ne peut plus, par exemple, travailler sur la partie 2 de la page puisqu'il n'a plus accès au code HTML de celle-ci. Dans la première version, il le pouvait. Aucune des deux méthodes n'est donc parfaite.

III-I-3 - Génération dynamique de formulaire - 3

Nous reprenons le même problème que précédemment mais cette fois-ci les valeurs sont prises dans une base de données. Celle-ci est dans notre exemple une base MySQL :

- la base s'appelle dbValeurs

- son propriétaire est admDbValeurs ayant le mot de passe mdpDbValeurs

- la base a une unique table appelée tvaleurs

- cette table n'a qu'un champ entier appelé valeur

dos> mysql --database=dbValeurs --user=admDbValeurs --password=mdpDbValeurs

mysql> show tables;
+---------------------+
| Tables_in_dbValeurs |
+---------------------+
| tvaleurs            |
+---------------------+
1 row in set (0.00 sec)

mysql> describe tvaleurs;
+--------+---------+------+-----+---------+-------+
| Field  | Type    | Null | Key | Default | Extra |
+--------+---------+------+-----+---------+-------+
| valeur | int(11) |      |     | 0       |       |
+--------+---------+------+-----+---------+-------+

mysql> select * from tvaleurs;
+--------+
| valeur |
+--------+
|      0 |
|      1 |
|      2 |
|      3 |
|      4 |
|      6 |
|      5 |
|      7 |
|      8 |
|      9 |
+--------+
10 rows in set (0.00 sec)

Dans une application utilisant une base de données, on trouvera généralement les étapes suivantes :

1. Connexion au SGBD

2. Émission de requêtes SQL vers une base du SGBD

3. Traitement des résultats de ces requêtes

4. Fermeture de la connexion au SGBD

Les étapes 2 et 3 sont réalisées de façon répétée, la fermeture de connexion n'ayant lieu qu'à la fin de l'exploitation de la base. C'est un schéma relativement classique pour toute personne ayant exploité une base de données de façon interactive. Le tableau suivant donne les instructions PHP pour réaliser ces différentes opérations ave le SGBD MySQL :

connexion au SGBD

$connexion=mysql_pconnect($hote,$user,$pwd)
$connexion=mysql_connect($hote,$user,$pwd)
$hote : nom internet de la machine sur laquelle s'exécute le SGBD MySQL. En effet il est possible de travailler avec des SGBD MySQL distants.
$user : nom d'un utilisateur connu du SGBD MySQL
$pwd : son mot de passe
$connexion : la connexion créée
mysql_pconnect crée une connexion persistante avec le SGBD MySQL. Une telle connexion n'est pas fermée à la fin du script. Elle reste ouverte. Ainsi lorsqu'il faudra ouvrir une nouvelle connexion avec le SGBD, PHP cherchera une connexion existante appartenant au même utilisateur. S'il la trouve, il l'utilise. Il y a là un gain de temps.
mysql_connect crée une connexion non persistante qu'on ferme donc lorsque le travail avec le SGBD MySQL est terminé.

requêtes SQL

$résultats=mysql_db_query($base,$requête,$connexion)
$base : la base de MySQL avec laquelle on va travailler
$requête : une requête SQL (insert, delete, update, select…)
$connexion : la connexion au SGBD MySQL
$résultats : les résultats de la requête - diffèrent selon que la requête est un select ou une opération de mise à jour (insert, update, delete…)

traitement des résultats d'un select

$résultats=mysql_db_query($base,"select …",$connexion)
Le résultat d'un select est une table, donc un ensemble de lignes et de colonnes. Cette table est accessible via $résultats.
$ligne=mysql_fetch_row($résultats) lit une ligne de la table et la met dans $ligne sous la forme d'un tableau. Ainsi $ligne[i] est la colonne i de la ligne récupérée. La fonction mysql_fetch_row peut être appelée de façon répétée. A chaque fois, elle lit une nouvelle ligne de la table $résultats. Lorsque la fin de la table est atteinte, la fonction rend la valeur false. Ainsi la table $résultats peut-elle être exploitée de la façon suivante :
while($ligne=mysql_fetch_row($résultats)){
// traite la ligne courante $ligne
}//while

traitement des résultats d'une requête de mise à jour

$résultats=mysql_db_query($base,"insert …",$connexion)
La valeur $résultats est vraie ou fausse selon que l'opération a réussi ou échoué. En cas de succès, la fonction mysql_affected_rows permet de connaître le nombre de lignes modifiées par l'opération de mise à jour.

fermeture de la connexion

mysql_close($connexion)
$connexion : une connexion au SGBD MySQL

Le code du frontal [valeurs.php] devient le suivant :

valeurs.php

1. <?php

3. // configuration

4. ini_set("register_globals","off");

5. ini_set("display_errors","off");

6. ini_set("track_errors","on");

8. // le tableau des valeurs

9. list($erreur,$valeurs)=getValeurs();

11. // y-at-il eu erreur ?

12. if($erreur){

13. // affichage page d'erreur

14. include "valeurs-err.php";

15. // fin

16. return;

17. }//if

19. // on récupère l'éventuel choix de l'utilisateur

20. $choix=$_POST["cmbValeurs"];

22. // on calcule la liste des valeurs à afficher

23. $HTMLvaleurs="";

24. for($i=0;$i<count($valeurs);$i++){

25. // si option courante est égale au choix, on la sélectionne

26. if (isset($choix) && $choix==$valeurs[$i])

27. $HTMLvaleurs.="<option selected>$valeurs[$i]</option>\n";

28. else $HTMLvaleurs.="<option>$valeurs[$i]</option>\n";

29. }//for

31. // on calcule la seconde partie de la page

32. $HTMLpart2="";

33. if(isset($choix)){

34. $HTMLpart2="<hr>\n";

35. $HTMLpart2.="<h3>Vous avez choisi le nombre $choix</h3>\n";

36. }//if

38. // on affiche la réponse

39. include "valeurs-p1.php";

41. // fin

42. return;

44. // ------------------------------------------------------------------------

45. function getValeurs(){

46. // récupère les valeurs dans une base MySQL

47. $user="admDbValeurs";

48. $pwd="mdpDbValeurs";

49. $db="dbValeurs";

50. $hote="localhost";

51. $table="tvaleurs";

52. $champ="valeur";

54. // ouverture d'une connexion persistante au serveur MySQL

55. // ou sinon d'une connexion normale

56. ($connexion=mysql_pconnect($hote,$user,$pwd))

57. || ($connexion=mysql_connect($hote,$user,$pwd));

58. if(! $connexion)

59. return array("Base de données indisponible(".mysql_error()."). Veuillez recommencer ultérieurement.");

61. // obtention des valeurs

62. $selectValeurs=mysql_db_query($db,"select $champ from $table",$connexion);

63. if(! $selectValeurs)

64. return array("Base de données indisponible(".mysql_error()."). Veuillez recommencer ultérieurement.");

66. // les valeurs sont mises dans un tableau

67. $valeurs=array();

68. while($ligne=mysql_fetch_row($selectValeurs)){

69. $valeurs[]=$ligne[0];

70. }//while

72. // fermeture de la connexion (si elle est persistante, elle ne sera en fait pas fermée)

73. mysql_close($connexion);

75. // retour du résultat

76. return array("",$valeurs);

77. }//getValeurs

79. ?>

Cette fois-ci, les valeurs à mettre dans le combo ne sont pas fournies par un tableau mais par la fonction getValeurs(). Cette fonction :

1. ouvre une connexion persistante (mysql_pconnect) avec le serveur mySQL en passant un nom d'utilisateur enregistré et son mot de passe.

2. une foix la connexion obtenue, une requête select est émise pour récupérer les valeurs présentes dans la table tvaleurs de la base dbValeurs.

3. le résultat du select est mis dans le tableau $valeurs qui est rendu au programme appelant.

4. la fonction rend en fait un tableau à deux résultats ($erreur, $valeurs) où le premier élément est un éventuel message d'erreur ou la chaîne vide sinon.

5. le programme appelant teste s'il y a eu erreur ou non et si oui fait afficher la page [valeurs-err.php]. Celle-ci est la suivante :

valeurs-err.php

1. <html>

2. <head>

3. <title>Erreur</title>

4. </head>

5. <body>

6. <h3>L'erreur suivante s'est produite</h3>

7. <font color="red">

8. <h4><?php echo $erreur ?></h4>

9. </font>

10. </body>

11. </html>

S'il n'y a pas eu erreur, le programme appelant dispose des valeurs dans le tableau $valeurs. On est donc ramené au problème précédent.

Voici deux exemples d'exécution :

1. avec erreur

1. sans erreur

III-I-4 - Génération dynamique de formulaire - 4

Dans l'exemple précédent, que se passerait-il si on changeait de SGBD ? Si on passait de MySQL à Oracle ou SQL Server par exemple ? Il faudrait réécrire la fonction getValeurs() qui fournit les valeurs. L'intérêt d'avoir rassemblé le code nécessaire à la récupération des valeurs à afficher dans la liste dans une fonction est que le code à modifier est bien ciblé et non pas éparpillé dans tout le programme. La fonction getValeurs() peut être réécrite de façon à ce qu'elle soit indépendante du SGBD utilisé. Il suffit qu'elle travaille avec le pilote ODBC du SGBD plutôt que directement avec le SGBD.

Il existe de nombreuses bases de données sur le marché. Afin d'uniformiser les accès aux bases de données sous MS Windows, Microsoft a développé une interface appelée ODBC (Open DataBase Connectivity). Cette couche cache les particularités de chaque base de données sous une interface standard. Il existe sous MS Windows de nombreux pilotes ODBC facilitant l'accès aux bases de données. Voici par exemple, une liste de pilotes ODBC installés sur une machine Windows :

Le SGBD MySQL a lui aussi un pilote ODBC. Une application s'appuyant sur des pilotes ODBC peut utiliser n'importe quelle bases de données ci-dessus sans ré-écriture.

Rendons notre base MySQL dbValeurs accessible au travers d'un pilote ODBC. La procédure ci-dessous est celle de Windows 2000. Pour les systèmes Win9x, la procédure est très analogue. On active le gestionnaire de ressources ODBC :

Menu Démarrer/Panneau de configuration/Outils d'administration/Sources de données ODBC

On utilise le bouton [Add] pour ajouter une nouvelle source de données ODBC :

On sélectionne le pilote ODBC MySQL, on fait [Terminer] puis on donne les caractéristiques de la source de données :

Windows DSN name

le nom donné à la source de données ODBC (odbc-valeurs)

MySQL host

le nom de la machine qui héberge le SGBD MySQL qui gère la source de données (localhost)

MySQL database name

le nom de la base MySQL qui est la source de données (dbValeurs)

User

un utilisateur ayant des droits d'accès suffisants sur la base MySQL à gérer (admDbValeurs)

Password

son mot de passe (mdpDbValeurs)

PHP est capable de travailler avec des pilotes ODBC. Le tableau suivant donne les fonctions utiles à connaître :

connexion au SGBD

$connexion=odbc_pconnect($dsn,$user,$pwd)
$connexion=odbc_connect($dsn,$user,$pwd)
$dsn : nom DSN (Data Source Name) de la machine sur laquelle s'exécute le SGBD
$user : nom d'un utilisateur connu du SGBD
$pwd : son mot de passe
$connexion : la connexion créée
odbc_pconnect crée une connexion persistante avec le SGBD. Une telle connexion n'est pas fermée à la fin du script. Elle reste ouverte. Ainsi lorsqu'il faudra ouvrir une nouvelle connexion avec le SGBD, PHP cherchera une connexion existante appartenant au même utilisateur. S'il la trouve, il l'utilise. Il y a là un gain de temps.
odbc_connect crée une connexion non persistante qu'on ferme donc lorsque le travail avec le SGBD est terminé.

requêtes SQL

$requêtePréparée=odbc_prepare($connexion,$requête)
$requête : une requête SQL (insert, delete, update, select…)
$connexion : la connexion au SGBD
Analyse la requête $requête et prépare son exécution. La requête ainsi "préparée" est référencée par le résultat $requêtePréparée. Préparer une requête pour son exécution n'est pas obligatoire mais améliore les performances puique l'analyse de la requête n'est faite qu'une fois. On demande ensuite l'exécution de la requête préparée. Si on demande l'exécution d'une requête non préparée de façon répétée, l'analyse de celle-ci est faite à chaque fois, ce qui est inutile. Une fois préparée la requête est exécutée par
$res=odbc_execute($requêtePréparée)
rend vrai ou faux selon que l'exécution de la requête réussit ou non

traitement des résultats d'un select

Le résultat d'un select est une table, donc un ensemble de lignes et de colonnes. Cette table est accessible via $requêtePréparée.
$res=odbc_fetch_row($requêtePréparée) lit une ligne de la table résultat du select. Rend vrai ou faux selon que l'exécution de la requête réussit ou non. Les éléments de la ligne récupérée sont disponibles via la fonction odbc_result :
$val=odbc_result($requêtePréparée,i) : colonne i de la ligne qui vien d'être lue
$val=odbc_result($requêtePréparée,"nomColonne") : colonne nomColonne de la ligne qui vient d'être lue
La fonction odbc_fetch_row peut être appelée de façon répétée. A chaque fois, elle lit une nouvelle ligne de la table des résultats. Lorsque la fin de la table est atteinte, la fonction rend la valeur false. Ainsi la table des résultats peut-elle être exploitée de la façon suivante :
while(odbc_fetch_row($résultats)){
// traite la ligne courante avec odbc_result
}//while

fermeture de la connexion

odbc_close($connexion)
$connexion : une connexion au SGBD MySQL

La fonction getValeurs() chargée de récupérer les valeurs dans la base de données ODBC est la suivante :

getValeurs

1. // ------------------------------------------------------------------------

2. function getValeurs(){

3. // récupère les valeurs dans une base MySQL

4. $user="admDbValeurs";

5. $pwd="mdpDbValeurs";

6. $db="dbValeurs";

7. $dsn="odbc-valeurs";

8. $table="tvaleurs";

9. $champ="valeur";

11. // ouverture d'une connexion persistante au serveur MySQL

12. // ou sinon d'une connexion normale

13. ($connexion=odbc_pconnect($dsn,$user,$pwd))

14. || ($connexion=odbc_connect($dsn,$user,$pwd));

15. if(! $connexion)

16. return array("1 - Base de données indisponible(".odbc_error()."). Veuillez recommencer ultérieurement.");

18. // obtention des valeurs

19. $selectValeurs=odbc_prepare($connexion,"select $champ from $table");

20. if(! odbc_execute($selectValeurs))

21. return array("2 - Base de données indisponible(".odbc_error()."). Veuillez recommencer ultérieurement.");

23. // les valeurs sont mises dans un tableau

24. $valeurs=array();

25. while(odbc_fetch_row($selectValeurs)){

26. $valeurs[]=odbc_result($selectValeurs,$champ);

27. }//while

29. // fermeture de la connexion (si elle est persistante, elle ne sera en fait pas fermée)

30. odbc_close($connexion);

32. // retour du résultat

33. return array("",$valeurs);

34. }//getValeurs

36. ?>

Si on exécute la nouvelle application sans activer la base de données odbc-valeurs, on obtient le résultat suivant :

On remarquera que le code d'erreur renvoyé par le pilote odbc (odbc_error()=S1000) est peu explicite. Si la base odbc-valeurs est rendue disponible, on obtient les mêmes résultats qu'auparavant.

En conclusion, on peut dire que cette solution est bonne pour la maintenance de l'application. En effet, si la base de données doit changer, l'application n'aura elle pas à changer. L'administrateur système créera simplement une nouvelle source de données ODBC pour la nouvelle base. Toujours en vue de la maintenance, il serait bon de mettre les paramètres d'accès à la base ($dsn, $user, $pwd) dans un fichier séparé que l'application chargerait au démarrage (include).

III-I-5 - Récupérer les valeurs d'un formulaire

Nous avons déjà à plusieurs reprises récupéré les valeurs d'un formulaire envoyées par un client web. L'exemple suivant montre un formulaire rassemblant les composants HTML les plus courants et s'applique à récupérer les paramètres envoyés par le navigateur client. Le formulaire est le suivant :

Il arrive prérepli. Ensuite l'utilisateur peut le modifier :

S'il appuie sur le bouton [Envoyer] le serveur lui renvoie la liste des valeurs du formulaire :

Le formulaire est une page HTML statique balises.html :

balises.html

1. <html>

3. <head>

4. <title>balises</title>

5. <script language="JavaScript">

6. function effacer(){

7. alert("Vous avez cliqué sur le bouton Effacer");

8. }//effacer

9. </script>

10. </head>

12. <body background="/images/standard.jpg">

13. <form method="POST" action="parameters.php">

14. <table border="0">

15. <tr>

16. <td>Etes-vous marié(e)</td>

17. <td>

18. <input type="radio" value="oui" name="R1">Oui

19. <input type="radio" name="R1" value="non" checked>Non

20. </td>

21. </tr>

22. <tr>

23. <td>Cases à cocher</td>

24. <td>

25. <input type="checkbox" name="C1" value="un">1

26. <input type="checkbox" name="C2" value="deux" checked>2

27. <input type="checkbox" name="C3" value="trois">3

28. </td>

29. </tr>

30. <tr>

31. <td>Champ de saisie</td>

32. <td>

33. <input type="text" name="txtSaisie" size="20" value="qqs mots">

34. </td>

35. </tr>

36. <tr>

37. <td>Mot de passe</td>

38. <td>

39. <input type="password" name="txtMdp" size="20" value="unMotDePasse">

40. </td>

41. </tr>

42. <tr>

43. <td>Boîte de saisie</td>

44. <td>

45. <textarea rows="2" name="areaSaisie" cols="20">

46. ligne1

47. ligne2

48. ligne3

49. </textarea>

50. </td>

51. </tr>

52. <tr>

53. <td>combo</td>

54. <td>

55. <select size="1" name="cmbValeurs">

56. <option>choix1</option>

57. <option selected>choix2</option>

58. <option>choix3</option>

59. </select>

60. </td>

61. </tr>

62. <tr>

63. <td>liste à choix simple</td>

64. <td>

65. <select size="3" name="lst1">

66. <option selected>liste1</option>

67. <option>liste2</option>

68. <option>liste3</option>

69. <option>liste4</option>

70. <option>liste5</option>

71. </select>

72. </td>

73. </tr>

74. <tr>

75. <td>liste à choix multiple</td>

76. <td>

77. <select size="3" name="lst2[]" multiple>

78. <option selected>liste1</option>

79. <option>liste2</option>

80. <option selected>liste3</option>

81. <option>liste4</option>

82. <option>liste5</option>

83. </select>

84. </td>

85. </tr>

86. <tr>

87. <td>bouton</td>

88. <td>

89. <input type="button" value="Effacer" name="cmdEffacer" onclick="effacer()">

90. </td>

91. </tr>

92. <tr>

93. <td>envoyer</td>

94. <td>

95. <input type="submit" value="Envoyer" name="cmdRenvoyer">

96. </td>

97. </tr>

98. <tr>

99. <td>rétablir</td>

100. <td>

101. <input type="reset" value="Rétablir" name="cmdRétablir">

102. </td>

103. </tr>

104. </table>

105. <input type="hidden" name="secret" value="uneValeur">

106. </form>

107. </body>

108. </html>

Le tableau ci-dessous rappelle le rôle des différentes balises de ce document et la valeur récupérée par PHP pour les différents types de composants d'un formulaire. La valeur d'un champ de nom HTML C peut être envoyée par un POST ou un GET. Dans le premier cas, elle sera récupérée dans la variable $_GET["C"] et dans le second cas dans la variable $_POST["C"]. Le tableau suivant suppose l'utilisation d'un POST.

Contrôle

balise HTML

valeur récupérée par PHP

formulaire

<form method="POST" >

champ de saisie

<input type="text" name="txtSaisie" size="20" value="qqs mots">

$_POST["txtSaisie"] : valeur contenue dans le champ txtSaisie du formulaire

champ de saisie cachée

<input type="password" name="txtMdp" size="20" value="unMotDePasse">

$_POST["txtmdp"] : valeur contenue dans le champ txtMdp du formulaire

champ de saisie multilignes

<textarea rows="2" name="areaSaisie" cols="20">
ligne1
ligne2
ligne3
</textarea>

$_POST["areaSaisie"] : lignes contenues dans le champ areaSaisie sous la forme d'une unique chaîne de caractères : "ligne1\r\nligne2\r\nligne3". Les lignes sont séparées entre elles par la séquence "\r\n".

boutons radio

<input type="radio" value="Oui" name="R1">Oui
<input type="radio" name="R1" value="non" checked>Non

$_POST["R1"] : valeur (=value) du bouton radio coché "oui" ou "non" selon les cas.

cases à cocher

<input type="checkbox" name="C1" value="un">1
<input type="checkbox" name="C2" value="deux" checked>2
<input type="checkbox" name="C3" value="trois">3

$_POST["C1"] : valeur (=value) de la case si elle est cochée sinon la variable n'existe pas. Ainsi si la case C1 a été cochée, $_POST["C1"] vaut "un", sinon $_POST["C1"] n'existe pas.

Combo

<select size="1" name="cmbValeurs">
<option>choix1</option>
<option selected>choix2</option>
<option>choix3</option>
</select>

$_POST["cmbValeurs"] : option sélectionnée dans la liste, par exemple "choix3".

liste à sélection unique

<select size="3" name="lst1">
<option selected>liste1</option>
<option>liste2</option>
<option>liste3</option>
<option>liste4</option>
<option>liste5</option>
</select>

$_POST["lst1"] : option sélectionnée dans la liste, par exemple "liste5".

liste à sélection multiple

<select size="3" name="lst2[]" multiple>
<option>liste1</option>
<option>liste2</option>
<option selected>liste3</option>
<option>liste4</option>
<option>liste5</option>
</select>

$_POST["lst2"] : tableau des options sélectionnées dans la liste, par exemple ["liste3,"liste5"]. On notera la syntaxe particulière de la balise HTML pour ce cas particulier : lst2[].

champ caché

<input type="hidden" name="secret" value="uneValeur">

$_POST["secret"] : valeur (=value) du champ, ici "uneValeur".

Dans notre exemple, les valeurs du formulaire sont adressées au programme parameters.php :

1. <form method="POST" action="parameters.php">

Le code de ce dernier est le suivant :

1. <?php

3. // configuration

4. ini_set("register_globals","off");

5. ini_set("display_errors","off");

7. // méthode d'appel

8. $méthode=$_SERVER["REQUEST_METHOD"];

10. // récupération des paramètres

11. // elle dépend de la méthode d'envoi de ceux-ci

12. if($méthode=="GET")

13. $param=$_GET;

14. else $param=$_POST;

15. $R1=$param["R1"];

16. $C1=$param["C1"];

17. $C2=$param["C2"];

18. $C3=$param["C3"];

19. $txtSaisie=$param["txtSaisie"];

20. $txtMdp=$param["txtMdp"];

21. $areaSaisie=implode("<br>",explode("\r\n",$param["areaSaisie"]));

22. $cmbValeurs=$param["cmbValeurs"];

23. $lst1=$param["lst1"];

24. $lst2=implode("<br>",$param["lst2"]);

25. $secret=$param["secret"];

27. // requête valide ?

28. $requêteValide=isset($R1) && (isset($C1) || isset($C2) || isset($C3))

29. && isset($txtSaisie) && isset($txtMdp) && isset($areaSaisie)

30. && isset($cmbValeurs) && isset($lst1) && isset($lst2)

31. && isset($secret);

33. // affichage page

34. if ($requêteValide)

35. include "parameters-p1.php";

36. else include "balises.html";

37. ?>

Détaillons quelques points de ce programme :

- l'application s'affranchit du mode de passage des valeurs du formulaire au serveur. Dans les deux cas possibles (GET et POST), le dictionnaire des valeurs passées est référencée par $param.

1. if($méthode=="GET")

2. $param=$_GET;

3. else $param=$_POST;

- à partir du contenu du champ areaSaisie "ligne1\r\nligne2\r\n…" on crée un tableau de chaînes par explode("\r\n",$param["areaSaisie"]). On a donc le tableau [ligne1,ligne2,…]. A partir de celui-ci on crée la chaîne "ligne1<br>ligne2<br>…" avec la fonction implode.

- la valeur de la liste à sélection multiple lst2 est un tableau, par exemple ["option3","option5"]. A partir de celui-ci, on crée une chaîne de caractères "option3<br>option5" avec la fonction implode.

- l'application vérifie que tous les paramètres ont été positionnés. Il faut se rappeler ici que n'importe quelle URL peut être appelée à la main ou par programme et qu'on n'a pas nécessairement les paramètres attendus. S'il manque des paramètres, la page balises.html est affichée sinon la page parameters-p1.php. Celle-ci affiche les valeurs récupérées et calculées dans parameters.php dans un tableau :

parameters.php

1. <html>

2. <head>

3. <title>Récupération des paramètres d'un formulaire</title>

4. </head>

5. <body>

6. <table border="1">

7. <tr>

8. <td>R1</td>

9. <td><?php echo $R1 ?></td>

10. </tr>

11. <tr>

12. <td>C1</td>

13. <td><?php echo $C1 ?></td>

14. </tr>

15. <tr>

16. <td>C2</td>

17. <td><?php echo $C2 ?></td>

18. </tr>

19. <tr>

20. <td>C3</td>

21. <td><?php echo $C3 ?></td>

22. </tr>

23. <tr>

24. <td>txtSaisie</td>

25. <td><?php echo $txtSaisie ?></td>

26. </tr>

27. <tr>

28. <td>txtMdp</td>

29. <td><?php echo $txtMdp ?></td>

30. </tr>

31. <tr>

32. <td>areaSaisie</td>

33. <td><?php echo $areaSaisie ?></td>

34. </tr>

35. <tr>

36. <td>cmbValeurs</td>

37. <td><?php echo $cmbValeurs ?></td>

38. </tr>

39. <tr>

40. <td>lst1</td>

41. <td><?php echo $lst1 ?></td>

42. </tr>

43. <tr>

44. <td>lst2</td>

45. <td><?php echo $lst2 ?></td>

46. </tr>

47. <tr>

48. <td>secret</td>

49. <td><?php echo $secret ?></td>

50. </tr>

51. </table>

52. </body>

53. </html>

III-J - Suivi de session

III-J-1 - Le problème

Une application web peut consister en plusieurs échanges de formulaires entre le serveur et le client. On a alors le fonctionnement suivant :

étape 1

- le client C1 ouvre une connexion avec le serveur et fait sa requête initiale.

- le serveur envoie le formulaire F1 au client C1 et ferme la connexion ouverte en 1.

étape 2

- le client C1 remplit le formulaire et le renvoie au serveur. Pour ce faire, le navigateur ouvre une nouvelle connexion avec le serveur.

- celui-ci traite les données du formulaire 1, calcule des informations I1 à partir de celles-ci, envoie un formulaire F2 au client C1 et ferme la connexion ouverte en 3.

étape 3

- le cycle des étapes 3 et 4 se répète dans des étapes 5 et 6. A l'issue de l'étape 6, le serveur aura reçu deux formulaires F1 et F2 et à partir de ceux-ci aura calculé des informations I1 et I2.

Le problème posé est : comment le serveur fait-il pour conserver les informations I1 et I2 liées au client C1 ? On appelle ce problème le suivi de la session du client C1. Pour comprendre sa racine, examinons le schéma d'une application serveur TCP-IP servant simultanément plusieurs clients :

Dans une application client-serveur TCP-IP classique :

1. le client crée une connexion avec le serveur

2. échange à travers celle-ci des données avec le serveur

3. la connexion est fermée par l'un des deux partenaires

Les deux points importants de ce mécanisme sont :

1. une connexion unique est créée pour chacun des clients

2. cette connexion est utilisée pendant toute la durée du dialogue du serveur avec son client

Ce qui permet au serveur de savoir à un moment donné avec quel client il travaille, c'est la connexion ou dit autrement le "tuyau" qui le relie à son client. Ce tuyau étant dédié à un client donné, tout ce qui arrive de ce tuyau vient de ce client et tout ce qui est envoyé dans ce tuyau arrive à ce même client.

Le mécanisme du client-serveur HTTP suit le schéma précédent avec cependant la particularité que le dialogue client-serveur est limité à un unique échange entre le client et le serveur :

- le client ouvre une connexion vers le serveur et fait sa demande

- le serveur fait sa réponse et ferme la connexion

Si au temps T1, un client C fait une demande au serveur, il obtient une connexion C1 qui va servir à l'échange unique demande-réponse. Si au temps T2, ce même client fait une seconde demande au serveur, il va obtenir une connexion C2 différente de la connexion C1. Pour le serveur, il n'y a alors aucune différence entre cette seconde demande de l'utilisateur C et sa demande initiale : dans les deux cas, le serveur considère le client comme un nouveau client. Pour qu'il y ait un lien entre les différentes connexions du client C au serveur, il faut que le client C se fasse "reconnaître" par le serveur comme un "habitué" et que le serveur récupère les informations qu'il a sur cet habitué.

Imaginons une administration qui fonctionnerait de la façon suivante :

1. Il y a une unique file d'attente

2. Il y a plusieurs guichets. Donc plusieurs clients peuvent être servis simultanément. Lorsqu'un guichet se libère, un client quitte la file d'attente pour être servi à ce guichet

3. Si c'est la première fois que le client se présente, la personne au guichet lui donne un jeton avec un numéro. Le client ne peut poser qu'une question. Lorsqu'il a sa réponse il doit quitter le guichet et passer à la fin de la file d'attente. Le guichetier note les informations de ce client dans un dossier portant son numéro.

4. Lorsqu'arrive à nouveau son tour, le client peut être servi par un autre guichetier que la fois précédente. Celui-ci lui demande son jeton et récupère le dossier ayant le numéro du jeton. De nouveau le client fait une demande, a une réponse et des informations sont rajoutées à son dossier.

5. et ainsi de suite… Au fil du temps, le client aura la réponse à toutes ses requêtes. Le suivi entre les différentes requêtes est réalisé grâce au jeton et au dossier associé à celui-ci.

Le mécanisme de suivi de session dans une application client-serveur web est analogue au fonctionnement précédent :

- lors de sa première demande, un client se voit donner un jeton par le serveur web

- il va présenter ce jeton à chacune de ses demandes ultérieures pour s'identifier

Le jeton peut revêtir différentes formes :

celui d'un champ caché dans un formulaire

- le client fait sa première demande (le serveur le reconnaît au fait que le client n'a pas de jeton)

- le serveur fait sa réponse (un formulaire) et met le jeton dans un champ caché de celui-ci. A ce moment, la connexion est fermée (le client quitte le guichet avec son jeton). Le serveur a pris soin éventuellement d'associer des informations à ce jeton.

- le client fait sa seconde demande en renvoyant le formulaire. Le serveur récupère dans celui-ci le jeton. Il peut alors traiter la seconde demande du client en ayant accès, grâce au jeton, aux informations calculées lors de la première demande. De nouvelles informations sont ajoutées au dossier lié au jeton, une seconde réponse est faite au client et la connexion est fermée pour la seconde fois. Le jeton a été mis de nouveau dans le formulaire de la réponse afin que l'utilisateur puisse le présenter lors de sa demande suivante.

- et ainsi de suite…

L'inconvénient principal de cette technique est que le jeton doit être mis dans un formulaire. Si la réponse du serveur n'est pas un formulaire, la méthode du champ caché n'est plus utilisable.

celui du cookie

- le client fait sa première demande (le serveur le reconnaît au fait que le client n'a pas de jeton)

- le serveur fait sa réponse en ajoutant un cookie dans les entêtes HTTP de celle-ci. Cela se fait à l'aide de la commande HTTP Set-Cookie :

Set-Cookie: param1=valeur1;param2=valeur2;….

où parami sont des noms de paramètres et valeursi leurs valeurs. Parmi les paramètres, il y aura le jeton. Bien souvent, il n'y a que le jeton dans le cookie, les autres informations étant consignées par le serveur dans le dossier lié au jeton. Le navigateur qui reçoit le cookie va le stocker dans un fichier sur le disque. Après la réponse du serveur, la connexion est fermée (le client quitte le guichet avec son jeton).

- le client fait sa seconde demande au serveur. A chaque fois qu'une demande à un serveur est faite, le navigateur regarde parmi tous les cookies qu'il a, s'il en a un provenant du serveur demandé. Si oui, il l'envoie au serveur toujours sous la forme d'une commande HTTP, la commande Cookie qui a une syntaxe analogue à celle de la commande Set-Cookie utilisée par le serveur :

Cookie: param1=valeur1;param2=valeur2;….

Parmi les entêtes HTTP envoyés par le navigateur, le serveur retrouvera le jeton lui permettant de reconnaître le client et de retrouver les informations qui lui sont liées.

C'est la forme la plus utilisée de jeton. Elle présente un inconvénient : un utilisateur peut configurer son navigateur afin qu'il n'accepte pas les cookies. Ce type d'utilisateur n'a alors pas accès aux applications web avec cookie.

réécriture d'URL

- le client fait sa première demande (le serveur le reconnaît au fait que le client n'a pas de jeton)

- le serveur envoie sa réponse. Celle-ci contient des liens que l'utilisateur doit utiliser pour continuer l'application. Dans l'URL de chacun de ces liens, le serveur rajoute le jeton sous la forme URL;jeton=valeur.

- lorsque l'utilisateur clique sur l'un des liens pour continuer l'application, le navigateur fait sa demande au serveur web en lui envoyant dans les entêtes HTTP l'URL URL;jeton=valeur demandée. Le serveur est alors capable de récupérer le jeton.

III-J-2 - L'API PHP pour le suivi de session

Nous présentons maintenant les principales méthodes utiles au suivi de session :

session_start()

démarre la session à laquelle appartient la requête en cours. Si celle-ci ne faisait pas encore partie d'une session, cette dernière est créée.

session_id()

identifiant de la session en cours

$_SESSION[$var]

dictionnaire mémorisant les données d'une session. Accessible en lecture et écriture

session_destroy()

supprime les données contenues dans la session en cours. Ces données restent disponibles pour l'échange client-serveur en cours mais seront indisponibles lors du prochain échange.

III-J-3 - Exemple 1

Nous présentons un exemple inspiré du livre "Programmation avec J2EE" aux éditions Wrox et distribué par Eyrolles. Cet exemple permet de voir comment fonctionne une session PHP. La page principale est la suivante :

On y trouve les éléments suivants :

1. l'ID de la session obtenue par la fonction session_id(). Cet ID généré par le navigateur est envoyé au client par un cookie que le navigateur renvoie lorsqu'il demande une URL de la même arborescence. C'est ce qui maintient la session.

2. un compteur qui est incrémenté au fil des demandes du navigateur et qui montre que la session est bien maintenue.

3. un lien permettant de supprimer les données attachées à la session en cours. Ceci est fait par la fonction session_destroy()

4. un lien pour recharger la page

Le code de l'application cycledevie.php est le suivant :

cycledevie.php

1. <?php

2. //cycledevie.php

4. // configuration

5. ini_set("register_globals","off");

6. ini_set("display_errors","off");

8. // on démarre une session

9. session_start();

11. // faut-il l'invalider ?

12. $action=$_GET["action"];

13. if($action=="invalider"){

14. // fin session

15. session_destroy();

16. }//if

18. // gestion du compteur

19. if(! isset($_SESSION["compteur"]))

20. // le compteur n'existe pas - on le crée

21. $_SESSION["compteur"]=0;

22. // le compteur existe - on l'incrémente

23. else $_SESSION["compteur"]++;

25. // on récupère l'ID de la session courante

26. $idSession=session_id();

28. // on récupère le compteur

29. $compteur=$_SESSION["compteur"];

31. // on passe la main à la page de visualisation

32. include "cycledevie-p1.php";

33. ?>

Notons les points suivants :

1. dès le début de l'aplication, une session est démarrée. Si le client a envoyé un jeton de session, c'est la session ayant cet identificateur qui est redémarrée et toutes les données qui lui sont liées sont placées dans le dictionnaire $_SESSION. Sinon, un nouveau jeton de session est créée.

2. si le client a envoyé un paramètre action ayant la valeur "invalider", les données de la session sont marquées comme étant "à supprimer" pour le prochain échange. Elles ne seront pas sauvegardées sur le serveur à la fin de l'échange contrairement à un échange normal.

3. on récupère un compteur lié à la session dans le dictionnaire $_SESSION, ainsi que l'ID de session (session_id()).

4. la page à envoyer au client est générée par le programme cycledevie-p1.php

La page cycledevie-p1.php affiche la page envoyée au client :

cycledevie-p1.php

1. <html>

2. <head>

3. <title>Gestion de sessions</title>

4. </head>

5. <body>

6. <h3>Cycle de vie d'une session PHP</h3>

7. <hr>

8. <br>ID session : <?php echo $idSession ?>

9. <br>compteur : <?php echo $compteur ?>

10. <br><a href="cycledevie.php?action=invalider">Invalider la session</a>

11. <br><a href="cycledevie.php">Recharger la page</a>

12. </body>

13. </html>

On remarquera l'URL attachée à chacun des deux liens :

1. cycledevie.php pour recharger la page

2. cycledevie.php?action=invalider pour invalider la session. Dans ce cas, un paramètre action=invalider est joint à l'URL. Il sera récupéré par le programme serveur cycledevie.php par l'instruction $action=$_GET["action"].

Rechargeons la page deux fois de suite :

Le compteur a bien été incrémenté. L'ID de session n'a pas changé. Invalidons maintenant la session :

On voit qu'on a perdu l'ID de la session mais que le compteur a lui été incrémenté une nouvelle fois. Rechargeons la page :

On voit qu'on recommence avec le même ID de session que précédemment. Le compteur lui repart à zéro. La fonction session_destroy() n'a donc pas un effet immédiat. Les données de la session courante sont supprimées simplement pour l'échange client-serveur qui suit celui où est faite la suppression. L'ID session n'a pas changé, ce qui tendrait à montrer que session_destroy() ne démarre pas une nouvelle session avec création d'un nouvel ID. Le cookie du jeton de session a été renvoyé par le navigateur client et PHP a récupéré la session de ce jeton, session qui n'avait plus de données.

Les tests précédents ont été réalisés avec le navigateur Netscape configuré pour utiliser les cookies. Configurons-le maintenant pour ne pas utiliser ceux-ci. Cela signifie qu'il ne stockera ni ne renverra les cookies envoyés par le serveur. On s'attend alors à ce que les sessions ne fonctionnent plus. Essayons avec un premier échange :

On a un ID de session, celui généré par session_start(). Rechargeons la page avec le lien :

De façon surprenante, les résultats ci-dessus montrent qu'on a une session en cours et que le compteur est correctement géré. Comment cela est-il possible puisque les cookies ayant été désactivés, il n'y a plus d'échange de jeton entre le serveur et le navigateur ? L'URL de la copie d'écran ci-dessus nous donne la réponse :

1. http://localhost/poly/sessions/1/cycledevie.php?PHPSESSID=587ce26f943a288d8f41212e30fed13c

C'est l'URL du lien "Recharger la page". Vérifions le code source de la page affichée par le navigateur :

1. <a href="cycledevie.php?action=invalider&PHPSESSID=587ce26f943a288d8f41212e30fed13c">Invalider la session</a>

2. <a href="cycledevie.php?PHPSESSID=587ce26f943a288d8f41212e30fed13c">Recharger la page</a>

Rappelons que le code initial des deux liens dans la page cycledevie-p1.php est celui-ci :

cycledevie-p1.php

1. <a href="cycledevie.php?action=invalider">Invalider la session</a>

2. <a href="cycledevie.php">Recharger la page</a>

L'interpréteur PHP a donc de lui-même réécrit les URL des deux liens en y ajoutant le jeton de session. Ainsi celui-ci est bien transmis par le navigateur lorsque les liens sont activés. Ce qui explique que même sans les cookies activés, la session reste correctement gérée.

III-J-4 - Exemple 3

Nous nous proposons d'écrire une application php qui serait cliente de l'application compteur précédente. Elle l'appellerait N fois de suite où N serait passé en paramètre. Notre but est de montrer un client web programmé et la façon de gérer le jeton de session. Notre point de départ sera un client web générique appelé de la façon suivante :

clientweb URL GET/HEAD

- URL : url demandée

- GET/HEAD : GET pour demander le code HTML de la page, HEAD pour se limiter aux seuls entêtes HTTP

Voici un exemple avec l'URL http://localhost/poly/sessions/2/cycledevie.php. Ce programme est celui déjà décrit avec une légère différence :

1. // on fixe le chemin du cookie

2. session_set_cookie_params(0,"/poly/sessions/2");

3. // on démarre une session

4. session_start();

La fonction session_set_cookie_params permet de fixer certains paramètres du cookie qui va contenir le jeton de session. Le 1er paramètre est la durée de vie du cookie. Une durée de vie nulle signifie que le cookie est supprimé par le navigateur qui l'a reçu lorsque celui-ci est fermé. Le second paramètre est le chemin des URL auxquelles le navigateur doit renvoyer le cookie. Dans l'exemple ci-dessus, si le navigateur a reçu le cookie de la machine localhost, il renverra le cookie à toute URL se trouvant dans l'arborescence http://localhost/poly/sessions/2/.

1. dos>e:\php43\php.exe clientweb.php http://localhost/poly/sessions/2/cycledevie.php GET

2. HTTP/1.1 200 OK

3. Date: Wed, 09 Oct 2002 13:58:16 GMT

4. Server: Apache/1.3.24 (Win32)

5. Set-Cookie: PHPSESSID=48d5aaa0e99850b17c33a6e22d38e5c4; path=/poly/sessions/2

6. Expires: Thu, 19 Nov 1981 08:52:00 GMT

7. Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0

8. Pragma: no-cache

9. Transfer-Encoding: chunked

10. Content-Type: text/html

12. <html>

13. <head>

14. <title>Gestion de sessions</title>

15. </head>

16. <body>

17. <h3>Cycle de vie d'une session PHP</h3>

18. <hr>

19. <br>ID session : 48d5aaa0e99850b17c33a6e22d38e5c4    <br>compteur : 0

20. <br><a href="cycledevie.php?action=invalider&PHPSESSID=48d5aaa0e99850b17c33a6e22d38e5c4">Invalid

21. er la session</a>

22. <br><a href="cycledevie.php?PHPSESSID=48d5aaa0e99850b17c33a6e22d38e5c4">Recharger la page</a>

23. </body>

24. </html>

Le programme clientweb affiche tout ce qu'il reçoit du serveur. On voit ci-dessus la commande HTTP Set-cookie avec laquelle le serveur envoie un cookie à son client. Ici le cookie contient deux informations :

- PHPSESSID qui est le jeton de la session

- path qui définit l'URL à laquelle appartient le cookie. path=/poly/sessions/2 indique au navigateur qu'il devra renvoyer le cookie au serveur à chaque fois qu'il demandera une URL commençant par /poly/sessions/2 de la machine qui lui a envoyé le cookie.

- un cookie peut définir également une durée de validité. Ici cette information est absente. Le cookie sera donc détruit à la fermeture du navigateur. Un cookie peut avoir une durée de validité de N jours par exemple. Tant qu'il est valide, le navigateur le renverra à chaque fois que l'une des URL de son domaine (Path) sera consultée. Prenons un site de vente en ligne de CD. Celui-ci peut suivre le cheminement de son client dans son catalogue et déterminer peu à peu ses préférences : la musique classique par exemple. Ces préférences peuvent être rangées dans un cookie ayant une durée de vie de 3 mois. Si ce même client revient au bout d'un mois sur le site, le navigateur renverra le cookie à l'application serveur. Celle-ci d'après les informations renfermées dans le cookie pourra alors adapter les pages générées aux préférences de son client.

Le code du client web suit.

1. <?php

3. // configuration

4. dl("php_curl.dll");    // bibliothèque CURL

6. // syntaxe : $0 URL GET

7. // il faut trois arguments

8. if($argc != 3){

9. // msg d'erreur

10. fputs(STDERR,"Syntaxe : $argv[0] URL GET/HEAD");

11. // arrêt

12. exit(1);

13. }//if

15. // le troisième argument doit être GET ou HEAD

16. $header=strtolower($argv[2]);

17. if($header!="get" && $header!="head"){

18. // msg d'erreur

19. fputs(STDERR,"Syntaxe : $argv[0] URL GET/HEAD");

20. // arrêt

21. exit(2);

22. }//if

24. // le premier argument est une URL

25. $URL=strtolower($argv[1]);

27. // préparation de la connexion

28. $connexion=curl_init($URL);

29. // paramétrage de la connexion

30. curl_setopt($connexion,CURLOPT_HEADER,1);

31. if($header=="head") curl_setopt($connexion,CURLOPT_NOBODY,1);

32. // exécution de la connexion

33. curl_exec($connexion);

34. // fermeture de la connexion

35. curl_close($connexion);

36. // fin

37. exit(0);

38. ?>

Le programme précédent utilise la bibliothèque CURL :

$connexion=curl_init($URL)

initialise un objet CURL avec l'URL à atteindre

curl_setopt ($connexion,option,valeur)

fixe la valeur de certaines options de la connexion. Citons les deux utilisées dans le programme :
CURLOPT_HEADER=1 : permet d'avoir les entêtes HTTP envoyées par le serveur
CURLOPT_NOBODY=1 : permet d'ignorer le document envoyé par le serveur derrière les entêtes HTTP

curl_exec($connexion)

réalise la connexion à $URL avec les options demandées. Affiche sur l'écran tout ce que le serveur envoie

curl_close($connexion)

ferme la connexion

Le programme précédent est plutôt simple. Cependant la bibliothèque CURL ne permet pas de manipuler finement la réponse du serveur, par exemple l'analyser ligne par ligne. Le programme suivant fait la même chose que le précédent mais avec les fonctions réseau de base de PHP. Il sera le point de départ pour l'écriture d'un client à notre application cycledevie.php.

1. <?php

3. // syntaxe : $0 URL GET/HEAD

4. // il faut trois arguments

5. if($argc != 3){

6. // msg d'erreur

7. fputs(STDERR,"Syntaxe : $argv[0] URL GET/HEAD");

8. // arrêt

9. exit(1);

10. }//if

12. // connexion et affichage du résultat

13. $résultats=getURL($argv[1],$argv[2]);

14. if(isset($résultats->erreur)){

15. // erreur

16. echo "L'erreur suivante s'est produite : $résultats->erreur\n";

17. }else{

18. // affichage réponse du serveur

19. echo $résultats->réponse;

20. }//if

22. // fin

23. exit(0);

25. //-----------------------------------------------------------------------

26. function getURL($URL,$header){

28. // se connecte à $URL

29. // fait un GET ou un HEAD selon la valeur de header

30. // la réponse du serveur forme le résultat de la fonction

32. // analyse de l'URL

33. $url=parse_url($URL);

34. // le protocole

35. if(strtolower($url["scheme"])!="http"){

36. $résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";

37. return $résultats;

38. }//if

39. // la machine

40. $hote=$url["host"];

41. if(! isset($hote)){

42. $résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";

43. return $résultats;

44. }//if

45. // le port

46. $port=$url["port"];

47. if(! isset($port)) $port=80;

48. // le chemin

49. $chemin=$url["path"];

50. // la requête

51. if(isset($url["query"])){

52. $résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";

53. return $résultats;

54. }//if

56. // analyse de $header

57. $header=strtoupper($header);

58. if($header!="GET" && $header!="HEAD"){

59. // msg d'erreur

60. $résultats->erreur="méthode [$header] doit être GET ou HEAD";

61. // arrêt

62. return $résultats;

63. }//if

65. // ouverture d'une connexion sur le port $port de $hote

66. $connexion=fsockopen($hote,$port,&$errno,&$erreur);

67. // retour si erreur

68. if(! $connexion){

69. $résultats->erreur="Echec de la connexion au site ($hote,$port) : $erreur";

70. return $résultats;

71. }//if

73. // $connexion représente un flux de communication bidirectionnel

74. // entre le client (ce programme) et le serveur web contacté

75. // ce canal est utilisé pour les échanges de commandes et d'informations

76. // le protocole de dialogue est HTTP

78. // le client envoie la commande get pour demander l'URL /

79. // syntaxe get URL HTTP/1.0

80. // les entêtes (headers) du protocole HTTP doivent se terminer par une ligne vide

81. fputs($connexion, "$header $chemin HTTP/1.0\n\n");

83. // le serveur va maintenant répondre sur le canal $connexion. Il va envoyer toutes

84. // ces données puis fermer le canal. Le client lit donc tout ce qui arrive de $connexion

85. // jusqu'à la fermeture du canal

86. $résultats->réponse="";

87. while($ligne=fgets($connexion,10000))

88. $résultats->réponse.=$ligne;

90. // le client ferme la connexion à son tour

91. fclose($connexion);

92. // retour

93. return $résultats;

94. }//getURL

96. ?>

Commentons quelques points de ce programme :

- le programme admet deux paramètres :la fonction getURL analyse l'URL $URL avec la fonction parse_url. L'instruction $url=parse_url($URL) va créer le tableau associatif $url avec les clés éventuelles suivantes :

- scheme : le protocole de l'URL (http, ftp, ..)

- host : la machine de l'URL

- port : le port de l'URL

- path : le chemin de l'URL

- querystring : les paramètres de l'URL

- une URL http dont on veut afficher le contenu à l'écran.

- une méthode GET ou HEAD à utiliser selon que l'on veut seulement les entêtes HTTP (HEAD) ou également le corps du document associé à l'URL (GET).

- les deux paramètres sont passés à la fonction getURL. Celle-ci rend un objet $résultats. Celui-ci a un champ erreur s'il y a une erreur, un champ réponse sinon. Le champ erreur sert à stocker un éventuel message d'erreur. Le champ réponse est la réponse du serveur web contacté.

- la fonction getURL analyse l'URL $URL avec la fonction parse_url. L'instruction $url=parse_url($URL) va créer le tableau associatif $url avec les clés éventuelles suivantes :

- scheme : le protocole de l'URL (http, ftp, ..)

- host : la machine de l'URL

- port : le port de l'URL

- path : le chemin de l'URL

- querystring : les paramètres de l'URL

Une url sera correcte si elle est de la forme http://machine[:port][/chemin].

- le paramètre $header est également vérifié

- une fois les paramètres vérifiés et corrects, on crée une connexion TCP sur la machine ($hote,$port), puis on envoie la commande HTTP GET ou HEAD selon le paramètre $header.

- on lit alors la réponse du serveur qu'on met dans $résultats->réponse.

L'exécution du programme donne les résultats suivants :

1. dos>"e:\php43\php.exe" geturl.php http://localhost/poly/sessions/2/cycledevie.php get

3. HTTP/1.1 200 OK

4. Date: Wed, 09 Oct 2002 14:56:55 GMT

5. Server: Apache/1.3.24 (Win32)

6. Set-Cookie: PHPSESSID=ea0d2673811ed069e7289d86933a4c0a; path=/poly/sessions/2

7. Expires: Thu, 19 Nov 1981 08:52:00 GMT

8. Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0

9. Pragma: no-cache

10. Connection: close

11. Content-Type: text/html

13. <html>

14. <head>

15. <title>Gestion de sessions</title>

16. </head>

17. <body>

18. <h3>Cycle de vie d'une session PHP</h3>

19. <hr>

20. <br>ID session : ea0d2673811ed069e7289d86933a4c0a    <br>compteur : 0

21. <br><a href="cycledevie.php?action=invalider&PHPSESSID=ea0d2673811ed069e7289d86933a4c0a">Invalid

22. er la session</a>

23. <br><a href="cycledevie.php?PHPSESSID=ea0d2673811ed069e7289d86933a4c0a">Recharger la page</a>

24. </body>

25. </html>

Le lecteur attentif aura noté que la réponse du serveur n'est pas la même selon le programme client utilisé. Dans le premier cas, le serveur avait envoyé un entête HTTP : Transfer-Encoding: chunked, entête qui n'a pas été envoyé dans le second cas. Cela vient du fait que le second client a envoyé l'entête HTTP : get URL HTTP/1.0 qui demande une URL et indique qu'il travaille avec le protocole HTTP version 1.0 ce qui oblige le serveur à lui répondre avec ce même protocole. Or l'entête HTTP Transfer-Encoding: chunked appartient au protocole HTTP version 1.1. Aussi le serveur ne l'a-t-il pas utilisé dans sa réponse. Ceci nous montre que le premier client a lui fait sa requête en indiquant qu'il travaillait avec le ptotocole HTTP version 1.1.

Nous créons maintenant le programme clientCompteur appelé de la façon suivante :

clientCompteur URL N [JSESSIONID]

- URL : url de l'application cycledevie

- N : nombre d'appels à faire à cette application

- PHPSESSID : paramètre facultatif - jeton d'une session

Le but du programme est d'appeler N fois l'application cycledevie.php en gérant le cookie de session et en affichant à chaque fois la valeur du compteur renvoyée par le serveur. A la fin des N appels, la valeur de celui-ci doit être N-1. Voici un premier exemple d'exécution :

1. dos>"e:\php43\php.exe" clientCompteur2.php http://localhost/poly/sessions/2/cycledevie.php 3

2. --> GET /poly/sessions/2/cycledevie.php HTTP/1.1

3. --> Host: localhost:80

4. --> Connection: close

5. -->

6. <-- HTTP/1.1 200 OK

7. <-- Date: Thu, 10 Oct 2002 06:27:48 GMT

8. <-- Server: Apache/1.3.24 (Win32)

9. <-- Set-Cookie: PHPSESSID=2425e00d1d65c2bdcbafc1ce6244f7ea; path=/poly/sessions/2

10. <-- Expires: Thu, 19 Nov 1981 08:52:00 GMT

11. <-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0

12. <-- Pragma: no-cache

13. <-- Connection: close

14. <-- Transfer-Encoding: chunked

15. <-- Content-Type: text/html

16. <--

18. [Le compteur est égal à 0]

20. --> GET /poly/sessions/2/cycledevie.php HTTP/1.1

21. --> Host: localhost:80

22. --> Connection: close

23. --> Cookie: PHPSESSID=2425e00d1d65c2bdcbafc1ce6244f7ea

24. -->

25. <-- HTTP/1.1 200 OK

26. <-- Date: Thu, 10 Oct 2002 06:27:48 GMT

27. <-- Server: Apache/1.3.24 (Win32)

28. <-- Expires: Thu, 19 Nov 1981 08:52:00 GMT

29. <-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0

30. <-- Pragma: no-cache

31. <-- Connection: close

32. <-- Transfer-Encoding: chunked

33. <-- Content-Type: text/html

34. <--

36. [Le compteur est égal à 1]

38. --> GET /poly/sessions/2/cycledevie.php HTTP/1.1

39. --> Host: localhost:80

40. --> Connection: close

41. --> Cookie: PHPSESSID=2425e00d1d65c2bdcbafc1ce6244f7ea

42. -->

43. <-- HTTP/1.1 200 OK

44. <-- Date: Thu, 10 Oct 2002 06:27:48 GMT

45. <-- Server: Apache/1.3.24 (Win32)

46. <-- Expires: Thu, 19 Nov 1981 08:52:00 GMT

47. <-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0

48. <-- Pragma: no-cache

49. <-- Connection: close

50. <-- Transfer-Encoding: chunked

51. <-- Content-Type: text/html

52. <--

54. [Le compteur est égal à 2]

Le programme affiche :

- les entêtes HTTP qu'il envoie au serveur sous la forme --> entêteEnvoyé

- les entêtes HTTP qu'il reçoit sous la forme <-- entêteReçu

- la valeur du compteur après chaque appel

On voit que lors du premier appel :

- le client n'envoie pas de cookie

- le serveur en envoie un (Set-Cookie:)

Pour les appels suivants :

- le client renvoie systématiquement le cookie qu'il a reçu du serveur lors du 1er appel. C'est ce qui va permettre au serveur de le reconnaître et d'incrémenter son compteur.

- le serveur lui, ne renvoie plus de cookie

Nous relançons le programme précédent en passant le jeton ci-dessus comme troisième paramètre :

1. dos>"e:\php43\php.exe" clientCompteur2.php http://localhost/poly/sessions/2/cycledevie.php 1 2425e00d1d65c2bdcbafc1ce6244f7ea

3. --> GET /poly/sessions/2/cycledevie.php HTTP/1.1

4. --> Host: localhost:80

5. --> Connection: close

6. --> Cookie: PHPSESSID=2425e00d1d65c2bdcbafc1ce6244f7ea

7. -->

8. <-- HTTP/1.1 200 OK

9. <-- Date: Thu, 10 Oct 2002 06:32:03 GMT

10. <-- Server: Apache/1.3.24 (Win32)

11. <-- Expires: Thu, 19 Nov 1981 08:52:00 GMT

12. <-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0

13. <-- Pragma: no-cache

14. <-- Connection: close

15. <-- Transfer-Encoding: chunked

16. <-- Content-Type: text/html

17. <--

19. [Le compteur est égal à 3]

On voit ici que dès le 1er appel du client, le serveur reçoit un cookie de session valide. On pointe peut-être là un trou de sécurité potentiel. Si je suis capable d'intercepter sur le réseau un jeton de session, je suis alors capable de me faire passer pour celui qui avait initié celle-ci. Dans notre exemple, le premier appel (sans jeton de session) représente celui qui initie la session (peut-être avec un login et mot de passe qui vont lui donner le droit d'avoir un jeton) et le second appel (avec jeton de session) représente celui qui a "piraté" le jeton de session du premier appel. Si l'opération en cours est une opération bancaire cela peut devenir ennuyeux…

Le code du client est le suivant :

1. <?php

3. // syntaxe : $0 URL N [PHPSESSID]

4. // il faut trois arguments

5. if($argc!=3 && $argc!=4){

6. // msg d'erreur

7. fputs(STDERR,"Syntaxe : $argv[0] URL N [PHPSESSID]");

8. // arrêt

9. exit(1);

10. }//if

12. // récupération des paramètres

13. $URL=$argv[1];

14. $N=$argv[2];

15. $PHPSESSID=$argv[3];

17. // connexion et affichage du résultat

18. $résultats=getURL($URL,$N,$PHPSESSID);

19. if(isset($résultats->erreur)){

20. // erreur

21. echo "L'erreur suivante s'est produite : $résultats->erreur\n";

22. }

24. // fin

25. exit(0);

27. //-----------------------------------------------------------------------

28. function getURL($URL,$N,$PHPSESSID){

30. // se connecte à URL

31. // fait un GET ou un HEAD selon la valeur de header

32. // la réponse du serveur forme le résultat de la fonction

34. // analyse de l'URL

35. $url=parse_url($URL);

36. // le protocole

37. if(strtolower($url["scheme"])!="http"){

38. $résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";

39. return $résultats;

40. }//if

41. // la machine

42. $hote=$url["host"];

43. if(! isset($hote)){

44. $résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";

45. return $résultats;

46. }//if

47. // le port

48. $port=$url["port"];

49. if(! isset($port)) $port=80;

50. // le chemin

51. $chemin=$url["path"];

52. // la requête

53. if(isset($url["query"])){

54. $résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";

55. return $résultats;

56. }//if

58. // vérification de $N

59. if (! preg_match("/^\d+$/",$N)){

60. // erreur

61. $résultats->erreur="nombre [$N] erroné";

62. // fin

63. return $résultats;

64. }//if

66. // on fait les $N appels à $URL

67. for($i=0;$i<$N;$i++){

68. // ouverture d'une connexion sur le port $port de $hote

69. $connexion=fsockopen($hote,$port,&$errno,&$erreur);

70. // retour si erreur

71. if(! $connexion){

72. $résultats->erreur="Echec de la connexion au site ($hote,$port) : $erreur";

73. return $résultats;

74. }//if

76. // $connexion représente un flux de communication bidirectionnel

77. // entre le client (ce programme) et le serveur web contacté

78. // ce canal est utilisé pour les échanges de commandes et d'informations

79. // le protocole de dialogue est HTTP

81. // le client envoie les entêtes HTTP

82. // get URL HTTP/1.1

83. envoie($connexion, "GET $chemin HTTP/1.1\n");

84. // host: hote:port

85. envoie($connexion, "Host: $hote:$port\n");

86. // Connection: close

87. envoie($connexion, "Connection: close\n");

88. // Cookie: $PHPSESSID

89. if($PHPSESSID) envoie($connexion, "Cookie: PHPSESSID=$PHPSESSID\n");

90. // ligne vide

91. envoie($connexion,"\n");

93. // le serveur va maintenant répondre sur le canal $connexion. Il va envoyer toutes

94. // ses données puis fermer le canal.

95. // Le client commence par lire les entêtes HTTP terminés par une ligne vide

96. $CHUNKED=0;

97. while(($ligne=fgets($connexion,10000)) && (($ligne=rtrim($ligne))!="")){

98. // écho ligne

99. echo "<-- $ligne\n";

100. // recherche du jeton si on ne l'a pas encore trouvé

101. if(! $PHPSESSID){

102. // recherche de la ligne set-cookie

103. if(preg_match("/^Set-Cookie: PHPSESSID=(.*?);/i",$ligne,$champs)){

104. // on a trouvé le jeton - on le mémorise

105. $PHPSESSID=$champs[1];

106. }//if

107. }//if

108. // recherche du mode de transfert du document

109. if(! $CHUNKED){

110. // recherche de la ligne Transfer-Encoding: chunked

111. if(preg_match("/^Transfer-Encoding: chunked/i",$ligne,$champs)){

112. // transfert par morceaux

113. $CHUNKED=1;

114. }//if

115. }//if

116. }//ligne suivante

118. // écho ligne

119. echo "<-- $ligne\n";

121. // la lecture du document dépend de la façon dont il a été envoyé

122. if($CHUNKED) $document=getChunkedDoc($connexion);

123. else $document=getDoc($connexion);

125. // recherche du compteur dans le document

126. if(preg_match("/<br>compteur : (\d+)/i",$document,$champs)){

127. // on a trouvé le compteur - on l'affiche

128. echo "\n[Le compteur est égal à $champs[1]]\n\n";

129. }//if

131. // le client ferme la connexion

132. fclose($connexion);

133. }//for i

134. }//getURL

136. //--------------------------

137. function getDoc($connexion){

138. // lecture du document sur $connexion

139. $doc="";

140. while($ligne=fread($connexion,10000))

141. $doc.=$ligne;

142. // fin

143. return $doc;

144. }//getDoc

146. //--------------------------

147. function getChunkedDoc($connexion){

148. // lecture du document sur $connexion

149. // ce document est envoyé par morceaux sous la forme

150. // nombre de caractères du morceau en hexadécimal

151. // suite du morceau

152. // ligne vide

154. // on lit la taille du morceau dans la 1ère ligne

155. $taille=hexdec(rtrim(fgets($connexion,10000)));

156. // on lit le document qui suit

157. $doc="";

158. while($taille!=0){

159. // lecture morceau de $taille caractères

160. $doc.=fread($connexion,$taille);

161. // ligne vide

162. fgets($connexion,10000);

163. // morceau suivant

164. // on lit la taille du morceau

165. $taille=hexdec(rtrim(fgets($connexion,10000)));

166. }//while

167. // c'est fini

168. return $doc;

169. }// getChunkedDoc

171. //--------------------------

172. function envoie($flux,$msg){

173. // envoie $msg sur $flux

174. fwrite($flux,$msg);

175. // echo écran

176. echo "--> $msg";

177. }//envoie

178. ?>

Décortiquons les points importants de ce programme :

- on doit faire N échanges client-serveur. C'est pourquoi ceux-ci sont dans une boucle

1. for($i=0;$i<$N;$i++){

- à chaque échange, le client ouvre une connexion TCP-IP avec le serveur. Une fois celle-ci obtenue, il envoie au serveur les entêtes HTTP de sa requête :

1. // le client envoie les entêtes HTTP

2. // get URL HTTP/1.1

3. envoie($connexion, "GET $chemin HTTP/1.1\n");

4. // host: hote:port

5. envoie($connexion, "Host: $hote:$port\n");

6. // Connection: close

7. envoie($connexion, "Connection: close\n");

8. // Cookie: $PHPSESSID

9. if($PHPSESSID) envoie($connexion, "Cookie: PHPSESSID=$PHPSESSID\n");

10. // ligne vide

11. envoie($connexion,"\n");

Si le jeton PHPSESSID est disponible, il est envoyé sous la forme d'un cookie, sinon il ne l'est pas. On notera que le client a indiqué qu'il travaillait avec le protocole HTTP/1.1. Ce qui explique qu'ultérieurement, le serveur va lui envoyer l'entête HTTP : Transfer-Encoding: chunked qui appartient au protocole HTTP/1.1 mais pas au protocole HTTP/1.0.

- Une fois sa requête envoyée, le client attend la réponse du serveur. Il commence par exploiter les entêtes HTTP de cette réponse. Il recherche dans celle-ci deux lignes :

[ligne vide]

Si la ligne Transfer-Encoding: chunked n'est pas présente, le document est envoyé en une seule fois derrière la ligne vide des entêtes HTTP. Donc, selon la présence ou non de cette ligne, le mode de réception du document va différer. Le code d'exploitation des entêtes HTTP est le suivant :

[document]

[nombre de caractères du document en hexadécimal]

1. // Le client commence par lire les entêtes HTTP terminés par une ligne vide

2. $CHUNKED=0;

3. while(($ligne=fgets($connexion,10000)) && (($ligne=rtrim($ligne))!="")){

4. // écho ligne

5. echo "<-- $ligne\n";

6. // recherche du jeton si on ne l'a pas encore trouvé

7. if(! $PHPSESSID){

8. // recherche de la ligne set-cookie

9. if(preg_match("/^Set-Cookie: PHPSESSID=(.*?);/i",$ligne,$champs)){

10. // on a trouvé le jeton - on le mémorise

11. $PHPSESSID=$champs[1];

12. }//if

13. }//if

14. // recherche du mode de transfert du document

15. if(! $CHUNKED){

16. // recherche de la ligne Transfer-Encoding: chunked

17. if(preg_match("/^Transfer-Encoding: chunked/i",$ligne,$champs)){

18. // transfert par morceaux

19. $CHUNKED=1;

20. }//if

21. }//if

22. }//ligne suivante

La ligne Cookie: est l'entête HTTP qui contient le jeton de session PHPSESSID. Le client doit le récupérer pour le renvoyer au serveur lors de l'échange suivant. La ligne Transfer-Encoding: chunked si elle est présente indique que le serveur va envoyer un document par morceaux. Chaque morceau est alors envoyé au client sous la forme suivante :

Cookie :

Transfer-Encoding: chunked

- lorsque le jeton aura été trouvé une première fois, il ne sera plus cherché lors des appels suivants au serveur. Lorsque les entêtes HTTP de la réponse ont été traités, on passe au document qui suit les entêtes HTTP. Celui-ci est lu différemment selon son mode de transfert :

1. // la lecture du document dépend de la façon dont il a été envoyé

2. if($CHUNKED) $document=getChunkedDoc($connexion);

3. else $document=getDoc($connexion);

- Dans le document $document reçu on cherche la ligne qui donne la valeur du compteur. Cette recherche est faite là aussi avec une expression régulière :

1. // recherche du compteur dans le document

2. if(preg_match("/<br>compteur : (\d+)/i",$document,$champs)){

3. // on a trouvé le compteur - on l'affiche

4. echo "\n[Le compteur est égal à $champs[1]]\n\n";

5. }//if

- Dans le cas où le serveur envoie le document en une fois, la réception de celui-ci est simple :

1. //--------------------------

2. function getDoc($connexion){

3. // lecture du document sur $connexion

4. $doc="";

5. while($ligne=fgets($connexion,10000))

6. $doc.=$ligne;

7. // fin

8. return $doc;

9. }//getDoc

- Dans le cas où le serveur envoie le document en plusieurs morceaux, la lecture de celui-ci est plus complexe :

1. function getChunkedDoc($connexion){

2. // lecture du document sur $connexion

3. // ce document est envoyé par morceaux sous la forme

4. // nombre de caractères du morceau en hexadécimal

5. // suite du morceau

7. // on lit la taille du morceau dans la 1ère ligne

8. $taille=hexdec(rtrim(fgets($connexion,10000)));

9. // on lit le document qui suit

10. $doc="";

11. while($taille!=0){

12. // lecture morceau de $taille caractères

13. $doc.=fread($connexion,$taille);

14. // ligne vide

15. fgets($connexion,10000);

16. // morceau suivant

17. // on lit la taille du morceau

18. $taille=hexdec(rtrim(fgets($connexion,10000)));

19. }//while

20. // c'est fini

21. return $doc;

22. }// getChunkedDoc

Rappelons qu'un morceau de document est envoyé sous la forme

[taille]

[partie de document]

[ligne vide]

On commence donc par lire la taille du document. Celle-ci connue, on demande à la fonction fread de lire $taille caractères dans le flux $connexion puis la ligne vide qui suit. On répète cela jusqu'à ce que le serveur envoie l'information qu'il va envoyer un document de taille 0.

III-J-5 - Exemple 4

Dans l'exemple précédent, le client web renvoie le jeton sous la forme d'un cookie. Nous avons vu qu'il pouvait aussi le renvoyer au sein même de l'URL demandée sous la forme URL;PHPSESSID=xxx. Vérifions-le. Le programme clientCompteur.php est transformé en clientCompteur2.php et modifié de la façon suivante :

clientCompteur2.php

1. ....

2. // le client envoie les entêtes HTTP

3. // get URL HTTP/1.1

4. if($PHPSESSID)

5. envoie($connexion, "GET $chemin?PHPSESSID=$PHPSESSID HTTP/1.1\n");

6. else envoie($connexion, "GET $chemin HTTP/1.1\n");

7. // host: hote:port

8. envoie($connexion, "Host: $hote:$port\n");

9. // Connection: close

10. envoie($connexion, "Connection: close\n");

11. // ligne vide

12. envoie($connexion,"\n");

13. ....

Le client demande donc l'URL du compteur par GET URL;PHPSESSID=xx HTTP/1.1 et n'envoie plus de cookie. C'est la seule modification. Voici les résultats d'un premier appel :

1. dos>"e:\php43\php.exe" clientCompteur2.php http://localhost/poly/sessions/2/cycledevie.php 2

3. --> GET /poly/sessions/2/cycledevie.php HTTP/1.1

4. --> Host: localhost:80

5. --> Connection: close

6. -->

7. <-- HTTP/1.1 200 OK

8. <-- Date: Thu, 10 Oct 2002 07:21:19 GMT

9. <-- Server: Apache/1.3.24 (Win32)

10. <-- Set-Cookie: PHPSESSID=573212ba82303d7903caf8944ee7a86f; path=/poly/sessions/2

11. <-- Expires: Thu, 19 Nov 1981 08:52:00 GMT

12. <-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0

13. <-- Pragma: no-cache

14. <-- Connection: close

15. <-- Transfer-Encoding: chunked

16. <-- Content-Type: text/html

17. <--

19. [Le compteur est égal à 0]

21. --> GET /poly/sessions/2/cycledevie.php?PHPSESSID=573212ba82303d7903caf8944ee7a86f HTTP/1.1

22. --> Host: localhost:80

23. --> Connection: close

24. -->

25. <-- HTTP/1.1 200 OK

26. <-- Date: Thu, 10 Oct 2002 07:21:19 GMT

27. <-- Server: Apache/1.3.24 (Win32)

28. <-- Expires: Thu, 19 Nov 1981 08:52:00 GMT

29. <-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0

30. <-- Pragma: no-cache

31. <-- Connection: close

32. <-- Transfer-Encoding: chunked

33. <-- Content-Type: text/html

34. <--

36. [Le compteur est égal à 1]

Lors du premier appel le client demande l'URL sans jeton de session. Le serveur lui répond en lui envoyant le jeton. Le client réinterroge alors la même URL en adjoignant le jeton reçu à celle-ci. On voit que le compteur est bien incrémenté preuve que le serveur a bien reconnu qu'il s'agissait de la même session.

III-J-6 - Exemple 5

Cet exemple montre une application composée de trois pages qu'on appellera page1, page2 et page3. L'utilisateur doit les obtenir dans cet ordre :

- page1 est un formulaire demandant une information : un nom

- page2 est un formulaire obtenu en réponse à l'envoi du formulaire de page1. Il demande une seconde information : un age

- page3 est un document HTML qui affiche le nom obtenu par page1 et l'âge obtenu par page2.

Il y a trois échanges client-serveur :

- au premier échange le formulaire page1 est demandé par le client et envoyé par le serveur

- au second échange le client envoie le formulaire page1 (nom) au serveur. Il reçoit en retour le formulaire page2 ou de nouveau le formulaire page1 s'il était erroné.

- au troisième échange le client envoie le formulaire page2 (age) au serveur. Il reçoit en retour le formulaire page3 ou de nouveau le formulaire page2 s'il était erroné. Le document page3 affiche le nom et l'âge. Le nom a été obtenu par le serveur au second échange et "oublié" depuis. On utilise une session pour enregistrer le nom à l'échange 2 afin qu'il soit disponible lors de l'échange 3.

La page page1 obtenue au premier échange est la suivante :

On remplit le champ du nom :

On utilise le bouton [Suite] et on obtient alors la page page2 suivante :

On remplit le champ de l'âge :

On utilise le bouton [Suite] et on obtient alors la page page3 suivante :

Lorsqu'on soumet la page page1 au serveur, celui-ci peut la renvoyer avec un code d'erreur si le nom est vide :

Lorsqu'on soumet la page page2 au serveur, celui-ci peut la renvoyer avec un code d'erreur si l'âge est invalide :

L'application est composée de six programmes :

etape1.php

fait appel à page1.php

page1.php

affiche page1. Le formulaire de page1 est traité par etape2.php.

etape2.php

traite les valeurs du formulaire de page1. S'il y a des erreurs, page1 est réaffichée par page1.php sinon c'est page2 qui est affichée par page2.php.

page2.php

affiche page2. Le formulaire de page2 est traité par etape3.php.

etape3.php

traite les valeurs du formulaire de page2. S'il y a des erreurs, page2 est réaffichée par page2.php sinon c'est page3 qui est affichée par page3.php.

page3.php

affiche page3.

L'étape 1 de l'application est traitée par le programme etape1.php suivant :

etape1.php

1. <?php

2. // etape1.php

4. // configuration

5. ini_set("register_globals","off");

6. ini_set("display_errors","off");

8. // démarrage session

9. session_start();

10. $_SESSION["session"]="";    //raz variable session

12. // préparation page1

13. $requête->nom="";

14. $requête->erreurs=array();

15. // affichage page1

16. include "page1.php";

17. // fin

18. exit(0);

19. ?>

Notons les points suivants :

- l'application a besoin d'un suivi de session. Aussi chaque étape de celle-ci démarre-t-elle une session.

- les informations de session à conserver le seront dans un objet $session.

- les informations nécessaires à l'affichage des différentes (trois) pages de l'application seront mises dans un objet $requête.

Le programme page1.php affiche les informations contenues dans $requête :

page1.php

1. <? // page1.php ?>

3. <html>

4. <head>

5. <title>page 1</title>

6. </head>

7. <body>

8. <h3>Page 1/3</h3>

9. <form name="frmNom" method="POST" action="etape2.php">

10. <table>

11. <tr>

12. <td>Votre nom</td>

13. <td><input type="text" name="nom" value="<? echo $requête->nom ?>"></td>

14. </tr>

15. </table>

16. <input type="submit" value="Suite">

17. </form>

18. <? // erreurs ?

19. if (count($requête->erreurs)!=0){

20. ?>

21. <hr>

22. <font color="red">

23. Les erreurs suivantes se sont produites

24. <ul>

25. <? for($i=0;$i<count($requête->erreurs);$i++){ ?>

26. <li><? echo $requête->erreurs[$i] ?>

27. <? }//for ?>

28. </ul>

29. <? }//if ?>

30. </body>

31. </html>

- la page reçoit un objet $requête contenant deux champs : nom et erreurs. Elle affiche la valeur de ces deux champs.

- elle présente de plus un formulaire. Les valeurs de celui-ci (nom) sont envoyées par la méthode POST au programme etape2.php :

1. <form name="frmNom" method="POST" action="etape2.php">

L'application etape2.php est chargée de traiter les valeurs du formulaire de page1 et de réafficher page1 s'il y a des erreurs (nom erroné) sinon d'afficher page2 pour avoir l'âge.

1. <?php

2. // etape2.php

4. // configuration

5. ini_set("register_globals","off");

6. ini_set("display_errors","off");

8. // démarrage session

9. session_start();

11. // normalement, on doit avoir un paramètre nom

12. // enregistré dans la requête

13. $requête->nom=$_POST["nom"];

15. // si pas de paramètres, on envoie page1 sans erreurs

16. if (! isset($requête->nom)){

17. $requête->nom="";

18. $requête->erreurs=array();

19. include "page1.php";

20. exit(0);

21. }//if

23. // si le paramètre nom est bien là - on vérifie sa validité

24. $page=calculerPage($requête);

26. // y-a-t-il eu des erreurs ?

27. if(count($page->erreurs)!=0){

28. // page 1 avec erreurs

29. $requête->erreurs=$page->erreurs;

30. include "page1.php";

31. exit(0);

32. }//if

34. // pas d'erreur - on mémorise le nom dans la session

35. unset($session);

36. $session->nom=$requête->nom;

37. $_SESSION["session"]=$session;

39. // affichage page 2

40. $requête->age="";

41. $requête->erreurs=array();

42. include "page2.php";

44. // fin

45. exit(0);

47. // ---------calculerPage

48. function calculerPage($requête){

49. // vérifie la validité de la requête $requête

50. // rend un tableau d'erreurs dans $page->erreurs

52. // au départ, pas d'erreurs

53. $page->erreurs=array();

54. // le nom ne doit pas être vide

55. if (preg_match("/^\s*$/",$requête->nom)){

56. $page->erreurs[]="Vous n'avez pas indiqué de nom";

57. }

58. // retour page

59. return $page;

60. }//calculerPage

61. ?>

- etape2 commence par vérifier qu'il a bien le paramètre nom attendu. Si ce n'est pas le cas, il fait réafficher une page1 vide. Ce cas est possible si etape2 est appelée directement par un client qui ne lui passe pas de paramètres.

- si le paramètre nom est présent, on vérifie sa validité. Cela se fait par l'intermédiaire d'une procédure appelée calculerPage dont le rôle est de produire un objet $page avec un champ erreurs qui est un tableau d'erreurs. Une seule erreur est possible mais on a voulu montrer qu'on pouvait gérer une liste d'erreurs.

- s'il y a des erreurs, la page page1 est réaffichée accompagnée de la liste des erreurs.

- s'il n'y a pas d'erreurs, le nom est mémorisé dans l'objet $session qui mémorise les données liées à la session courante. Puis la page page2 est affichée.

Le programme page2.php affiche page2 :

1. <? // page2.php ?>

3. <html>

4. <head>

5. <title>page 2</title>

6. </head>

7. <body>

8. <h3>Page 2/3</h3>

9. <form name="frmAge" method="POST" action="etape3.php">

10. <table>

11. <tr>

12. <td>Nom</td>

13. <td><font color="green"><? echo $requête->nom ?></font></td>

14. </tr>

15. <tr>

16. <td>Votre âge</td>

17. <td><input type="text" name="age" size="3" value="<? echo $requête->age ?>"></td>

18. </tr>

19. </table>

20. <input type="submit" value="Suite">

21. </form>

22. <? // erreurs ?

23. if (count($requête->erreurs)!=0){

24. ?>

25. <hr>

26. <font color="red">

27. Les erreurs suivantes se sont produites

28. <ul>

29. <? for($i=0;$i<count($requête->erreurs);$i++){

30. echo "<li>".$requête->erreurs[$i];

31. }//for

32. ?>

33. </ul>

34. </font>

35. <? } ?>

36. </body>

37. </html>

Le principe de cette page est très analogue à celui de page2.php. Elle affiche le contenu d'un objet $requête contenant les champs nom, age et erreurs. Elle affiche un formulaire dont les valeurs seront traitées par etape3.php.

1. <form name="frmAge" method="POST" action="etape3.php">

Le programme etape3.php traite donc les valeurs du formulaire de page2 réduites ici à l'âge :

etape3.php

1. <?php

2. // etape3.php

4. // configuration

5. ini_set("register_globals","off");

6. ini_set("display_errors","off");

8. // démarrage session

9. session_start();

11. // on récupère les paramètres nom et age

12. $requête->age=$_POST["age"];

13. $session=$_SESSION["session"];

14. $requête->nom=$session->nom;

16. // normalement, on doit avoir un nom et un âge

17. if (! isset($requête->age) || ! isset($requête->nom)){

18. // si appel incorrect, on envoie page1

19. $_SESSION["session"]="";    // par précaution

20. $requête->nom="";

21. $requête->erreurs=array();

22. include "page1.php";

23. exit(0);

24. }//if

26. // le paramètre age est bien là - on vérifie sa validité

27. $page=calculerPage($requête);

29. // y-a-t-il eu des erreurs ?

30. if(count($page->erreurs)!=0){

31. // page 2 avec erreurs

32. $requête->erreurs=$page->erreurs;

33. include "page2.php";

34. exit(0);

35. }//if

37. // pas d'erreur - mémorisation de l'âge dans la session

38. $session->age=$requête->age;

39. $_SESSION["session"]=$session;

41. // affichage page 3

42. include "page3.php";

43. // fin

44. exit(0);

46. // ---------calculerPage

47. function calculerPage($requête){

48. // vérifie la validité de la requête $requête

49. // rend un tableau d'erreurs dans $page->erreurs

51. // au départ, pas d'erreurs

52. $page->erreurs=array();

53. // l'âge doit avoir un format valide

54. if (! preg_match("/^\s*\d{1,3}\s*$/",$requête->age)){

55. $page->erreurs[]="âge incorrect";

56. }

57. // retour page

58. return $page;

59. }//calculerPage

60. ?>

- le programme commence par récupérer le nom dans la session (provenant de page1) et l'âge dans le formulaire de page2. S'il manque l'une de ces informations, la page page1 est affichée.

- la validité de l'âge est ensuite vérifiée. Si l'âge est incorrect, la page page2 est réaffichée avec une liste d'erreurs. Si l'âge est correct, c'est la page page3 qui est affichée. Celle-ci se contente d'afficher les deux valeurs (nom,age) obtenues par les deux formulaires (page1,page2).

1. <? // page3.php ?>

3. <html>

4. <head>

5. <title>page 3</title>

6. </head>

7. <body>

8. <h3>Page 3/3</h3>

9. <table>

10. <tr>

11. <td>Nom</td>

12. <td><font color="green"><? echo $requête->nom ?></font></td>

13. </tr>

14. <tr>

15. <td>Votre âge</td>

16. <td><font color="green"><? echo $requête->age ?></font></td>

17. </tr>

18. </table>

19. </body>

20. </html>

IV - Exemples

IV-A - Application Impôts : Introduction

Nous introduisons ici l'application IMPOTS qui sera utilisée à de nombreuses reprises par la suite. Il s'agit d'une application permettant de calculer l'impôt d'un contribuable. On se place dans le cas simplifié d'un contribuable n'ayant que son seul salaire à déclarer :

- on calcule le nombre de parts du salarié nbParts=nbEnfants/2 +1 s'il n'est pas marié, nbEnfants/2+2 s'il est marié, où nbEnfants est son nombre d'enfants.

- s'il a au moins trois enfants, il a une demi-part de plus

- on calcule son revenu imposable R=0.72*S où S est son salaire annuel

- on calcule son coefficient familial QF=R/nbParts

- on calcule son impôt I. Considérons le tableau suivant :

12620.0

0

0

13190

0.05

631

15640

0.1

1290.5

24740

0.15

2072.5

31810

0.2

3309.5

39970

0.25

4900

48360

0.3

6898.5

55790

0.35

9316.5

92970

0.4

12106

127860

0.45

16754.5

151250

0.50

23147.5

172040

0.55

30710

195000

0.60

39312

0

0.65

49062

Chaque ligne a 3 champs. Pour calculer l'impôt I, on recherche la première ligne où QF<=champ1. Par exemple, si QF=23000 on trouvera la ligne

247400.152072.5

L'impôt I est alors égal à 0.15*R - 2072.5*nbParts. Si QF est tel que la relation QF<=champ1 n'est jamais vérifiée, alors ce sont les coefficients de la dernière ligne qui sont utilisés. Ici :

00.6549062

ce qui donne l'impôt I=0.65*R - 49062*nbParts.

Les données définissant les différentes tranches d'impôt sont dans une base de données ODBC-MySQL. MySQL est un SGBD du domaine public utilisable sur différentes plate-formes dont Windows et Linux. Avec ce SGBD, une base de données dbimpots a été créée avec dedans une unique table appelée impots. L'accès à la base est contrôlé par un login/motdepasse ici admimpots/mdpimpots. La copie d'écran suivante montre comment utiliser la base dbimpots avec MySQL :

1. dos>mysql -u admimpots -p

2. Enter password: *********

3. Welcome to the MySQL monitor.  Commands end with ; or \g.

4. Your MySQL connection id is 18 to server version: 3.23.49-max-nt

6. Type 'help;' or '\h' for help. Type '\c' to clear the buffer.

8. mysql> use dbimpots;

9. Database changed

11. mysql> show tables;

12. +--------------------+

13. | Tables_in_dbimpots |

14. +--------------------+

15. | impots             |

16. +--------------------+

17. 1 row in set (0.00 sec)

19. mysql> describe impots;

20. +---------+--------+------+-----+---------+-------+

21. | Field   | Type   | Null | Key | Default | Extra |

22. +---------+--------+------+-----+---------+-------+

23. | limites | double | YES  |     | NULL    |       |

24. | coeffR  | double | YES  |     | NULL    |       |

25. | coeffN  | double | YES  |     | NULL    |       |

26. +---------+--------+------+-----+---------+-------+

27. 3 rows in set (0.02 sec)

29. mysql> select * from impots;

30. +---------+--------+---------+

31. | limites | coeffR | coeffN  |

32. +---------+--------+---------+

33. |   12620 |      0 |       0 |

34. |   13190 |   0.05 |     631 |

35. |   15640 |    0.1 |  1290.5 |

36. |   24740 |   0.15 |  2072.5 |

37. |   31810 |    0.2 |  3309.5 |

38. |   39970 |   0.25 |    4900 |

39. |   48360 |    0.3 |    6898 |

40. |   55790 |   0.35 |  9316.5 |

41. |   92970 |    0.4 |   12106 |

42. |  127860 |   0.45 |   16754 |

43. |  151250 |    0.5 | 23147.5 |

44. |  172040 |   0.55 |   30710 |

45. |  195000 |    0.6 |   39312 |

46. |       0 |   0.65 |   49062 |

47. +---------+--------+---------+

48. 14 rows in set (0.00 sec)

50. mysql>quit

La base de données dbimpots est transformée en source de données ODBC de la façon suivante :

- on lance le gestionnaire des sources de données ODBC 32 bits

- on utilise le bouton [Add] pour ajouter une nouvelle source de données ODBC

- on désigne le pilote MySQL et on fait [Terminer]

- le pilote MySQL demande un certain nombre de renseignements :

1

le nom DSN à donner à la source de données ODBC - peut-être quelconque

2

la machine sur laquelle s'exécute le SGBD MySQL - ici localhost. Il est intéressant de noter que la base de données pourrait être une base de données distante. Les applications locales utilisant la source de données ODBC ne s'en apercevraient pas. Ce serait le cas notamment de notre application PHP.

3

la base de données MySQL à utiliser. MySQL est un SGBD qui gère des bases de données relationnelles qui sont des ensembles de tables reliées entre-elles par des relations. Ici, on donne le nom de la base gérée.

4

le nom d'un utilisateur ayant un droit d'accès à cette base

5

son mot de passe

IV-B - Application Impôts : la classe ImpotsDSN

Notre application s'appuiera sur la classe PHP ImpotsDSN qui aura les attributs, constructeur et méthode suivante :

1. // attributs

2. var $limites;        // tableau des limites

3. var $coeffR;        // tableau des coeffR

4. var $coeffN;        // tableau des coeffN

5. var $erreurs;        // tableau des erreurs

7. // constructeur

8. function ImpotsDSN($impots){...}

10. // méthode

11. function calculer($personne){...}

- nous avons vu dans l'exposé du problème que nous avions besoin de trois séries de données

12620.0

0

0

13190

0.05

631

15640

0.1

1290.5

24740

0.15

2072.5

31810

0.2

3309.5

39970

0.25

4900

48360

0.3

6898.5

55790

0.35

9316.5

92970

0.4

12106

127860

0.45

16754.5

151250

0.50

23147.5

172040

0.55

30710

195000

0.60

39312

0

0.65

49062

La 1re colonne sera placée dans l'attribut $limites de la classe ImotsDSN, la seconde dans l'attribut $coeffR, la troisième dans $coeffN. Ces trois attributs sont initialisés par le constructeur de la classe. Celui-ci va chercher les données dans une source de données ODBC. Diverses erreurs peuvent se produire lors de la construction de l'objet. Ces erreurs sont rapportées dans l'attribut $erreurs sous la forme d'un tableau de messages d'erreurs.

- le constructeur reçoit en paramètre un dictionnaire $impots avec les champs suivants :

1. // dsn : nom DSN de la source ODBC de données contenant les valeurs des tableaux limites, coeffR, coeffN

2. // user : nom d'un utilisateur ayant un droit de lecture sur la source ODBC

3. // pwd : son mot de passe

4. // table : nom de la table contenant les valeurs (limites, coeffR, coeffN)

5. // limites : nom de la colonne contenant les valeurs limites

6. // coeffR : nom de la colonne contenant les valeurs coeffR

7. // coeffN : nom de la colonne contenant les valeurs coeffN

Le paramètre du constructeur apporte toutes les informations nécessaires pour aller lire les données dans la source de données ODBC

- une fois l'objet ImpotsDSN construit, les utilisateur de la classe pourront appeler la méthode calculerImpots de l'objet. Celui-ci reçoit en paramètre un dictionnaire $personne :

1. // $personne["marié"] : oui, non

2. // $personne["enfants"] : nombre d'enfants

3. // $personne["salaire"] : salaire annuel

La classe complète est la suivante :

1. <?php

2. // définition d'une classe objImpots

3. class ImpotsDSN{

5. // attributs : les 3 tableaux de données

6. var $limites;        // tableau des limites

7. var $coeffR;        // tableau des coeffR

8. var $coeffN;        // tableau des coeffN

9. var $erreurs;        // tableau des erreurs

11. // constructeur

12. function ImpotsDSN($impots){

13. // $impots : dictionnaire contenant les champs suivants

14. // dsn : nom DSN de la source ODBC de données contenant les valeurs des tableaux limites, coeffR, coeffN

15. // user : nom d'un utilistaeur ayant un droit de lecture sur la source ODBC

16. // pwd : son mot de passe

17. // table : nom de la table contenant les valeurs (limites, coeffR, coeffN)

18. // limites : nom de la colonne contenant les valeurs limites

19. // coeffR : nom de la colonne contenant les valeurs coeffR

20. // coeffN : nom de la colonne contenant les valeurs coeffN

22. // au départ pas d'erreurs

23. $this->erreurs=array();

25. // vérification de l'appel

26. if (! isset($impots[dsn]) || ! isset($impots[user]) ||! isset($impots[pwd]) ||! isset($impots[table]) ||

27. ! isset($impots[limites]) ||! isset($impots[coeffR]) ||! isset($impots[coeffN])){

28. // erreur

29. $this->erreurs[]="Appel incorrect";

30. // fin

31. return;

32. }//if

34. // ouverture de la base DSN

35. $connexion=odbc_connect($impots[dsn],$impots[user],$impots[pwd]);

36. // erreur ?

37. if(! $connexion){

38. // erreur

39. $this->erreurs[]="Impossible d'ouvrir la base DSN [$impots[dsn]] (".odbc_error().")";

40. // fin

41. return;

42. }//if

44. // émission d'une requête sur la base

45. $requête=odbc_prepare($connexion,"select $impots[limites],$impots[coeffR],$impots[coeffN] from $impots[table]");

46. if(! odbc_execute($requête)){

47. // erreur

48. $this->erreurs[]="Impossible d'expoiter la base DSN [$impots[dsn]] (".odbc_error().")";

49. // fin

50. odbc_close($connexion);

51. return;

52. }//if

54. // exploitation des résultats de la requête

55. $this->limites=array();

56. $this->coeffR=array();

57. $this->coeffN=array();

58. while(odbc_fetch_row($requête)){

59. // une ligne de plus

60. $this->limites[]=odbc_result($requête,$impots[limites]);

61. $this->coeffR[]=odbc_result($requête,$impots[coeffR]);

62. $this->coeffN[]=odbc_result($requête,$impots[coeffN]);

63. }//while

65. // fermeture base

66. odbc_close($connexion);

67. }//constructeur

69. // --------------------------------------------------------------------------

70. function calculer($personne){

71. // $personne["marié"] : oui, non

72. // $personne["enfants"] : nombre d'enfants

73. // $personne["salaire"] : salaire annuel

75. // l'objet est-il dans un état correct ?

76. if (! is_array($this->erreurs) || count($this->erreurs)!=0) return -1;

78. // vérification de l'appel

79. if (! isset($personne[marié]) || ! isset($personne[enfants]) ||! isset($personne[salaire])){

80. // erreur

81. $this->erreurs[]="Appel incorrect";

82. // fin

83. return -1;

84. }//if

86. // les paramètres sont-ils corrects ?

87. $personne[marié]=strtolower($personne[marié]);

88. if($personne[marié]!=oui && $personne[marié]!=non){

89. // erreur

90. $this->erreurs[]="Statut marital [$personne[marié]] incorrect";

91. }//if

92. if(! preg_match("/^\s*\d{1,3}\s*$/",$personne[enfants])){

93. // erreur

94. $this->erreurs[]="Nombre d'enfants [$personne[enfants]] incorrect";

95. }//if

96. if(! preg_match("/^\s*\d+\s*$/",$personne[salaire])){

97. // erreur

98. $this->erreurs[]="salaire [$personne[salaire]] incorrect";

99. }//if

100. // des erreurs ?

101. if(count($this->erreurs)!=0) return -1;

103. // nombre de parts

104. if($personne[marié]==oui) $nbParts=$personne[enfants]/2+2;

105. else $nbParts=$personne[enfants]/2+1;

106. // une 1/2 part de plus si au moins 3 enfants

107. if($personne[enfants]>=3) $nbParts+=0.5;

108. // revenu imposable

109. $revenuImposable=0.72*$personne[salaire];

110. // quotient familial

111. $quotient=$revenuImposable/$nbParts;

112. // est mis à la fin du tableau limites pour arrêter la boucle qui suit

113. $this->limites[$this->nbLimites]=$quotient;

114. // calcul de l'impôt

115. $i=0;

116. while($quotient>$this->limites[$i]) $i++;

117. // du fait qu'on a placé $quotient à la fin du tableau $limites, la boucle précédente

118. // ne peut déborder du tableau $limites

119. // maintenant on peut calculer l'impôt

120. return floor($revenuImposable*$this->coeffR[$i]-$nbParts*$this->coeffN[$i]);

121. }//calculImpots

122. }//classe impôts

123. ?>

Un programme de test pourrait être le suivant :

1. <?php

2. // bibilothèques

3. include "ImpotsDSN.php";

5. // création d'un objet impots

6. $conf=array(dsn=>"mysql-dbimpots",user=>admimpots,pwd=>mdpimpots,

7. table=>impots,limites=>limites,coeffR=>coeffR,coeffN=>coeffN);

9. $objImpots=new ImpotsDSN($conf);

11. // des erreurs ?

12. if(count($objImpots->erreurs)!=0){

13. // msg

14. echo "Les erreurs suivantes se sont produites :\n";

15. $erreurs=$objImpots->erreurs;

16. for($i=0;$i<count($erreurs);$i++){

17. echo "$erreurs[$i]\n";

18. }//for

19. // fin

20. exit(1);

21. }//if

23. // des essais

24. calculerImpots($objImpots,oui,2,200000);

25. calculerImpots($objImpots,non,2,200000);

26. calculerImpots($objImpots,oui,3,200000);

27. calculerImpots($objImpots,non,3,200000);

28. calculerImpots($objImpots,array(),array(),array());

30. // fin

31. exit(0);

33. // -----------------------------------------------------------

34. function calculerImpots($objImpots,$marié,$enfants,$salaire){

35. // echo

36. echo "impots($marié,$enfants,$salaire)\n";

38. // calcul de l'impôt

39. $personne=array(marié=>$marié,enfants=>$enfants,salaire=>$salaire);

40. $montant=$objImpots->calculer($personne);

41. // des erreurs ?

42. if(count($objImpots->erreurs)!=0){

43. // msg

44. echo "Les erreurs suivantes se sont produites :\n";

45. $erreurs=$objImpots->erreurs;

46. for($i=0;$i<count($erreurs);$i++){

47. echo "$erreurs[$i]\n";

48. }//for

49. }else echo "montant=$montant\n";

50. }//calculerImp

- le programme de test prend soin d"inclure" le fichier contenant la classe ImpotsDSN

- puis crée un objet objImpots de la classe ImpotsDSN en passant au constructeur de l'objet les informations dont il a besoin

- une fois cet objet créé, la méthode calculer de celui-ci va être appelée cinq fois

Les résultats obtenus sont les suivants :

1. dos>e:\php43\php.exe test.php

2. impots(oui,2,200000)

3. montant=22504

4. impots(non,2,200000)

5. montant=33388

6. impots(oui,3,200000)

7. montant=16400

8. impots(non,3,200000)

9. montant=22504

10. impots(Array,Array,Array)

11. Les erreurs suivantes se sont produites :

12. Statut marital [array] incorrect

13. Nombre d'enfants [Array] incorrect

14. salaire [Array] incorrect

Nous utiliserons désormais la classe ImpotsDSN sans en redonner la définition.

IV-C - Application Impôts : version 1

Nous présentons maintenant la version 1 de l'application IMPOTS. On se place dans le contexte d'une application web qui présenterait une interface HTML à un utilisateur afin d'obtenir les trois paramètres nécessaires au calcul de l'impôt :

1. l'état marital (marié ou non)

2. le nombre d'enfants

3. le salaire annuel

L'affichage du formulaire est réalisé par la page PHP suivante :

1. <?php //formulaire des impôts ?>

3. <html>

4. <head>

5. <title>impots</title>

6. <script language="JavaScript" type="text/javascript">

7. function effacer(){

8. // raz du formulaire

9. with(document.frmImpots){

10. optMarie[0].checked=false;

11. optMarie[1].checked=true;

12. txtEnfants.value="";

13. txtSalaire.value="";

14. txtImpots.value="";

15. }//with

16. }//effacer

17. </script>

18. </head>

20. <body background="/poly/impots/images/standard.jpg">

21. <center>

22. Calcul d'impôts

23. <hr>

24. <form name="frmImpots" method="POST">

25. <table>

26. <tr>

27. <td>Etes-vous marié(e)</td>

28. <td>

29. <input type="radio" name="optMarie" value="oui" <?php echo $requête->chkoui ?>>oui

30. <input type="radio" name="optMarie" value="non" <?php echo $requête->chknon ?>>non

31. </td>

32. </tr>

33. <tr>

34. <td>Nombre d'enfants</td>

35. <td><input type="text" size="5" name="txtEnfants" value="<?php echo $requête->enfants ?>"></td>

36. </tr>

37. <tr>

38. <td>Salaire annuel</td>

39. <td><input type="text" size="10" name="txtSalaire" value="<?php echo $requête->salaire ?>"></td>

40. </tr>

41. <tr>

42. <td><font color="green">Impôt</font></td>

43. <td><input type="text" size="10" name="txtImpots" value="<?php echo $requête->impots ?>" readonly></td>

44. </tr>

45. <tr></tr>

46. <tr>

47. <td><input type="submit" value="Calculer"></td>

48. <td><input type="button" value="Effacer" onclick="effacer()"></td>

49. </tr>

50. </table>

51. </form>

52. </center>

53. <?php

54. // y-a-t-il des erreurs ?

55. if(count($requête->erreurs)!=0){

56. // affichage des erreurs

57. echo "<hr>\n<font color=\"red\">\n";

58. echo "Les erreurs suivantes se sont produites<br>";

59. echo "<ul>";

60. for($i=0;$i<count($requête->erreurs);$i++){

61. echo "<li>".$requête->erreurs[$i]."</li>\n";

62. }

63. echo "</ul>\n</font>\n";

64. }//if

65. ?>

66. </body>

67. </html>

La page PHP se contente d'afficher des informations qui lui sont passées par le programme principal de l'application dans la variable $requête qui est un objet avec les champs suivants :

chkoui, chknon

attributs des boutons radio oui, non - ont pour valeurs possibles "checked" ou "" afin d'allumer ou non le bouton radio correspondant

enfants

le nombre d'enfants du contribuable

salaire

son salaire annuel

impots

le montant de l'impôt à payer

erreurs

une liste éventuelle d'erreurs - peut être vide.

La page envoyée au client contient un script javascript contenant une fonction effacer associée au bouton "Effacer" dont le rôle est de remettre le formulaire dans son état initial : bouton non coché, champs de saisie vides. On notera que ce résultat ne pouvait pas être obtenu avec un bouton HTML de type "reset". En effet, lorsque ce type de bouton est utilisé, le navigateur remet le formulaire dans l'état où il l'a reçu. Or dans notre application, le navigateur reçoit des formulaires qui peuvent être non vides.

L'application qui traite le formulaire précédent est la suivante :

1. <?php

2. // traite le formulaire des impôts

4. // bibilothèques

5. include "ImpotsDSN.php";

7. // configuration de l'application

8. ini_set("register_globals","off");

9. ini_set("display_errors","off");

10. $formulaireImpots="impots_form.php";

11. $erreursImpots="impots_erreurs.php";

12. $bdImpots=array(dsn=>"mysql-dbimpots",user=>admimpots,pwd=>mdpimpots,

13. table=>impots,limites=>limites,coeffR=>coeffR,coeffN=>coeffN);

15. // on récupère les paramètres

16. $requête->marié=$_POST["optMarie"];

17. $requête->enfants=$_POST["txtEnfants"];

18. $requête->salaire=$_POST["txtSalaire"];

20. // a-t-on tous les paramètres ?

21. if(! isset($requête->marié) || ! isset($requête->enfants) || ! isset($requête->salaire)){

22. // préparation formulaire vide

23. $requête->chkoui="";

24. $requête->chknon="checked";

25. $requête->enfants="";

26. $requête->salaire="";

27. $requête->impots="";

28. $requête->erreurs=array();

29. // affichage formulaire

30. include $formulaireImpots;

31. // fin

32. exit(0);

33. }//if

35. // vérification paramètres

36. $requête=vérifier($requête);

38. // des erreurs ?

39. if(count($requête->erreurs)!=0){

40. // affichage formulaire

41. include "$formulaireImpots";

42. // fin

43. exit(0);

44. }//if

46. // calcul de la réponse

47. $requête=calculerImpots($bdImpots,$requête);

49. // des erreurs ?

50. if(count($requête->erreurs)!=0){

51. // affichage formulaire erreurs

52. include "$erreursImpots";

53. // fin

54. exit(0);

55. }//if

57. // affichage formulaire

58. include "$formulaireImpots";

59. // fin

60. exit(0);

62. // --------------------------------------------------------

63. function vérifier($requête){

64. // vérifie la validité des paramètres de la requête

66. // au départ pas d'erreurs

67. $requête->erreurs=array();

69. // statut valide ?

70. $requête->marié=strtolower($requête->marié);

71. if($requête->marié!="oui" && $requête->marié!="non"){

72. // une erreur

73. $requête->erreurs[]="Statut marital [$requête->marié] incorrect";

74. }

76. // nbre d'enfants valide ?

77. if(! preg_match("/^\s*\d+\s*$/",$requête->enfants)){

78. // une erreur

79. $requête->erreurs[]="Nombre d'enfants [$requête->enfants] incorrect";

80. }

82. // salaire valide ?

83. if(! preg_match("/^\s*\d+\s*$/",$requête->salaire)){

84. // une erreur

85. $requête->erreurs[]="Salaire [$requête->salaire] incorrect";

86. }

88. // état des boutons radio

89. if($requête->marié=="oui"){

90. $requête->chkoui="checked";

91. $requête->chknon="";

92. }else{

93. $requête->chknon="checked";

94. $requête->chkoui="";

95. }

97. // on rend la requête

98. return $requête;

99. }//vérifier

101. // --------------------------------------------------------

102. function calculerImpots($bdImpots,$requête){

103. // calcule le montant de l'impôt

105. // $bdImpots : dictionnaire contenant les informations nécessaires pour lire la source de données ODBC

106. // $requête : la requête contenant les informations sur le contribuable

108. // on construit un objet ImpotsDSN

109. $objImpots=new ImpotsDSN($bdImpots);

111. // des erreurs ?

112. if(count($objImpots->erreurs)!=0){

113. // on met les erreurs dans la requête

114. $requête->erreurs=$objImpots->erreurs;

115. // fini

116. return $requête;

117. }//if

119. // calcul de l'impôt

120. $personne=array(marié=>"$requête->marié",enfants=>"$requête->enfants",salaire=>"$requête->salaire");

121. $requête->impots=$objImpots->calculer($personne);

123. // on rend le résultat

124. return $requête;

125. }//calculerImpots

Commentaires :

- tout d'abord, la classe ImpotsDSN est incluse. C'est elle qui est à la base du calcul d'impôts et qui va nous cacher l'accès à la base de données

- un certain nombre d'initialisations sont faites visant à faciliter la maintenance de l'application. Si des paramètres changent, on changera leurs valeurs dans cette section. En général, ces valeurs de configuration sont plutôt stockées dans un fichier ou une base de données.

- on récupère les trois paramètres du formulaire correspondant aux champs HTML optMarie, txtEnfants, txtSalaire.

- ces paramètres peuvent être totalement ou partiellement absents. Ce sera notamment le cas lors de la demande initiale du formulaire. Dans ce cas-là, on se contente d'envoyer un formulaire vide.

- ensuite la validité des trois paramètres récupérés est vérifiée. S'ils s'avèrent incorrects, le formulaire est renvoyé tel qu'il a été saisi mais avec de plus une liste d'erreurs.

- une fois vérifiée la validité des trois paramètres, on peut faire le calcul de l'impôt. Celui-ci est calculé par la fonction calculerImpots. Cette fonction construit un objet de type ImpotsDSN et utilise la méthode calculer de cet objet.

- La construction de l'objet ImpotsDSN peut échouer si la base de données est indisponible. Dans ce cas, l'application affiche une page d'erreur spécifique indiquant les erreurs qui se sont produites.

- Si tout s'est bien passé, le formulaire est renvoyé tel qu'il a été saisi mais avec de plus le montant de l'impôt à payer.

La page d'erreurs est la suivante :

1. <?php    // page d'erreur ?>

2. <html>

3. <head>

4. <title>Application impôts indisponible</title>

5. </head>

6. <body background="/poly/impots/images/standard.jpg">

7. <h3>Calcul d'impôts</h3>

8. <hr>

9. Application indisponible. Recommencez ultérieurement.<br><br>

10. <font color="red">

11. Les erreurs suivantes se sont produites<br>

12. <ul>

13. <?php

14. // affichage des erreurs

15. for($i=0;$i<count($requête->erreurs);$i++){

16. echo "<li>".$requête->erreurs[$i]."</li>\n";

17. }

18. ?>

19. </ul>

20. </font>

21. </body>

22. </html>

Voici quelques exemples d'erreurs. Tout d'abord on fournit un mot de passe incorrect à la base :

On fournit des données incorrectes dans le formulaire :

Enfin, on fournit des valeurs correctes :

IV-D - Application Impôts : version 2

Dans l'exemple précédent, la validité des paramètres txtEnfants, txtSalaire du formulaire est vérifiée par le serveur. On se propose ici de la vérifier par un script Javascript inclus dans la page du formulaire. C'est alors le navigateur qui fait la vérification des paramètres. Le serveur n'est alors sollicité que si ceux-ci sont valides. On économise ainsi de la "bande passante". La page impots-form.php d'affichage devient la suivante :

1. <?php //formulaire des impôts ?>

3. <html>

4. <head>

5. <title>impots</title>

6. <script language="JavaScript" type="text/javascript">

7. function effacer(){

8. ........

9. }//effacer

11. //------------------------------

12. function calculer(){

13. // vérification des paramètres avant de les envoyer au serveur

14. with(document.frmImpots){

15. //nbre d'enfants

16. champs=/^\s*(\d+)\s*$/.exec(txtEnfants.value);

17. if(champs==null){

18. // le modéle n'est pas vérifié

19. alert("Le nombre d'enfants n'a pas été donné ou est incorrect");

20. nbEnfants.focus();

21. return;

22. }//if

23. //salaire

24. champs=/^\s*(\d+)\s*$/.exec(txtSalaire.value);

25. if(champs==null){

26. // le modéle n'est pas vérifié

27. alert("Le salaire n'a pas été donné ou est incorrect");

28. salaire.focus();

29. return;

30. }//if

31. // c'est bon - on envoie le formulaire au serveur

32. submit();

33. }//with

34. }//calculer

35. </script>

36. </head>

38. <body background="/poly/impots/images/standard.jpg">

39. ............

40. <td><input type="button" value="Calculer" onclick="calculer()"></td>

41. <td><input type="button" value="Effacer" onclick="effacer()"></td>

42. ........

43. </body>

44. </html>

On notera les changements suivants :

- le bouton Calculer n'est plus de type submit mais de type button associé à une fonction appelée calculer. C'est celle-ci qui fera l'analyse des champs txtEnfants et txTsalaire. Si elles sont correctess, les valeurs du formulaire seront envoyées au serveur (submit) sinon un message d'erreur sera affiché.

Voici un exemple d'affichage en cas d'erreur :

IV-E - Application Impôts : version 3

Nous modifions légèrement l'application pour y introduire la notion de session. Nous considérons maintenant que l'application est une application de simulation de calcul d'impôts. Un utilisateur peut alors simuler différentes "configurations" de contribuable et voir quelle serait pour chacune d'elles l'impôt à payer. La page web ci-dessous donne un exemple de ce qui pourrait être obtenu :

Le programme d'affichage de la page ci-dessus est le suivant :

1. <?php //formulaire des impôts ?>

3. <html>

4. <head>

5. <title>impots</title>

6. <script language="JavaScript" type="text/javascript">

7. function effacer(){

8. ...

9. }//effacer

11. //------------------------------

12. function calculer(){

13. ...

14. }//calculer

15. </script>

16. </head>

18. <body background="/poly/impots/images/standard.jpg">

19. <center>

20. Calcul d'impôts

21. <hr>

22. <form name="frmImpots" method="POST">

23. <table>

24. <tr>

25. <td>Etes-vous marié(e)</td>

26. <td>

27. <input type="radio" name="optMarie" value="oui" <?php echo $requête[chkoui] ?>>oui

28. <input type="radio" name="optMarie" value="non" <?php echo $requête[chknon] ?>>non

29. </td>

30. </tr>

31. <tr>

32. <td>Nombre d'enfants</td>

33. <td><input type="text" size="5" name="txtEnfants" value="<?php echo $requête[enfants] ?>"></td>

34. </tr>

35. <tr>

36. <td>Salaire annuel</td>

37. <td><input type="text" size="10" name="txtSalaire" value="<?php echo $requête[salaire] ?>"></td>

38. </tr>

39. <tr>

40. <td><font color="green">Impôt</font></td>

41. <td><input type="text" size="10" name="txtImpots" value="<?php echo $requête[impots] ?>" readonly></td>

42. </tr>

43. <tr></tr>

44. <tr>

45. <td><input type="button" value="Calculer" onclick="calculer()"></td>

46. <td><input type="button" value="Effacer" onclick="effacer()"></td>

47. </tr>

48. </table>

49. </form>

50. </center>

51. <?php

52. // y-a-t-il des erreurs ?

53. if(count($requête[erreurs])!=0){

54. // affichage des erreurs

55. echo "<hr>\n<font color=\"red\">\n";

56. echo "Les erreurs suivantes se sont produites<br>";

57. echo "<ul>";

58. for($i=0;$i<count($requête[erreurs]);$i++){

59. echo "<li>".$requête[erreurs][$i]."</li>\n";

60. }

61. echo "</ul>\n</font>\n";

62. }//if

63. // y-a-t-il des simulations ?

64. else if(count($requête[simulations])!=0){

65. // affichage des simulations

66. echo "<hr>\n<h2>Résultats des simulations</h2>\n";

67. echo "<table border=\"1\">\n";

68. echo "<tr><td>Marié</td><td>Enfants</td><td>Salaire annuel (F)</td><td>Impôts à payer (F)</td></tr>\n";

69. for($i=0;$i<count($requête[simulations]);$i++){

70. echo "<tr>".

71. "<td>".$requête[simulations][$i][0]."</td>".

72. "<td>".$requête[simulations][$i][1]."</td>".

73. "<td>".$requête[simulations][$i][2]."</td>".

74. "<td>".$requête[simulations][$i][3]."</td>".

75. "</tr>\n";

76. }//for

77. echo "</table>\n";

78. }//if

79. ?>

80. </body>

81. </html>

Le programme d'affichage présente des différences mineures avec sa version précédente :

1. le dictionnaire reçoit un dictionnaire $requête au lieu d'un objet $requête. On écrit donc $requête[enfants] et non $requête->enfants.

2. ce dictionnaire a un champ simulations qui est un tableau a deux dimensions. $requête[simulations][i] est la simulation n° i. Celle-ci est elle-même un tableau de quatre chaînes de caractères : statut marital, nombre d'enfants, salaire annuel, impôt à payer.

Le programme principal a lui évolué plus profondément :

1. <?php

2. // traite le formulaire des impôts

4. // bibilothèques

5. include "ImpotsDSN.php";

7. // démarrage session

8. session_start();

10. // configuration de l'application

11. ini_set("register_globals","off");

12. ini_set("display_errors","off");

13. $formulaireImpots="impots_form.php";

14. $erreursImpots="impots_erreurs.php";

15. $bdImpots=array(dsn=>"mysql-dbimpots",user=>admimpots,pwd=>mdpimpots,

16. table=>impots,limites=>limites,coeffR=>coeffR,coeffN=>coeffN);

18. // on récupère les paramètres de la session

19. $session=$_SESSION["session"];

20. // session valide ?

21. if(! isset($session) || ! isset($session[objImpots]) || ! isset($session[simulations])){

22. // on démarre une nouvelle session

23. $session=array(objImpots=>new ImpotsDSN($bdImpots),simulations=>array());

24. // des erreurs ?

25. if(count($session[objImpots]->erreurs)!=0){

26. $requête=array(erreurs=>$session[objImpots]->erreurs);

27. // affichage page d'erreurs

28. include $erreursImpots;

29. // fin

30. $session=array();

31. terminerSession($session);

32. }//if

33. }//if

35. // on récupère les paramètres de l'échange en cours

36. $requête[marié]=$_POST["optMarie"];

37. $requête[enfants]=$_POST["txtEnfants"];

38. $requête[salaire]=$_POST["txtSalaire"];

40. // a-t-on tous les paramètres ?

41. if(! isset($requête[marié]) || ! isset($requête[enfants]) || ! isset($requête[salaire])){

42. // affichage formulaire vide

43. $requête=array(chkoui=>"",chknon=>"checked",enfants=>"",salaire=>"",impots=>"",

44. erreurs=>array(),simulations=>array());

45. include $formulaireImpots;

46. // fin

47. terminerSession($session);

48. }//if

50. // vérification paramètres

51. $requête=vérifier($requête);

53. // des erreurs ?

54. if(count($requête[erreurs])!=0){

55. // affichage formulaire

56. include "$formulaireImpots";

57. // fin

58. terminerSession($session);

59. }//if

61. // calcul de l'impôt à payer

62. $requête[impots]=$session[objImpots]->calculer(array(marié=>$requête[marié],

63. enfants=>$requête[enfants],salaire=>$requête[salaire]));

65. // une simulation de plus

66. $session[simulations][]=array($requête[marié],$requête[enfants],$requête[salaire],$requête[impots]);

67. $requête[simulations]=$session[simulations];

69. // affichage formulaire

70. include "$formulaireImpots";

72. // fin

73. terminerSession($session);

75. // --------------------------------------------------------

76. function vérifier($requête){

77. // vérifie la validité des paramètres de la requête

79. // au départ pas d'erreurs

80. $requête[erreurs]=array();

82. // statut valide ?

83. $requête[marié]=strtolower($requête[marié]);

84. if($requête[marié]!="oui" && $requête[marié]!="non"){

85. // une erreur

86. $requête[erreurs][]="Statut marital [$requête[marié]] incorrect";

87. }

89. // nbre d'enfants valide ?

90. if(! preg_match("/^\s*\d+\s*$/",$requête[enfants])){

91. // une erreur

92. $requête[erreurs][]="Nombre d'enfants [$requête[enfants]] incorrect";

93. }

95. // salaire valide ?

96. if(! preg_match("/^\s*\d+\s*$/",$requête[salaire])){

97. // une erreur

98. $requête[erreurs][]="Salaire [$requête[salaire]] incorrect";

99. }

101. // état des boutons radio

102. if($requête[marié]=="oui"){

103. $requête[chkoui]="checked";

104. $requête[chknon]="";

105. }else{

106. $requête[chknon]="checked";

107. $requête[chkoui]="";

108. }

110. // on rend la requête

111. return $requête;

112. }//vérifier

114. // --------------------------------------------------------

115. function terminerSession($session){

116. // on mémorise la session

117. $_SESSION[session]=$session;

118. // fin script

119. exit(0);

120. }//terminerSession

- tout d'abord, cette application gère une session. On lance donc une session en début de script :

1. session_start();

- la session mémorise une seule variable : le dictionnaire $session. Ce dictionnaire a deux champs :

1. objImpots : un objet ImpotsDSN. On mémorise cet objet dans la session du client afin d'éviter des accès successifs inutiles à la base de données ODBC.

2. simulations : le tableau des simulations pour se rappeler d'un échange à l'autre les simulations des échanges précédents.

1. objImpots : un objet ImpotsDSN. On mémorise cet objet dans la session du client afin d'éviter des accès successifs inutiles à la base de données ODBC.

2. simulations : le tableau des simulations pour se rappeler d'un échange à l'autre les simulations des échanges précédents.

- une fois la session démarrée, on récupère la variable $session. Si celle-ci n'existe pas encore, c'est que la session démarre. On crée alors un objet ImpotsDSN et un tableau de simulations vide. Si besoin est, des erreurs sont signalées.

1. // on récupère les paramètres de la session

2. $session=$_SESSION["session"];

3. // session valide ?

4. if(! isset($session) || ! isset($session[objImpots]) || ! isset($session[simulations])){

5. // on démarre une nouvelle session

6. $session=array(objImpots=>new ImpotsDSN($bdImpots),simulations=>array());

7. // des erreurs ?

8. if(count($session[objImpots]->erreurs)!=0){

9. $requête=array(erreurs=>$session[objImpots]->erreurs);

10. // affichage page d'erreurs

11. include $erreursImpots;

12. // fin

13. $session=array();

14. terminerSession($session);

15. }//if

16. }//if

- une fois la session correctement initialisée, les paramètres de l'échange sont récupérés. S'il n'y a pas tous les paramètres attendus, un formulaire vide est envoyé.

1. // on récupère les paramètres de l'échange en cours

2. $requête[marié]=$_POST["optMarie"];

3. $requête[enfants]=$_POST["txtEnfants"];

4. $requête[salaire]=$_POST["txtSalaire"];

6. // a-t-on tous les paramètres ?

7. if(! isset($requête[marié]) || ! isset($requête[enfants]) || ! isset($requête[salaire])){

8. // affichage formulaire vide

9. $requête=array(chkoui=>"",chknon=>"checked",enfants=>"",salaire=>"",impots=>"",

10. erreurs=>array(),simulations=>array());

11. include $formulaireImpots;

12. // fin

13. terminerSession($session);

14. }//if

- si on a bien tous les paramètres, ils sont vérifiés. S'il y a des erreurs, elles sont signalées :

1. // vérification paramètres

2. $requête=vérifier($requête);

4. // des erreurs ?

5. if(count($requête[erreurs])!=0){

6. // affichage formulaire

7. include "$formulaireImpots";

8. // fin

9. terminerSession($session);

10. }//if

- si les paramètres sont corrects, l'impôt est calculé :

1. // calcul de l'impôt à payer

2. $requête[impots]=$session[objImpots]->calculer(array(marié=>$requête[marié],

3. enfants=>$requête[enfants],salaire=>$requête[salaire]));

- la simulation en cours est ajoutée au tableau des simulations et celui-ci envoyé au client avec le formulaire :

1. // une simulation de plus

2. $session[simulations][]=array($requête[marié],$requête[enfants],$requête[salaire],$requête[impots]);

3. $requête[simulations]=$session[simulations];

5. // affichage formulaire

6. include "$formulaireImpots";

8. // fin

9. terminerSession($session);

- dans tous les cas, le script se termine en enregistrant la variable $session dans la session. Ceci est fait dans la procédure terminerSession.

1. function terminerSession($session){

2. // on mémorise la session

3. $_SESSION[session]=$session;

4. // fin script

5. exit(0);

6. }//terminerSession

Terminons en présentant le programme d'affichage des erreurs d'accès à la base de données (impots-erreurs.php) :

impots-erreurs.php

1. <?php    // page d'erreur ?>

2. <html>

3. <head>

4. <title>Application impôts indisponible</title>

5. </head>

6. <body background="/poly/impots/images/standard.jpg">

7. <h3>Calcul d'impôts</h3>

8. <hr>

9. Application indisponible. Recommencez ultérieurement.<br><br>

10. <font color="red">

11. Les erreurs suivantes se sont produites<br>

12. <ul>

13. <?php

14. // affichage des erreurs

15. for($i=0;$i<count($requête[erreurs]);$i++){

16. echo "<li>".$requête[erreurs][$i]."</li>\n";

17. }

18. ?>

19. </ul>

20. </font>

21. </body>

22. </html>

Ici également, l'objet $requête a été remplacé par un dictionnaire $requête.

IV-F - Application Impôts : version 4

Nous allons maintenant créer une application autonome qui sera un client web de l'application web impots précédente. L'application sera une application console lancée à partir d'une fenêtre DOS :

1. dos>e:\php43\php.exe cltImpots.php

2. Syntaxe cltImpots.php urlImpots marié enfants salaire [jeton]

Les paramètres du client web cltImpots sont les suivants :

1. // syntaxe $0 urlImpots marié enfants salaire jeton

2. // client d'un service d'impôts fonctionnant sur urlImpots

3. // envoie à ce service les trois informations : marié, enfants, salaire

4. // éventuellement avec le jeton de session si celui-ci a été passé

5. // affiche le tableau des simulations de la session

Voici quelques exemples d'utilisation. Tout d'abord un premier exemple sans jeton de session.

1. dos>e:\php43\php.exe cltImpots.php http://localhost/poly/impots/6/impots.php oui 2 200000

3. Jeton de session=[a6297317667bc981c462120987b8dd18]

5. Simulations :

6. [Marié,Enfants,Salaire annuel (F),Impôts à payer (F)]

7. [oui,2,200000,22504]

Nous avons bien récupéré le montant de l'impôt à payer (22504 f). Nous avons également récupéré le jeton de session. Nous pouvons maintenant l'utiliser pour une seconde interrogation :

1. dos>e:\php43\php.exe cltImpots.php http://localhost/poly/impots/6/impots.php oui 3 200000 a6297317667bc981c462120987b8dd18

3. Jeton de session=[a6297317667bc981c462120987b8dd18]

5. Simulations :

6. [Marié,Enfants,Salaire annuel (F),Impôts à payer (F)]

7. [oui,2,200000,22504]

8. [oui,3,200000,16400]

Nous obtenons bien le tableau des simulations envoyées par le serveur web. Le jeton récupéré reste bien sûr le même. Si nous interrogeons le service web alors que la base de données n'a pas été lancée, nous obtenons le résultat suivant :

1. dos>e:\php43\php.exe cltImpots2.php http://localhost/poly/impots/6/impots.php oui 3 200000

3. Jeton de session=[8369014d5053212bc42f64bbdfb152ee]

5. Les erreurs suivantes se sont produites :

6. Impossible d'ouvrir la base DSN [mysql-dbimpots] (S1000)

Lorsqu'on écrit un client web programmé, il est nécessaire de savoir exactement ce qu'envoie le serveur en réponse aux différentes demandes possibles d'un client. Le serveur envoie un ensemble de lignes HTML comprenant des informations utiles et d'autres qui ne sont là que pour la mise en page HTML. Les expressions régulières de PHP peuvent nous aider à trouver les informations utiles dans le flot des lignes envoyées par le serveur. Pour cela, nous avons besoin de connaître le format exact des différentes réponses du serveur. Pour cela, nous pouvons interroger le service web avec un navigateur et regarder le code source qui a été envoyé. On notera que cette méthode ne permet pas de connaître les entêtes HTTP de la réponse du serveur web. Il est parfois utile de connaître ceux-ci. On peut alors utiliser l'un des deux clients web génériques vus dans le chapitre précédent.

Si nous répétons les exemples précédents, voici le code HTML reçu par le navigateur pour le tableau des simulations :

1. <h2>Résultats des simulations</h2>

2. <table border="1">

3. <tr><td>Marié</td><td>Enfants</td><td>Salaire annuel (F)</td><td>Impôts à payer (F)</td></tr>

4. <tr><td>oui</td><td>2</td><td>200000</td><td>22504</td></tr>

5. <tr><td>oui</td><td>3</td><td>200000</td><td>16400</td></tr>

6. </table>

C'est une table HTML, la seule dans le document envoyé. Ainsi l'expression régulière "|<tr>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*</tr>|" doit-elle permettre de retrouver dans le document les différentes simulations reçues. Lorsque la base de données est indisponible, on reçoit le document suivant :

1. Application indisponible. Recommencez ultérieurement.<br><br>

2. <font color="red">

3. Les erreurs suivantes se sont produites<br>

4. <ul>

5. <li>Impossible d'ouvrir la base DSN [mysql-dbimpots] (S1000)</li>

6. </ul>

7. </font>

Pour récupérer l'erreur, le client web devra chercher dans la réponse du serveur web les lignes correspondant au modèle : "|<li>(.+?)</li>|".

Nous avons maintenant les lignes directrices de ce qu'il faut faire lorsque l'utilisateur demande le calcul de l'impôt à partir du client console précédent :

- vérifier que tous les paramètres sont valides et éventuellement signaler les erreurs.

- se connecter à l'URL donnée comme premier paramètre. Pour cela on suivra le modèle du client web générique déjà présenté et étudié

- dans le flux de la réponse du serveur, utiliser des expressions régulières pour soit :

- trouver les messages d'erreurs

- trouver les résultats des simulations

- trouver les messages d'erreurs

- trouver les résultats des simulations

Le code du client cltImpots.php est le suivant :

cltImpots.php

1. <?php

2. // syntaxe $0 urlImpots marié enfants salaire jeton

3. // client d'un service d'impôts fonctionnant sur urlImpots

4. // envoie à ce service les trois informations : marié, enfants, salaire

5. // éventuellement avec le jeton de session si celui-ci a été passé

6. // affiche le tableau des simulations de la session

8. // vérification nbre d'arguments

9. if(count($argv)!=5 && count($argv)!=6){

10. // erreur

11. fwrite(STDERR,"Syntaxe $argv[0] urlImpots marié enfants salaire [jeton]\n");

12. // fin

13. exit(1);

14. }//if

16. // on note l'URL

17. $urlImpots=analyseURL($argv[1]);

18. if(isset($urlImpots[erreur])){

19. // erreur

20. fwrite(STDERR,"$urlImpots[erreur]\n");

21. // fin

22. exit(1);

23. }//if

25. // analyse de la demande de l'utilisateur

26. $demande=analyseDemande("$argv[2],$argv[3],$argv[4]");

27. // erreurs ?

28. if($demande[erreur]){

29. // msg

30. echo "$demande[erreur]\n";

31. // c'est fini

32. exit(1);

33. }//if

35. // on peut faire la demande - on utilise le jeton de session

36. $impots=getImpots($urlImpots,$demande,$argv[5]);

38. // on affiche le jeton de session

39. echo "Jeton de session=[$impots[jeton]]\n";

41. // des erreurs ?

42. if(count($impots[erreurs])!=0){

43. //msg d'erreurs

44. echo "Les erreurs suivantes se sont produites :\n";

45. for($i=0;$i<count($impots[erreurs]);$i++){

46. echo $impots[erreurs][$i]."\n";

47. }//for

48. }else{

49. // pas d'erreurs - affichage des simulations

50. echo "Simulations :\n";

51. for($i=0;$i<count($impots[simulations]);$i++){

52. echo "[".implode(",",$impots[simulations][$i])."]\n";

53. }//for

54. }//if

56. // fin du programme

57. exit(0);

59. // --------------------------------------------------------------

60. function analyseURL($URL){

61. // analyse la validité de l'URL $URL

62. $url=parse_url($URL);

63. // le protocole

64. if(strtolower($url[scheme])!="http"){

65. $url[erreur]="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";

66. return $url;

67. }//if

68. // la machine

69. if(! isset($url[host])){

70. $url[erreur]="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";

71. return $url;

72. }//if

73. // le port

74. if(! isset($url[port])) $url[port]=80;

75. // la requête

76. if(isset($url["query"])){

77. $url[erreur]="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";

78. return $url;

79. }//if

80. // retour

81. return $url;

82. }//analyseURL

84. // -----------------------------------------------------------

85. function analyseDemande($demande){

86. // $demande : chaîne à analyser

87. // doit être de la forme marié, enfants, salaire

88. // format valide

89. if(! preg_match("/^\s*(oui|non)\s*,\s*(\d{1,3})\s*,\s*(\d+)\s*$/i",$demande,$champs)){

90. // format invalide

91. return(array(erreur=>"Format (marié, enfants, salaire) invalide."));

92. }

93. // c'est bon

94. $marié=strtolower($champs[1]);

95. return array(marié=>$marié,enfants=>$champs[2],salaire=>$champs[3]);

96. }//analyseDemande

98. // --------------------------------------------------------------

99. function getImpots($urlImpots,$demande,$jeton){

100. // $urlImpots : URL à interroger

101. // $demande : dictionnaire contenant les champs marie, enfants, salaire

102. // $jeton : un éventuel jeton de session

104. // ouverture d'une connexion sur le port $urlImpots[port] de $urlImpots[host]

105. $connexion=fsockopen($urlImpots[host],$urlImpots[port],&$errno,&$erreur);

106. // retour si erreur

107. if(! $connexion){

108. return array(erreurs=>array("Echec de la connexion au site ($urlImpots[host],$urlImpots[port]) : $erreur"));

109. }//if

111. // on envoie les entetes HTTP au serveur

112. POST($connexion,$urlImpots,$jeton,

113. array(optMarie=>$demande[marié],txtEnfants=>$demande[enfants],txtSalaire=>$demande[salaire]));

115. // on lit la réponse du serveur - d'abord les entêtes HTTP

116. // la première ligne

117. $ligne=fgets($connexion,10000);

119. // URL trouvée ?

120. if(! preg_match("/^(.+?) 200 OK\s*$/",$ligne)){

121. // URL inaccessible - retour avec erreur

122. return array(erreurs=>array("L'URL $urlImpots[path] n'a pu être trouvée"));

123. }//if

125. // on lit les autres entêtes HTTP

126. while(($ligne=fgets($connexion,10000)) && (($ligne=rtrim($ligne))!="")){

127. // recherche du jeton si on ne l'a pas encore trouvé

128. if(! $jeton){

129. // recherche de la ligne set-cookie

130. if(preg_match("/^Set-Cookie: PHPSESSID=(.*?);/i",$ligne,$champs)){

131. // on a trouvé le jeton - on le mémorise

132. $jeton=$champs[1];

133. }//if

134. }//if

135. }//ligne suivante

137. // on lit le document qui suit

138. $document="";

139. while($ligne=fread($connexion,10000)){

140. $document.=$ligne;

141. }//while

143. // on ferme la connexion

144. fclose($connexion);

146. // on analyse le document

147. $impots=getInfos($document);

149. // on rend le résultat

150. return array(jeton=>$jeton,erreurs=>$impots[erreurs],simulations=>$impots[simulations]);

151. }//getImpots

153. // --------------------------------------------------------------

154. function POST($connexion,$url,$jeton,$paramètres){

155. // $connexion : la connexion a serveur web

156. // $url : l'URL à interroger

157. // $paramètres : dictionnaire des paramètres à poster

159. // on prépare le POST

160. $post="";

161. while(list($paramètre,$valeur)=each($paramètres)){

162. $post.=$paramètre."=".urlencode($valeur)."&";

163. }//while

164. // on enlève le dernier caractère

165. $post=substr($post,0,-1);

167. // on prépare la requête HTTP au format HTTP/1.0

168. $HTTP="POST $url[path] HTTP/1.0\n";

169. $HTTP.="Content-type: application/x-www-form-urlencoded\n";

170. $HTTP.="Content-length: ".strlen($post)."\n";

171. $HTTP.="Connection: close\n";

172. if($jeton) $HTTP.="Cookie: PHPSESSID=$jeton\n";

173. $HTTP.="\n";

174. $HTTP.=$post;

176. // on envoie la requête HTTP

177. fwrite($connexion,$HTTP);

178. }//POST

180. // --------------------------------------------------------------

181. function getInfos($document){

182. // $document : document HTML

183. // on cherche soit la liste d'erreurs

184. // soit le tableau des simulations

186. // préparation du résultat

187. $impots[erreurs]=array();

188. $impots[simulations]=array();

190. // y-a-t-il une erreur ?

191. // modèle de la ligne d'erreur

192. $modErreur="|<li>(.+?)</li>|";

193. // recherche dans le document

194. if(preg_match_all($modErreur,$document,$champs,PREG_SET_ORDER)){

195. // récupération des erreurs

196. for($i=0;$i<count($champs);$i++){

197. $impots[erreurs][]=$champs[$i][1];

198. }//for

199. // fini

200. return $impots;

201. }//if

203. // modèle d'une ligne du tableau des simulations

204. // <tr><td>oui</td><td>2</td><td>200000</td><td>22504</td></tr>

205. $modSimulation="|<tr>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*</tr>|";

206. // recherche dans le document

207. if(preg_match_all($modSimulation,$document,$champs,PREG_SET_ORDER)){

208. // récupération des simulations

209. for($i=0;$i<count($champs);$i++){

210. $impots[simulations][]=array($champs[$i][1],$champs[$i][2],$champs[$i][3],$champs[$i][4]);

211. }//for

212. // retour résultat

213. return $impots;

214. }//if

216. // pas normal d'arriver là

217. return $impots;

218. }//getInfos

219. ?>

Commentaires :

- le programme commence par vérifier la validité des paramètres qu'il a reçus. Il utilise pour ce faire deux fonctions : analyseURL qui vérifie la validité de l'URL et analyseDemande qui vérifie les autres paramètres.

- le calcul de l'impôt est fait par la fonction getImpots :

getImpots

1. $impots=getImpots($urlImpots,$demande,$argv[5]);

- la fonction getImpots est déclarée comme suit :

1. // --------------------------------------------------------------

2. function getImpots($urlImpots,$demande,$jeton){

3. // $urlImpots : URL à interroger

4. // $demande : dictionnaire contenant les champs marie, enfants, salaire

5. // $jeton : un éventuel jeton de session

$urlImpots est un dictionnaire contenant les champs :

host : machine où réside le service web

port : son port de service

path : le chemin de la ressource demandée

$demande est un dictionnaire contenant les champs :

marié : oui/non : état marital

enfants : nombre d'enfants

salaire : salaire annuel

$jeton est le jeton de session.

La fonction rend comme résultat un dictionnaire ayant les champs :

erreurs : tableau de messages d'erreurs

simulations : tableau de simulations, une simulation étant elle-même un tableau de quatre éléments (marié,enfants,salaire,impôt)

jeton : le jeton de session

- une fois le résultat de getImpots obtenu, le programme affiche les résultats et se termine :

1. // on affiche le jeton de session

2. echo "Jeton de session=[$impots[jeton]]\n";

4. // des erreurs ?

5. if(count($impots[erreurs])!=0){

6. //msg d'erreurs

7. echo "Les erreurs suivantes se sont produites :\n";

8. for($i=0;$i<count($impots[erreurs]);$i++){

9. echo $impots[erreurs][$i]."\n";

10. }//for

11. }else{

12. // pas d'erreurs - affichage des simulations

13. echo "Simulations :\n";

14. for($i=0;$i<count($impots[simulations]);$i++){

15. echo "[".implode(",",$impots[simulations][$i])."]\n";

16. }//for

17. }//if

19. // fin du programme

20. exit(0);

- analysons maintenant la fonction getImpots($urlImpots,$demande,$jeton) qui doit

- créer une connexion TCP-IP sur le port $urlImpots[port] de la machine $urlImpots[host]

- envoyer au serveur web les entêtes HTTP qu'il attend notamment le jeton de session s'il y en a un

- envoyer au serveur web une requête POST avec les paramètres contenus dans le dictionnaire $demande

- analyser la réponse du serveur web pour y trouver soit une liste d'erreurs soit un tableau de simulations.

- créer une connexion TCP-IP sur le port $urlImpots[port] de la machine $urlImpots[host]

- envoyer au serveur web les entêtes HTTP qu'il attend notamment le jeton de session s'il y en a un

- envoyer au serveur web une requête POST avec les paramètres contenus dans le dictionnaire $demande

- analyser la réponse du serveur web pour y trouver soit une liste d'erreurs soit un tableau de simulations.

- l'envoi des entêtes HTTP se fait à l'aide d'une fonction POST :

1. // on envoie les entetes HTTP au serveur

2. POST($connexion,$urlImpots,$jeton,

3. array(optMarie=>$demande[marié],txtEnfants=>$demande[enfants],txtSalaire=>$demande[salaire]));

- une fois les entêtes HTTP envoyés, la fonction getImpots lit en totalité de la réponse du serveur et la met dans $document. La réponse est lue sans être analysée sauf pour la première ligne qui nous permet de savoir si le serveur web a trouvé ou non l'URL qu'on lui a demandée. En effet si l'URL a été trouvée, le serveur web répond HTTP/1.X 200 OK où X dépend de la version de HTTP utilisée.

1. // URL trouvée ?

2. if(! preg_match("/^(.+?) 200 OK\s*$/",$ligne)){

3. // URL inaccessible - retour avec erreur

4. return array(erreurs=>array("L'URL $urlImpots[path] n'a pu être trouvée"));

5. }//if

- le document est analysé à l'aide de la fonction getInfos :

1. // on analyse le document

2. $impots=getInfos($document);

Le résultat rendu est un dictionnaire à deux champs :

erreurs : liste des erreurs - peut être vide

simulations : liste des simulations - peut être vide

- ceci fait, la fonction getImpots peut rendre son résultat sous la forme d'un dictionnaire.

1. // on rend le résultat

2. return array(jeton=>$jeton,erreurs=>$impots[erreurs],simulations=>$impots[simulations]);

- analysons maintenant la fonction POST :

1. // --------------------------------------------------------------

2. function POST($connexion,$url,$jeton,$paramètres){

3. // $connexion : la connexion a serveur web

4. // $url : l'URL à interroger

5. // $paramètres : dictionnaire des paramètres à poster

7. // on prépare le POST

8. $post="";

9. while(list($paramètre,$valeur)=each($paramètres)){

10. $post.=$paramètre."=".urlencode($valeur)."&";

11. }//while

12. // on enlève le dernier caractère

13. $post=substr($post,0,-1);

15. // on prépare la requête HTTP au format HTTP/1.0

16. $HTTP="POST $url[path] HTTP/1.0\n";

17. $HTTP.="Content-type: application/x-www-form-urlencoded\n";

18. $HTTP.="Content-length: ".strlen($post)."\n";

19. $HTTP.="Connection: close\n";

20. if($jeton) $HTTP.="Cookie: PHPSESSID=$jeton\n";

21. $HTTP.="\n";

22. $HTTP.=$post;

24. // on envoie la requête HTTP

25. fwrite($connexion,$HTTP);

26. }//POST

Si par programme, nous avions eu l'occasion de demander une ressource web par un GET, nous n'avions pas encore eu l'occasion de le faire avec un POST. Le POST diffère du GET dans la façon d'envoyer des paramètres au serveur. Il doit envoyer les entêtes HTTP suivants :

1. POST chemin HTTP/1.X

2. Content-type: application/x-www-form-urlencoded

3. Content-length: N

avec

- chemin : chemin de la ressource web demandée, ici $url[path]

- HTTP/1.X : le protocole HTTP désiré. Ici on a choisi HTTP/1.0 pour avoir la réponse en un seul bloc. HTTP/1.1 autorise l'envoi de la réponse en plusieurs blocs (chunked).

- N désigne le nombre de caractères que le client s'apprête à envoyer au serveur

Les N caractères constituant les paramètres de la requête sont envoyés immédiatement derrière la ligne vide qui termine les entêtes HTTP envoyés au serveur. Ces paramètres sont sous la forme param1=val1&param2=val2&… où parami est le nom du paramètre et vali sa valeur. Les valeurs vali peuvent contenir des caractères "gênants" tels des espaces, le caractère &, le caractère =, etc.. Ces caractères doivent être remplacés par une chaîne %XX où XX est leur code hexadécimal. La fonction PHP urlencode fait ce travail. Le travail inverse est fait par urldecode.

Enfin on notera que si on a un jeton, il est envoyé avec un entête HTTP Cookie: PHPSESSID=jeton.

- il nous reste à étudier la fonction qui analyse la réponse du serveur :

1. // --------------------------------------------------------------

2. function getInfos($document){

3. // $document : document HTML

4. // on cherche soit la liste d'erreurs

5. // soit le tableau des simulations

7. // préparation du résultat

8. $impots[erreurs]=array();

9. $impots[simulations]=array();

- rappelons que les erreurs sont envoyées par le serveur sous la forme <li>message d'erreur</li>. L'expression régulière "|<li>(.+?)</li>|" doit permettre de récupérer ces informations. Nous avons ici utilisé le signe | pour délimiter l'expression régulière plutôt que le signe / car celui-ci existe aussi dans l'expression régulière elle-même. La fonction preg_match_all ($modèle, $document, $champs, PREG_SET_ORDER) permet de récupérer toutes les occurrences de $modèle trouvées dans $document. Celles-ci sont mises dans le tableau $champs. Ainsi $champs[i] représente l'occurrence n° i de $modèle dans $document. Dans $champs[$i][0] est placée la chaîne correspondant au modèle. Si celui-ci avait des parenthèses, la chaîne correspondant à la première parenthèse est placée dans $champs[$i][1], la seconde dans $champs[$i][2] et ainsi de suite. Le code pour récupérer les erreurs sera donc le suivant :

1. // y-a-t-il une erreur ?

2. // modèle de la ligne d'erreur

3. $modErreur="|<li>(.+?)</li>|";

4. // recherche dans le document

5. if(preg_match_all($modErreur,$document,$champs,PREG_SET_ORDER)){

6. // récupération des erreurs

7. for($i=0;$i<count($champs);$i++){

8. $impots[erreurs][]=$champs[$i][1];

9. }//for

10. // fini

11. return $impots;

12. }//if

IV-G - Application Impôts : Conclusion

Nous avons montré différentes versions de notre application client-serveur de calcul d'impôts :

- version 1 : le service est assuré par un ensemble de programmes PHP, le client est un navigateur. Il fait une seule simulation et n'a pas de mémoire des précédentes.

- version 2 : on ajoute quelques capacités côté navigateur en mettant des scripts javascript dans le document HTML chargé par celui-ci. Il contrôle la validité des paramètres du formulaire.

- version 3 : on permet au service de se souvenir des différentes simulations opérées par un client en gérant une session. L'interface HTML est modifiée en conséquence pour afficher celles-ci.

- version 4 : le client est désormais une application console autonome. Cela nous permet de revenir sur l'écriture de clients web programmés.

A ce point, on peut faire quelques remarques :

- les versions 1 à 3 autorisent des navigateurs sans capacité autre que celle de pouvoir exécuter des scripts javascript. On notera qu'un utilisateur a toujours la possibilité d'inhiber l'exécution de ces derniers. L'application ne fonctionnera alors que partiellement dans sa version 1 (option Effacer ne fonctionnera pas) et pas du tout dans ses versions 2 et 3 (options Effacer et Calculer ne fonctionneront pas). Il pourrait être intéressant de prévoir une version du service n'utilisant pas de scripts javascript.

Lorsqu'on écrit un service Web, il faut se demander quels types de clients on vise. Si on veut viser le plus grand nombre de clients, on écrira une application qui n'envoie aux navigateurs que du HTML (pas de javascript ni d'applet). Si on travaille au sein d'un intranet et qu'on maîtrise la configuration des postes de celui-ci on peut alors se permettre d'être plus exigeant au niveau du client.

La version 4 est un client web qui retrouve l'information dont il a besoin au sein du flux HTML envoyé par le serveur. Très souvent on ne maîtrise pas ce flux. C'est le cas lorsqu'on a écrit un client pour un service web existant sur le réseau et géré par quelqu'un d'autre. Prenon un exemple. Supposons que notre service de simulations de calcul d'impôts ait été écrit par une société X. Actuellement le service envoie les simulations dans un tableau HTML et notre client exploite ce fait pour les récupérer. Il compare ainsi chaque ligne de la réponse du serveur à l'expression régulière :

1. // modèle d'une ligne du tableau des simulations

2. // <tr><td>oui</td><td>2</td><td>200000</td><td>22504</td></tr>

3. $modSimulation="|<tr>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*</tr>|";

Supposons maintenant que le concepteur de l'application change l'apparence visuelle de la réponse en mettant les simulations non pas dans un tableau mais dans une liste sous la forme :

1. Résultat des simulations

2. <ul>

3. <li>oui,2,200000,22504

4. <li>non,2,200000,33388

5. </ul>

Dans ce cas, notre client web devra être réécrit. C'est là, la menace permanente pesant sur les clients web d'applications qu'on ne maîtrise pas soi-même. XML peut apporter une solution à ce problème :

- au lieu de générer du HTML, le service de simulations va générer du XML. Dans notre exemple, cela pourrait être

1. <simulations>

2. <entetes marie="marié" enfants="enfants" salaire="salaire" impot="impôt"/>

3. <simulation marie="oui" enfants="2" salaire="200000" impot="22504" />

4. <simulation marie="non" enfants="2" salaire="200000" impot="33388" />

5. </simulations>

- une feuille de style pourrait être associée à cette réponse indiquant aux navigateurs la forme visuelle à donner à cette réponse XML

- les clients web programmés ignoreraient cette feuille de style et récuperaient l'information directement dans le flux XML de la réponse

Si le concepteur du service souhaite modifier la présentation visuelle des résultats qu'il fournit, il modifiera la feuille de style et non le XML. Grâce à la feuille de style, les navigateurs afficheront la nouvelle forme visuelle et les clients web programmés n'auront pas eux à être modifiés.

V - XML et PHP

Dans ce chapitre, nous allons faire une introduction à l'utilisation de documents XML (eXtensible Markup Language) avec PHP. Nous le ferons dans le contexte de l'application impôts étudiée dans le chapitre précédent.

V-A - Fichiers XML et feuilles de style XSL

Considérons le fichier XML suivant qui pourrait représenter le résultat de simulations :

1. <?xml version="1.0" encoding="windows-1252"?>

2. <simulations>

3. <simulation marie="oui" enfants="2" salaire="200000" impot="22504"/>

4. <simulation marie="non" enfants="2" salaire="200000" impot="33388"/>

5. </simulations>

Si on le visualise avec IE 6, on obtient le résultat suivant :

IE6 reconnaît qu'il a affaire à un fichier XML (grâce au suffixe .xml du fichier) et le met en page d'une façon qui lui est propre. Avec Netscape on obtient une page vide. Cependant si on regarde le code source (View/Source) on a bien le fichier XML d'origine :

Pourquoi Netscape n'affiche-t-il rien ? Parce qu'il lui faut une feuille de style qui lui dise comment transformer le fichier XML en fichier HTML qu'il pourra alors afficher. Il se trouve que IE 6 a lui une feuille de style par défaut lorsque le fichier XML n'en propose pas ce qui était le cas ici.

Il existe un langage appelé XSL (eXtensible StyleSheet Language) permettant de décrire les transformations à effectuer pour passer un fichier XML en un fichier texte quelconque. XSL permet l'utilisation de nombreuses instructions et ressemble fort aux langages de programmation. Nous le détaillerons pas ici car il y faudrait plusieurs dizaines de pages. Nous allons simplement décrire deux exemples de feuilles de style XSL. La première est celle qui va transformer le fichier XML simulations.xml en code HTML. On modifie ce dernier afin qu'il désigne la feuille de style que pourront utiliser les navigateurs pour le transformer en document HTML qu'ils pourront afficher :

1. <?xml version="1.0" encoding="windows-1252"?>

2. <?xml-stylesheet type="text/xsl" href="simulations.xsl"?>

3. <simulations>

4. <simulation marie="oui" enfants="2" salaire="200000" impot="22504"/>

5. <simulation marie="non" enfants="2" salaire="200000" impot="33388"/>

6. </simulations>

La commande XML

1. <?xml-stylesheet type="text/xsl" href="simulations.xsl"?>

désigne le fichier simulations.xsl comme un feuille de style (xml-stylesheet) de type text/xsl c.a.d. un fichier texte contenant du code XSL. Cette feuille de style sera utilisée par les navigateurs pour transformer le texte XML en document HTML. Voici le résultat obtenu avec Netscape 7 lorsqu'on charge le fichier XML simulations.xml :

Lorsque nous regardons le code source du document (View/Source) nous retrouvons le document XML initial et non le document HTML affiché :

Netscape a utilisé la feuille de style simulations.xsl pour transformer le document XML ci-dessus en document HTML affichable. Il est maintenant temps de regarder le contenu de cette feuille de style :

1. <?xml version="1.0" encoding="windows-1252"?>

2. <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

3. <xsl:output method="html" indent="yes"/>

5. <xsl:template match="/">

6. <html>

7. <head>

8. <title>Simulations de calculs d'impôts</title>

9. </head>

10. <body>

11. <center>

12. <h3>Simulations de calculs d'impôts</h3>

13. <hr/>

14. <table border="1">

15. <th>marié</th><th>enfants</th><th>salaire</th><th>impôt</th>

16. <xsl:apply-templates select="/simulations/simulation"/>

17. </table>

18. </center>

19. </body>

20. </html>

21. </xsl:template>

24. <xsl:template match="simulation">

25. <tr>

26. <td><xsl:value-of select="@marie"/></td>

27. <td><xsl:value-of select="@enfants"/></td>

28. <td><xsl:value-of select="@salaire"/></td>

29. <td><xsl:value-of select="@impot"/></td>

30. </tr>

31. </xsl:template>

32. </xsl:stylesheet>

- une feuille de style XSL est un fichier XML et en suit donc les règles. Il doit être en autres choses "bien formé" c'est-à-dire que toute balise ouverte doit être fermée.

- le fichier commence par deux commandes XML qu'on pourra garder dans toute feuille de style XSL sous Windows :

1. <?xml version="1.0" encoding="windows-1252"?>

2. <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

L'attribut encoding="windows-1252" permet d'utiliser les caractères accentués dans la feuille de style.

- La balise <xsl:output method="html" indent="yes"/> indique à l'interpréteur XSL qu'on veut produire du HTML "indenté".

- La balise <xsl:template match="élément"> sert à définir l'élément du document XML sur lequel vont s'appliquer les instructions que l'on va trouver entre <xsl:template …> et </xsl:template>.

1. <xsl:template match="/">

2. ................

3. </xsl:template>

Dans l'exemple ci-dessus l'élément "/" désigne la racine du document. Cela signifie que dès que le début du document XML va être rencontré, les commandes XSL situées entre les deux balises vont être exécutées.

- Tout ce qui n'est pas balise XSL est mis tel quel dans le flux de sortie. Les balises XSL elles sont exécutées. Certaines d'entre-elles produisent un résultat dans le flux de sortie. Etudions l'exemple suivant :

1. <xsl:template match="/">

2. <html>

3. <head>

4. <title>Simulations de calculs d'impôts</title>

5. </head>

6. <body>

7. <center>

8. <h3>Simulations de calculs d'impôts</h3>

9. <hr/>

10. <table border="1">

11. <th>marié</th><th>enfants</th><th>salaire</th><th>impôt</th>

12. <xsl:apply-templates select="/simulations/simulation"/>

13. </table>

14. </center>

15. </body>

16. </html>

17. </xsl:template>

Rappelons que le document XML analysé est le suivant :

1. <?xml version="1.0" encoding="windows-1252"?>

2. <simulations>

3. <simulation marie="oui" enfants="2" salaire="200000" impot="22504"/>

4. <simulation marie="non" enfants="2" salaire="200000" impot="33388"/>

5. </simulations>

Dès le début du document XML analysé (match="/"), l'interpréteur XSL va produire en sortie le texte

1. <html>

2. <head>

3. <title>Simulations de calculs d'impôts</title>

4. </head>

5. <body>

6. <center>

7. <h3>Simulations de calculs d'impôts</h3>

8. <hr>

9. <table border="1">

10. <th>marié</th><th>enfants</th><th>salaire</th><th>impôt</th>

On remarquera que dans le texte initial on avait <hr/> et non pas <hr>. Dans le texte initial on ne pouvait pas écrire <hr> qui si elle est une balise HTML valide est une balise XML invalide. Or nous avons affaire ici à un texte XML qui doit être "bien formé", c.a.d que toute balise doit être fermée. On écrit donc <hr/> et parce qu'on a écrit <xsl:output text="html …> l'interpréteur transformera le texte <hr/> en <hr>. Derrière ce texte, viendra ensuite le texte produit par la commande XSL :

1. <xsl:apply-templates select="/simulations/simulation"/>

Nous verrons ultérieurement quel est ce texte. Enfin l'interpréteur ajoutera le texte :

1. </table>

2. </center>

3. </body>

4. </html>

La commande <xsl:apply-templates select="/simulations/simulation"/> demande qu'on exécute le "template" (modèle) de l'élément /simulations/simulation. Elle sera exécutée à chaque fois que l'interpréteur XSL rencontrera dans le texte XML analysé une balise <simulation>..</simulations> ou <simulation/> à l'intérieur d'une balise <simulations>..</simulations>. A la rencontre d'une telle balise, l'interpréteur exécutera les instructions du modèle suivant :

1. <xsl:template match="simulation">

2. <tr>

3. <td><xsl:value-of select="@marie"/></td>

4. <td><xsl:value-of select="@enfants"/></td>

5. <td><xsl:value-of select="@salaire"/></td>

6. <td><xsl:value-of select="@impot"/></td>

7. </tr>

8. </xsl:template>

Prenons les lignes XML suivantes :

1. <simulations>

2. <simulation marie="oui" enfants="2" salaire="200000" impot="22504"/>

La ligne <simulation ..> correspond au modèle de l'instruction XSL <xsl:apply-templates select="/simulations/simulation>". L'interpréteur XSL va donc chercher à lui appliquer les instructions qui correspondent à ce modèle. Il va trouver le modèle <xsl:template match="simulation"> et va l'exécuter. Rappelons que ce qui n'est pas une commande XSL est repris tel quel par l'interpréteur XSL et que les commandes XSL sont elles remplacées par le résultat de leur exécution. L'instruction XSL <xsl:value-of select="@champ"/> est ainsi remplacé par la valeur de l'attribut "champ" du nœud analysé (ici un nœud <simulation>). L'analyse de la ligne XML précédente va produire en sortie le résultat suivant :

XSL

sortie

<tr><td>

<tr><td>

<xsl:value-of select="@marie"/>

oui

</td><td>

</td><td>

<xsl:value-of select="@enfants"/>

2

</td><td>

</td><td>

<xsl:value-of select="@salaire"/>

200000

</td><td>

</td><td>

<xsl:value-of select="@impot"/>

22504

</td></tr>

</td></tr>

Au total, la ligne XML

1. <simulation marie="oui" enfants="2" salaire="200000" impot="22504"/>

va être transformée en ligne HTML :

1. <tr><td>oui</td><td>2</td><td>200000</td><td>22504</td></tr>

Toutes ces explications sont un peu rudimentaires mais il devrait apparaître maintenant au lecteur que le texte XML suivant :

1. <?xml version="1.0" encoding="windows-1252"?>

2. <?xml-stylesheet type="text/xsl" href="simulations.xsl"?>

3. <simulations>

4. <simulation marie="oui" enfants="2" salaire="200000" impot="22504"/>

5. <simulation marie="non" enfants="2" salaire="200000" impot="33388"/>

6. </simulations>

accompagné de la feuille de style XSL simulations.xsl suivante :

1. <?xml version="1.0" encoding="windows-1252"?>

2. <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

3. <xsl:output method="html" indent="yes"/>

5. <xsl:template match="/">

6. <html>

7. <head>

8. <title>Simulations de calculs d'impôts</title>

9. </head>

10. <body>

11. <center>

12. <h3>Simulations de calculs d'impôts</h3>

13. <hr/>

14. <table border="1">

15. <th>marié</th><th>enfants</th><th>salaire</th><th>impôt</th>

16. <xsl:apply-templates select="/simulations/simulation"/>

17. </table>

18. </center>

19. </body>

20. </html>

21. </xsl:template>

24. <xsl:template match="simulation">

25. <tr>

26. <td><xsl:value-of select="@marie"/></td>

27. <td><xsl:value-of select="@enfants"/></td>

28. <td><xsl:value-of select="@salaire"/></td>

29. <td><xsl:value-of select="@impot"/></td>

30. </tr>

31. </xsl:template>

32. </xsl:stylesheet>

produit le texte HTML suivant :

1. <html>

2. <head>

3. <title>Simulations de calculs d'impôts</title>

4. </head>

5. <body>

6. <center>

7. <h3>Simulations de calculs d'impots</h3>

8. <hr>

9. <table border="1">

10. <th>marié</th><th>enfants</th><th>salaire</th><th>impôt</th>

11. <tr>

12. <td>oui</td><td>2</td><td>200000</td><td>22504</td>

13. </tr>

14. <tr>

15. <td>non</td><td>2</td><td>200000</td><td>33388</td>

16. </tr>

17. </table>

18. </center>

19. </body>

20. </html>

Le fichier XML simulations.xml suivant

1. <?xml version="1.0" encoding="windows-1252"?>

2. <?xml-stylesheet type="text/xsl" href="simulations.xsl"?>

3. <simulations>

4. <simulation marie="oui" enfants="2" salaire="200000" impot="22504"/>

5. <simulation marie="non" enfants="2" salaire="200000" impot="33388"/>

6. </simulations>

lu par un navigateur récent (ici Netscape 7) est alors affiché comme suit :

V-B - Application impôts : version 5

V-B-1 - Les fichiers XML et feuilles de style XSL de l'application impôts

Revenons à notre point de départ qui était l'application web impôts et rappelons que nous voulons la modifier afin que la réponse faite aux clients soit une réponse au format XML plutôt qu'une réponse HTML. Cette réponse HTML sera accompagnée d'une feuille de style XSL afin que les navigateurs puissent l'afficher. Dans le paragraphe précédent, nous avons présenté :

1. le fichier simulations.xml qui est le prototype d'une réponse XML comportant des simulations de calculs d'impôts

2. le fichier simulations.xsl qui sera la feuille de style XSL qui accompagnera cette réponse XML

Il nous faut prévoir également le cas de la réponse avec des erreurs. Le prototype de la réponse XML dans ce cas sera le fichier erreurs.xml suivant :

1. <?xml version="1.0" encoding="windows-1252"?>

2. <?xml-stylesheet type="text/xsl" href="erreurs.xsl"?>

3. <erreurs>

4. <erreur>erreur 1</erreur>

5. <erreur>erreur 2</erreur>

6. </erreurs>

La feuille de style erreurs.xsl permettant d'afficher ce document XML dans un navigateur sera la suivante :

1. <?xml version="1.0" encoding="windows-1252"?>

2. <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

3. <xsl:output method="html" indent="yes"/>

5. <xsl:template match="/">

6. <html>

7. <head>

8. <title>Simulations de calculs d'impôts</title>

9. </head>

10. <body>

11. <center>

12. <h3>Simulations de calculs d'impôts</h3>

13. </center>

14. <hr/>

15. Les erreurs suivantes se sont produites :

16. <ul>

17. <xsl:apply-templates select="/erreurs/erreur"/>

18. </ul>

19. </body>

20. </html>

21. </xsl:template>

23. <xsl:template match="erreur">

24. <li><xsl:value-of select="."/></li>

25. </xsl:template>

26. </xsl:stylesheet>

Cette feuille de style introduit une commande XSL non encore rencontrée : <xsl:value-of select="."/>. Cette commande produit en sortie la valeur du nœud analysé, ici un nœud <erreur>texte</erreur>. La valeur de ce nœud est le texte compris entre les deux balises d'ouverture et de fermeture, ici texte.

Le code erreurs.xml est transformé par la feuille de style erreurs.xsl en document HTML suivant :

1. <html>

2. <head>

3. <title>Simulations de calculs d'impots</title>

4. </head>

5. <body>

6. <center>

7. <h3>Simulations de calculs d'impots</h3>

8. </center>

9. <hr>

10. Les erreurs suivantes se sont produites :

11. <ul>

12. <li>erreur 1</li>

13. <li>erreur 2</li>

14. </ul>

15. </body>

16. </html>

Le fichier erreurs.xml accompagné de sa feuille de style est affiché par un navigateur de la façon suivante :

V-B-2 - L'application xmlsimulations

Nous créons un fichier xmlsimulations.html que nous mettons dans le répertoire de l'application impots. La page visualisée est la suivante :

Ce document HTML est un document statique. Son code est le suivant :

1. <html>

2. <head>

3. <title>impots</title>

4. <script language="JavaScript" type="text/javascript">

5. function effacer(){

6. // raz du formulaire

7. with(document.frmImpots){

8. optMarie[0].checked=false;

9. optMarie[1].checked=true;

10. txtEnfants.value="";

11. txtSalaire.value="";

12. txtImpots.value="";

13. }//with

14. }//effacer

16. function calculer(){

17. // vérification des paramètres avant de les envoyer au serveur

18. with(document.frmImpots){

19. //nbre d'enfants

20. champs=/^\s*(\d+)\s*$/.exec(txtEnfants.value);

21. if(champs==null){

22. // le modéle n'est pas vérifié

23. alert("Le nombre d'enfants n'a pas été donné ou est incorrect");

24. nbEnfants.focus();

25. return;

26. }//if

27. //salaire

28. champs=/^\s*(\d+)\s*$/.exec(txtSalaire.value);

29. if(champs==null){

30. // le modéle n'est pas vérifié

31. alert("Le salaire n'a pas été donné ou est incorrect");

32. salaire.focus();

33. return;

34. }//if

35. // c'est bon - on envoie

36. submit();

37. }//with

38. }//calculer

39. </script>

40. </head>

42. <body background="/poly/impots/7/images/standard.jpg">

43. <center>

44. Calcul d'impôts

45. <hr>

46. <form name="frmImpots" action="/poly/impots/7/xmlsimulations.php" method="POST">

47. <table>

48. <tr>

49. <td>Etes-vous marié(e)</td>

50. <td>

51. <input type="radio" name="optMarie" value="oui">oui

52. <input type="radio" name="optMarie" value="non" checked>non

53. </td>

54. </tr>

55. <tr>

56. <td>Nombre d'enfants</td>

57. <td><input type="text" size="3" name="txtEnfants" value=""></td>

58. </tr>

59. <tr>

60. <td>Salaire annuel</td>

61. <td><input type="text" size="10" name="txtSalaire" value=""></td>

62. </tr>

63. <tr></tr>

64. <tr>

65. <td><input type="button" value="Calculer" onclick="calculer()"></td>

66. <td><input type="button" value="Effacer" onclick="effacer()"></td>

67. </tr>

68. </table>

69. </form>

70. </center>

71. </body>

72. </html>

On notera que les données du formulaire sont postées à l'URL /poly/impots/7/xmlsimulations.php. Le code de l'application xmlsimulations.php est très analogue à celui de l'application impots.php. Le lecteur est invité à revoir celui-ci. Rappelons le code d'entrée  :

1. <?php

2. // traite le formulaire des impôts

4. // bibilothèques

5. include "ImpotsDSN.php";

7. // démarrage session

8. session_start();

10. // configuration de l'application

11. ini_set("register_globals","off");

12. ini_set("display_errors","off");

13. $formulaireImpots="impots_form.php";

14. $erreursImpots="impots_erreurs.php";

15. $bdImpots=array(dsn=>"mysql-dbimpots",user=>admimpots,pwd=>mdpimpots,

16. table=>impots,limites=>limites,coeffR=>coeffR,coeffN=>coeffN);

18. // on récupère les paramètres de la session

19. $session=$_SESSION["session"];

20. // session valide ?

21. if(! isset($session) || ! isset($session[objImpots]) || ! isset($session[simulations])){

22. // on démarre une nouvelle session

23. $session=array(objImpots=>new ImpotsDSN($bdImpots),simulations=>array());

24. // des erreurs ?

25. if(count($session[objImpots]->erreurs)!=0){

26. $requête=array(erreurs=>$session[objImpots]->erreurs);

27. // affichage page d'erreurs

28. include $erreursImpots;

29. // fin

30. $session=array();

31. terminerSession($session);

32. }//if

33. }//if

35. // on récupère les paramètres de l'échange en cours

36. $requête[marié]=$_POST["optMarie"];

37. $requête[enfants]=$_POST["txtEnfants"];

38. $requête[salaire]=$_POST["txtSalaire"];

40. // a-t-on tous les paramètres ?

41. if(! isset($requête[marié]) || ! isset($requête[enfants]) || ! isset($requête[salaire])){

42. // affichage formulaire vide

43. $requête=array(chkoui=>"",chknon=>"checked",enfants=>"",salaire=>"",impots=>"",

44. erreurs=>array(),simulations=>array());

45. include $formulaireImpots;

46. // fin

47. terminerSession($session);

48. }//if

50. // vérification paramètres

51. $requête=vérifier($requête);

53. // des erreurs ?

54. if(count($requête[erreurs])!=0){

55. // affichage formulaire

56. include "$formulaireImpots";

57. // fin

58. terminerSession($session);

59. }//if

61. // calcul de l'impôt à payer

62. $requête[impots]=$session[objImpots]->calculer(array(marié=>$requête[marié],

63. enfants=>$requête[enfants],salaire=>$requête[salaire]));

65. // une simulation de plus

66. $session[simulations][]=array($requête[marié],$requête[enfants],$requête[salaire],$requête[impots]);

67. $requête[simulations]=$session[simulations];

69. // affichage formulaire

70. include "$formulaireImpots";

72. // fin

73. terminerSession($session);

74. ...

Les pages HTML étaient affichées par les lignes include "…". On veut ici générer du XML et non du HTML. Il suffit d'écrire deux nouvelles applications impots_erreurs.php et impots_simulations.php pour qu'elle génèrent du XML au lieu du HTML. Le reste de l'application ne change pas. Le code devient alors le suivant :

1. <?php

2. // traite le formulaire des impôts

4. // bibilothèques

5. include "ImpotsDSN.php";

7. // démarrage session

8. session_start();

10. // configuration de l'application

11. ini_set("register_globals","off");

12. ini_set("display_errors","off");

13. $formulaireImpots="xmlsimulations.html";

14. $erreursImpots="impots_erreurs.php";

15. $simulationsImpots="impots_simulations.php";

16. $bdImpots=array(dsn=>"mysql-dbimpots",user=>admimpots,pwd=>mdpimpots,

17. table=>impots,limites=>limites,coeffR=>coeffR,coeffN=>coeffN);

19. // on récupère les paramètres de la session

20. $session=$_SESSION["session"];

21. // session valide ?

22. if(! isset($session) || ! isset($session[objImpots]) || ! isset($session[simulations])){

23. // on démarre une nouvelle session

24. $session=array(objImpots=>new ImpotsDSN($bdImpots),simulations=>array());

25. // des erreurs ?

26. if(count($session[objImpots]->erreurs)!=0){

27. $requête=array(erreurs=>$session[objImpots]->erreurs);

28. // affichage page d'erreurs au format XML

29. header("Content-type: text/xml");

30. include $erreursImpots;

31. // fin

32. $session=array();

33. terminerSession($session);

34. }//if

35. }//if

37. // on récupère les paramètres de l'échange en cours

38. $requête[marié]=$_POST["optMarie"];

39. $requête[enfants]=$_POST["txtEnfants"];

40. $requête[salaire]=$_POST["txtSalaire"];

42. // a-t-on tous les paramètres ?

43. if(! isset($requête[marié]) || ! isset($requête[enfants]) || ! isset($requête[salaire])){

44. // affichage formulaire vide

45. include $formulaireImpots;

46. // fin

47. terminerSession($session);

48. }//if

50. // vérification paramètres

51. $requête=vérifier($requête);

53. // des erreurs ?

54. if(count($requête[erreurs])!=0){

55. // affichage erreurs au format XML

56. header("Content-type: text/xml");

57. include "$erreursImpots";

58. // fin

59. terminerSession($session);

60. }//if

62. // calcul de l'impôt à payer

63. $requête[impots]=$session[objImpots]->calculer(array(marié=>$requête[marié],

64. enfants=>$requête[enfants],salaire=>$requête[salaire]));

66. // une simulation de plus

67. $session[simulations][]=array($requête[marié],$requête[enfants],$requête[salaire],$requête[impots]);

68. $requête[simulations]=$session[simulations];

70. // affichage simulations au format XML

71. header("Content-type: text/xml");

72. include "$simulationsImpots";

74. // fin

75. terminerSession($session);

Nous avons précédemment présenté et étudié les deux types de réponse XML à fournir ainsi que les feuilles de style qui doivent les accompagner. Le code de l'application impots_simulations.php est le suivant :

1. <?php

2. // génère le code XML de la page de simulations de l'application impots

4. // qqs constantes

5. $xslSimulations="simulations.xsl";

7. // entêtes XML

8. echo "<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?>\n";

9. echo "<?xml-stylesheet type=\"text/xsl\" href=\"$xslSimulations\" ?>\n";

10. // les simulations

11. echo "<simulations>\n";

12. for ($i=0;$i<count($requête[simulations]);$i++){

13. // simulation $i

14. echo "<simulation marie=\"".$requête[simulations][$i][0]."\" ".

15. "enfants=\"".$requête[simulations][$i][1]."\" ".

16. "salaire=\"".$requête[simulations][$i][2]."\" ".

17. "impot=\"".$requête[simulations][$i][3]."\" />\n";

18. }//for

19. echo "</simulations>\n";

20. ?>

Ce code permet à partir du dictionnaire $requête de générer du code XML analogue au suivant :

1. <?xml version="1.0" encoding="ISO-8859-1" ?>

2. <?xml-stylesheet type="text/xsl" href="simulations.xsl" ?>

3. <simulations>

4. <simulation marie="non" enfants="3" salaire="200000" impot="22504" />

5. <simulation marie="oui" enfants="3" salaire="200000" impot="16400" />

6. <simulation marie="oui" enfants="2" salaire="200000" impot="22504" />

7. </simulations>

La feuille de style simulations.xsl transformera ce code XML en code HTML.

Le code de l'application impots_erreurs.php est le suivant :

impots_erreurs.php

1. <?php

2. // génère le code XML de la page d'erreurs de l'application impots

4. // qqs constantes

5. $xslErreurs="erreurs.xsl";

7. // entêtes XML

8. echo "<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?>\n";

9. echo "<?xml-stylesheet type=\"text/xsl\" href=\"$xslErreurs\" ?>\n";

10. // les erreurs

11. echo "<erreurs>\n";

12. for ($i=0;$i<count($requête[erreurs]);$i++){

13. // erreur $i

14. echo "<erreur>".$requête[erreurs][$i]."</erreur>";

15. }//for

16. echo "</erreurs>\n";

17. ?>

Ce code permet à partir du dictionnaire $requête de générer du code XML analogue au suivant :

1. <?xml version="1.0" encoding="ISO-8859-1" ?>

2. <?xml-stylesheet type="text/xsl" href="erreurs.xsl" ?>

3. <erreurs>

4. <erreur>Impossible d'ouvrir la base DSN [mysql-dbimpots] (S1000)</erreur>

5. </erreurs>

La feuille de style erreurs.xsl transformera ce code XML en code HTML.

Voyons un premier exemple :

Le SGBD MySQL n'est pas lancé. On reçoit alors la réponse suivante :

Si on regarde le code source reçu par le navigateur, on a la chose suivante :

1. <?xml version="1.0" encoding="ISO-8859-1" ?>

2. <?xml-stylesheet type="text/xsl" href="erreurs.xsl" ?>

3. <erreurs>

4. <erreur>Impossible d'ouvrir la base DSN [mysql-dbimpots] (S1000)</erreur>

5. </erreurs>

Maintenant, nous lançons le SGBD MySQL et faisons différentes simulations successives. Nous obtenons la réponse suivante :

Le code reçu par le navigateur est le suivant :

1. <?xml version="1.0" encoding="ISO-8859-1" ?>

2. <?xml-stylesheet type="text/xsl" href="simulations.xsl" ?>

3. <simulations>

4. <simulation marie="oui" enfants="2" salaire="200000" impot="22504" />

5. <simulation marie="non" enfants="2" salaire="200000" impot="33388" />

6. <simulation marie="non" enfants="3" salaire="200000" impot="22504" />

7. <simulation marie="oui" enfants="3" salaire="200000" impot="16400" />

8. </simulations>

On remarquera que notre nouvelle application est plus simple à maintenir que la précédente. Une partie du travail a été transférée sur les feuilles de style XSL. L'intérêt de cette nouvelle répartition des tâches est qu'une fois le format XML des réponses a été fixé, le développement des feuilles de style est indépendant de celui de l'application.

V-C - Analyse d'un document XML en PHP

La prochaine version de notre application impots va être un client programmé de l'application précédente xmlsimulations. Notre client va donc recevoir du code XML qu'il devra analyser pour en retirer les informations qui l'intéresse. Nous allons faire ici une pause dans nos différentes versions et apprendre comment on peut analyser un document XML en PHP. Nous le ferons à partir de l'exemple suivant :

1. dos>e:\php43\php.exe xmlParser.php

2. syntaxe : xmlParser.php fichierXML

L'application xmlParser.php admet un

paramètre : l'URI (Uniform Resource Identifier) du document XML à analyser. Dans notre exemple, cette URI sera simplement le nom d'un fichier XML placé dans le répertoire de l'application xmlParser.php. Considérons deux exemples d'exécution. Dans le premier exemple, le fichier XML analysé est le fichier erreurs.xml suivant :

1. <?xml version="1.0" encoding="ISO-8859-1"?>

2. <?xml-stylesheet type="text/xsl" href="erreurs.xsl"?>

3. <erreurs>

4. <erreur>erreur 1</erreur>

5. <erreur>erreur 2</erreur>

6. </erreurs>

L'analyse donne les résultats suivants :

1. dos>e:\php43\php.exe xmlParser.php erreurs.xml

2. ERREURS

3. ERREUR

4. [erreur 1]

5. /ERREUR

6. ERREUR

7. [erreur 2]

8. /ERREUR

9. /ERREURS

Nous n'avions pas encore écrit ce que faisait l'application xmlParser.php mais l'on voit ici qu'elle affiche la structure du document XML analysé. Le second exemple analyse le fichier XML simulations.xml suivant :

1. <?xml version="1.0" encoding="ISO-8859-1"?>

2. <?xml-stylesheet type="text/xsl" href="simulations.xsl"?>

3. <simulations>

4. <simulation marie="oui" enfants="2" salaire="200000" impot="22504"/>

5. <simulation marie="non" enfants="2" salaire="200000" impot="33388"/>

6. </simulations>

L'analyse donne les résultats suivants :

1. dos>e:\php43\php.exe xmlParser.php simulations.xml

2. SIMULATIONS

3. SIMULATION,(MARIE,oui) (ENFANTS,2) (SALAIRE,200000) (IMPOT,22504)

4. /SIMULATION

5. SIMULATION,(MARIE,non) (ENFANTS,2) (SALAIRE,200000) (IMPOT,33388)

6. /SIMULATION

7. /SIMULATIONS

L'application xmlParser.php contient tout ce dont nous avons besoin dans notre application impots puisqu'elle a été capable de retrouver soit les erreurs soit les simulations que pourrait envoyer le serveur web. Examinons son code :

1. <?

2. // syntaxe $0 fichierXML

3. // affiche la structure et le contenu du fichier fichierXML

5. // vérification appel

6. if(count($argv)!=2){

7. // msg d'erreur

8. fwrite(STDERR,"syntaxe : $argv[0] fichierXML\n");

9. // fin

10. exit(1);

11. }//if

13. // initialisations

14. $file=$argv[1];       // le fichier xml

15. $depth=0;               // niveau d'indentation=profondeur dans l'arborescence

17. // le programme

19. // on crée un objet d'analyse de texte xml

20. $xml_parser=xml_parser_create();

22. // on indique quelles fonctions exécuter en début et fin de balise

23. xml_set_element_handler($xml_parser,"startElement","endElement");

25. // on indique quelle fonction exécuter lors de la rencontre de données

26. xml_set_character_data_handler($xml_parser,"afficheData");

28. // ouverture du fichier xml en lecture

29. if (! ($fp=@fopen($file,"r"))){

30. fwrite(STDERR,"impossible d'ouvrir le fichier xml $file\n");

31. exit(2);

32. }//if

33. // exploitation du fichier xml par blocs de 4096 octets

34. while($data=fread($fp,4096)){

35. // analyse des données lues

36. if (! xml_parse($xml_parser,$data,feof($fp))){

37. // il s'est produit une erreur

38. fprintf(STDERR,"erreur XML : %s à la ligne %d\n",

39. xml_error_string(xml_get_error_code($xml_parser)),

40. xml_get_current_line_number($xml_parser));

41. // fin

42. exit(3);

43. }//if

44. }//while

45. // le fichier a été exploré

46. // on libère les ressources occupées par l'analyseur xml

47. xml_parser_free($xml_parser);

48. // fin

49. exit(0);

51. // -----------------------------------------------------------

52. // fonction appelée lors de la rencontre d'une balise de début

53. function startElement($parser,$name,$attributs){

54. global $depth;

55. // une suite d'espaces (indentation)

56. for($i=0;$i<$depth;$i++){

57. print " ";

58. }//for

59. // attributs

60. $précisions="";

61. while(list($attrib,$valeur)=each($attributs)){

62. $précisions.="($attrib,$valeur) ";

63. }

64. // on affiche le nom de la balise et les éventuels attributs

65. if($précisions)

66. print "$name,$précisions\n";

67. else print "$name\n";

68. // un niveau d'arborescence en plus

69. $depth++;

70. }//startElement

72. // -----------------------------------------------------------

73. // la fonction appelée lors de la rencontre d'une balise de fin

74. function endElement($parser,$name){

75. // fin de balise

76. // niveau d'indentation

77. global $depth;

78. $depth--;

79. // une suite d'espaces (indentation)

80. for($i=0;$i<$depth;$i++){

81. echo " ";

82. }//for

83. // le nom de la balise

84. echo "/$name\n";

85. }//endElement

87. // -----------------------------------------------------------

88. // la fonction d'affichage des données

89. function afficheData($parser,$data){

90. // niveau d'indentation

91. global $depth;

92. // les données sont affichées

93. $data=trim($data);

94. if($data!=""){

95. // une suite d'espaces (indentation)

96. for($i=0;$i<$depth;$i++){

97. echo " ";

98. }//for

99. echo "[$data]\n";

100. }//if

101. }//afficheData

102. ?>

Etudions le code se rapportant à XML. Pour analyser un document XML, notre application a besoin d'un analyseur de code XML appelé habituellement un "parseur".

1. // on crée un objet d'analyse de texte xml

2. $xml_parser=xml_parser_create();

Lorsque le parseur va analyser le document XML, il va émettre des événements tels que : j'ai rencontré le début du document, le début d'une balise, un attribut de balise, le contenu d'une balise, la fin d'une balise, la fin du document… Il transmet ces événements à des méthodes qu'on doit lui désigner :

1. // on indique quelles fonctions exécuter en début et fin de balise

2. xml_set_element_handler($xml_parser,"startElement","endElement");

3. // on indique quelle fonction exécuter lors de la rencontre de données

4. xml_set_character_data_handler($xml_parser,"afficheData");

événement émis par le parseur

méthode de traitement

début d'un élément :<balise>

function startElement($parser,$name,$attributs)
$parser : le parseur du document
$name : nom de l'élément analysé. Si l'élément rencontré est <simulations>, on aura name="simulations".
$attributs : liste des attributs de la balise sous la forme (ATTRIBUT,valeur) où ATTRIBUT est le nom en majuscules de la balise.

valeur d'un élément :
<balise>valeur</balise>

function afficheData($parser,$data)
$parser : le parseur du document
$data : les données de la balise

fin d'un élément : </balise> ou <balise …/>

function endElement($parser,$name)
les paramètres sont ceux de la méthode startElement.

La fonction startElement permet de récupérer les attributs de l'élément grâce au paramètre $attributs. Celui-ci est un dictionnaire des attributs de la balise. Ainsi si on a la balise suivante :

1. <simulation marie="oui" enfants="2" salaire="200000" impot="22504"/>

le dictionnaire $attributs sera le suivant : array(marie=>oui,enfants=>2,salaire=>200000,impot=>22504)

Une fois définis le parseur, et les méthodes précédentes, l'analyse d'un document se fait la fonction xml_parse :

1. // exploitation du fichier xml par blocs de 4096 octets

2. while($data=fread($fp,4096)){

3. // analyse des données lues

4. if (! xml_parse($xml_parser,$data,feof($fp))){

5. ...

6. }//if

7. }//while

analyse d'un
document $doc

function xml_parse($parser,$doc,$fin)
le parseur $parser analyse le document $doc. $doc peut être un morceau d'un document plus important. Le paramètre $fin indique si c'est le dernier morceau (true) ou non (false). Lors de l'analyse du document $doc, les fonctions définies par xml_set_element_handler sont appelées à chaque début et fin de balise. La fonction définie par xml_set_character_data_handler est elle appelée à chaque fois que le contenu d'une balise a été obtenu.

Pendant l'analyse du document XML, il peut se produire des erreurs notamment si le document XML est "mal formé" par exemple lors de l'oubli de balises de fermeture. Dans ce cas, la fonction xml_parse rend une valeur évaluée à "faux" :

1. if (! xml_parse($xml_parser,$data,feof($fp))){

2. // il s'est produit une erreur

3. fprintf(STDERR,"erreur XML : %s à la ligne %d\n",

4. xml_error_string(xml_get_error_code($xml_parser)),

5. xml_get_current_line_number($xml_parser));

6. // fin

7. exit(3);

8. }//if

gestion d'erreurs

function xml_get_error_code($parser)
rend le code de la dernière erreur survenue
function xml_ error_string($code)
rend le message d'erreur associé au code passé en paramètre
function xml_ get_current_line($parser)
rend le n° de la ligne du document XML en cours d'analyse par le parseur

Lorsque le document a été analysé, on libère les ressources attribuées au parseur :

1. // on libère les ressources occupées par l'analyseur xml

2. xml_parser_free($xml_parser);

libération des
ressources attribuées
au parseur $parser

function xml_free($parser)

Une fois ceci expliqué, le programme précédent accompagné des exemples d'exécution se comprend de lui-même.

V-D - Application impôts : version 6

Nous avons maintenant tous les éléments pour écrire un clients programmé pour notre service d'impôts qui délivre du XML. Nous reprenons la version 4 de notre application pour faire le client et nous gardons la version 5 pour ce qui est du serveur. Dans cette application client-serveur :

- le service de simulations du calcul d'impôts est fait par l'application xmlsimulations.php. La réponse du serveur est donc au format XML comme nous l'avons vu dans la version 5.

- le client n'est plus un navigateur mais un client php autonome. Son interface console est celle de la version 4.

Le lecteur est invité à relire le code de l'application cltImpots.php qui était le client programmé de la version 4. Celui-ci recevait un document $document du serveur. Celui-ci était alors un document HTML. C'est maintenant un document XML. Le document HTML $document était analysé par la fonction suivante :

1. function getInfos($document){

2. // $document : document HTML

3. // on cherche soit la liste d'erreurs

4. // soit le tableau des simulations

6. // préparation du résultat

7. $impots[erreurs]=array();

8. $impots[simulations]=array();

10. ........

11. return $impots;

12. }//getInfos

La fonction recevait le document HTML $document, l'analysait et rendait un dictionnaire $impots à deux attributs :

- erreurs : un tableau d'erreurs

- simulations : un tableau de simulations, chaque simulation étant elle-même un tableau à quatre éléments (marie,enfants,salaire,impot).

L'application cltImpots.php devient maintenant cltXmlSimulations.php. Seule la partie qui traite le document reçu du serveur doit changer pour tenir compte du fait que c'est maintenant un document XML. La fonction getInfos devient alors la suivante :

1. // --------------------------------------------------------------

2. function getInfos($document){

3. // $document : document XML

4. // on cherche soit la liste d'erreurs

5. // soit le tableau des simulations

7. global $impots,$balises;

9. // préparation du résultat

10. $impots[erreurs]=array();

11. $impots[simulations]=array();

12. // balises en cours

13. $balises=array();

15. // on crée un objet d'analyse de texte xml

16. $xml_parser=xml_parser_create();

17. // on indique quelles fonctions exécuter en début et fin de balise

18. xml_set_element_handler($xml_parser,"startElement","endElement");

19. // on indique quelle fonction exécuter lors de la rencontre de données

20. xml_set_character_data_handler($xml_parser,"getData");

21. // on fait l'analyse de $document

22. xml_parse($xml_parser,$document);

23. // on libère les ressources occupées par l'analyseur xml

24. xml_parser_free($xml_parser);

25. // fin

26. return $impots;

27. }//getInfos

29. // -----------------------------------------------------------

30. // fonction appelée lors de la rencontre d'une balise de début

31. function startElement($parser,$name,$attributs){

32. global $impots,$balise,$balises,$contenu;

34. // on note le nom de la balise et son contenu

35. $balise=strtolower($name);

36. $contenu="";

37. // on l'ajoute à la pile des balises

38. array_push($balises,$balise);

39. // s'agit-il d'une simulation ?

40. if($balise=="simulation"){

41. // on note les attributs de la simulation

42. $impots[simulations][]=array($attributs[MARIE],$attributs[ENFANTS],$attributs[SALAIRE],$attributs[IMPOT]);

43. }//if

44. }//startElement

46. // -----------------------------------------------------------

47. // la fonction appelée lors de la rencontre d'une balise de fin

48. function endElement($parser,$name){

49. // on récupère la balise courante

50. global $impots,$balises,$contenu;

51. $balise=array_pop($balises);

52. // s'agit-il d'une balise d'erreur ?

53. if($balise=="erreur"){

54. // une erreur de plus

55. $impots[erreurs][]=trim($contenu);

56. }//if

57. }//endElement

59. // -----------------------------------------------------------

60. // la fonction de traitement du contenu d'une balise

61. function getData($parser,$data){

62. // données globales

63. global $balise,$contenu;

64. // s'agit-il d'une balise d'erreur ?

65. if($balise=="erreur"){

66. // on cumule au contenu de la balise courante

67. $contenu.=$data;

68. }//if

69. }//getData

Commentaires :

- la fonction getInfos($document) commence par créer un parseur, puis le configure et enfin lance l'analyse du document $document :

1. // on crée un objet d'analyse de texte xml

2. $xml_parser=xml_parser_create();

3. // on indique quelles fonctions exécuter en début et fin de balise

4. xml_set_element_handler($xml_parser,"startElement","endElement");

5. // on indique quelle fonction exécuter lors de la rencontre de données

6. xml_set_character_data_handler($xml_parser,"getData");

7. // on fait l'analyse de $document

8. xml_parse($xml_parser,$document);

- au retour de l'analyse, on libère les ressources allouées au parseur et on rend le dictionnaire $impots.

1. // on libère les ressources occupées par l'analyseur xml

2. xml_parser_free($xml_parser);

3. // fin

4. return $impots;

- la fonction startElement($parser,$name,$attributs) est appelée à chaque début de balise. Elle

- note la balise $name dans un tableau de balises $balises. Ce tableau est géré comme une pile : à la rencontre du symbole de fin de balise, la dernière balise empilée dans $balises sera dépilée. La balise courante est également notée dans $balise. Dans le dictionnaire $attributs, on trouve les attributs de la balise rencontrée, ces attributs étant en majuscules.

- enregistre les attributs dans le dictionnaire global $impots[simulations], s'il s'agit d'une balise de simulation.

- la fonction getData($parser,$data) lorsque le contenu $data d'une balise a été récupéré. Ici, on a pris une précaution. Dans certaines API, notamment Java, de traitement de documents XML, il est indiqué que cette fonction peut être appelée de façon répétée, c.a.d. qu'on n'a pas forcément le contenu $data d'une balise en une seule fois. Ici, la documentation PHP n'indique pas cette restriction. Par précaution, on cumule la valeur $data obtenue à une variable globale $contenu. Ce n'est qu'à la rencontre du symbole de fin de balise qu'on estimera avoir obtenu l'intégralité du contenu de la balise. La seule balise concernée par ce traitement est la balise <erreur>.

- la fonction endElement($parser,$name) est appelée à chaque fin de balise. Elle est utilisée ici pour changer le nom de la balise courante en enlevant la dernière balise de la pile de balises et pour ajouter le contenu de la balise <erreur> qui se termine au tableau $impots[erreurs].

Voici quelques exemples d'exécution, tout d'abord avec un SGBD qui n'a pas été lancé :

1. dos>e:\php43\php.exe cltXmlSimulations.php http://localhost/poly/impots/8/xmlsimulations.php oui 2 200000

2. Jeton de session=[e8c29ea12f79e4771960068d161229fd]

3. Les erreurs suivantes se sont produites :

4. Impossible d'ouvrir la base DSN [mysql-dbimpots] (S1000)

Puis avec le SGBD lancé :

1. dos>e:\php43\php.exe cltXmlSimulations.php http://localhost/poly/impots/8/xmlsimulations.php oui 3 200000

2. Jeton de session=[69a54d79db10b70ed0a2d55d5026ac8b]

3. Simulations :

4. [oui,3,200000,16400]

6. dos >e:\php43\php.exe cltXmlSimulations.php http://localhost/poly/impots/8/xmlsimulations.php oui 2 200000 69a54d79db10b70ed0a2d55d5026ac8b

7. Jeton de session=[69a54d79db10b70ed0a2d55d5026ac8b]

8. Simulations :

9. [oui,3,200000,16400]

10. [oui,2,200000,22504]

12. dos >e:\php43\php.exe cltXmlSimulations.php http://localhost/poly/impots/8/xmlsimulations.php non 2 200000 69a54d79db10b70ed0a2d55d5026ac8b

13. Jeton de session=[69a54d79db10b70ed0a2d55d5026ac8b]

14. Simulations :

15. [oui,3,200000,16400]

16. [oui,2,200000,22504]

17. [non,2,200000,33388]

V-E - Conclusion

Grâce à sa réponse XML l'application impots est devenue plus facile à gérer à la fois pour son concepteur que pour les concepteurs des application clientes.

1. la conception de l'application serveur peut maintenant être confiée à deux types de personnes : le développeur PHP de la servlet et l'infographiste qui va gérer l'apparence de la réponse du navigateur dans les navigateurs. Il suffit à ce dernier de connaître la structure de la réponse XML du serveur pour construire les feuilles de style qui vont l'accompagner. Rappelons que celles-ci font l'objet de fichiers XSL à part et indépendants de l'aplication PHP. L'infographiste peut donc travailler indépendamment du développeur.

2. les concepteurs des applications clientes ont eux aussi simplement besoin de connaître la structure de la réponse XML du serveur. Les modifications que pourrait apporter l'infographiste aux feuilles de style n'ont aucune répercussion sur cette réponse XML qui reste toujours la même. C'est un énorme avantage.

3. Comment le développeur peut-il faire évoluer son application PHP sans tout casser ? Tout d'abord, tant que sa réponse XML ne change pas, il peut organiser son application comme il veut. Il peut aussi faire évoluer la réponse XML tant qu'il garde les éléments <erreur> et <simulation> attendus par ses clients. Il peut ainsi ajouter de nouvelles balises à cette réponse. L'infographiste les prendra en compte dans ses feuilles de style et les navigateurs pourront avoir les nouvelles versions de la réponse. Les clients programmés eux continueront à fonctionner avec l'ancien modèle, les nouvelles balises étant tout simplement ignorées. Pour que cela soit possible, il faut que dans l'analyse XML de la réponse du serveur, les balises cherchées soient bien identifiées. C'est ce qui a été fait dans notre client XML de l'application impôts où dans les procédures on disait spécifiquement qu'on traitait les balises <erreur> et <simulation>. Du coup les autres balises sont ignorées.

VI - A suivre…

Nous avons donné dans ce document suffisamment d'informations pour démarrer sérieusement la programmation web en PHP. Certains thèmes n'ont été qu'effleurés et mériteraient des approfondissements :

1. XML (eXtensible Markup Language), Feuilles de style CSS (Cascading Style Sheets) et XSL (eXtensible Stylesheet Language)

Nous avons abordé ces thème mais beaucoup reste à faire. Ils méritent à eux trois un polycopié.

1. Javascript

Nous avons utilisé ici et là quelques scripts Javascript exécutés par le navigateur sans jamais s'y attarder. Nous ne sommes par exemple jamais entrés dans les détails du langage Javascript. Par manque de place et de temps. Ce langage peut nécessiter là encore un livre à lui tout seul. Nous avons rajouté en annexe, trois exemples significatifs en javascript. Ils sont donnés sans explications. Néanmoins ils sont compréhensibles par la simple lecture des commentaires et peuvent servir d'exemples.

VII - Compléments

VII-A - Les outils du développement web

Nous indiquons ici où trouver et comment installer les outils nécessaires au développement web. Certains outils ont vu leurs versions évoluer et il se peut que les explications données ici ne conviennent plus pour les versions les plus récentes. Le lecteur sera alors amené à s'adpater… Dans le cours de programmation web, nous utiliserons essentiellement les outils suivants, tous disponibles gratuitement :

- un navigateur récent capable d'afficher du XML. Les exemples du cours ont été testés avec Internet Explorer 6.

- un JDK (Java Development Kit) récent. Les exemples du cours ont été testés avec le JDK 1.4. Ce JDK amène avec lui le Plug-in Java 1.4 pour les navigateurs ce qui permet à ces derniers d'afficher des applets Java utilisant le JDK 1.4.

- un environnement de développement Java pour écrire des servlets Java. Ici c'est JBuilder 7.

- des serveurs web : Apache, PWS (Personal Web Server), Tomcat.

- Apache sera utilisé pour le développement d'applications web en PERL (Practical Extracting and Reporting Language) ou PHP (Personal Home Page)

- PWS sera utilisé pour le développement d'applications web en ASP (Active Server Pages) ou PHP

- Tomcat sera utilisé pour le développement d'applications web à l'aide de servlets Java ou de pages JSP (Java Server pages)

- une application de gestion de base de données : MySQL

- EasyPHP : un outil qui amène ensemble le serveur Web Apache, le langage PHP et le SGBD MySQL

VII-A-1 - Serveurs Web, Navigateurs, Langages de scripts

1. Serveurs Web principaux

2. Navigateurs principaux

3. Langages de scripts côté serveur

4. Langages de scripts côté navigateur

- Apache (Linux, Windows)

- Interner Information Server IIS (NT), Personal Web Server PWS (Windows 9x)

- Internet Explorer (Windows)

- Netscape (Linux, Windows)

- VBScript (IIS, PWS)

- JavaScript (IIS, PWS)

- Perl (Apache, IIS, PWS)

- PHP (Apache, IIS, PWS)

- Java (Apache, Tomcat)

- Langages .NET

- VBScript (IE)

- Javascript (IE, Netscape)

- Perlscript (IE)

- Java (IE, Netscape)

VII-A-2 - Où trouver les outils

Netscape

http://www.netscape.com/ (lien downloads)

Internet Explorer

http://www.microsoft.com/windows/ie/default.asp

PHP

http://www.php.net
http://www.php.net/downloads.php (Windows Binaries)

PERL

http://www.activestate.com http://www.activestate.com/Products/
http://www.activestate.com/Products/ActivePerl/

Vbscript, Javascript

http://msdn.microsoft.com/scripting (suivre le lien windows script)

JAVA

http://java.sun.com/
http://java.sun.com/downloads.html (JSE)
http://java.sun.com/j2se/1.4/download.html

Apache

http://www.apache.org/
http://www.apache.org/dist/httpd/binaries/win32/

PWS

inclus dans NT 4.0 Option pack for Windows 95
inclus dans le CD de Windows 98
http://www.microsoft.com/ntserver/nts/downloads/recommended/NT4OptPk/win95.asp

IIS
(windows NT/2000)

http://www.microsoft.com

Tomcat

http://jakarta.apache.org/tomcat/

JBuilder

http://www.borland.com/jbuilder/
http://www.borland.com/products/downloads/download_jbuilder.html

EasyPHP

http://www.easyphp.org/
http://www.easyphp.org/telechargements.php3

VII-A-3 - EasyPHP

Cette application est très pratique en ce qu'elle amène dans un même paquetage :

1. le serveur Web Apache (1.3.x)

2. le langage PHP (4.x)

3. le SGBD MySQL (3.23.x)

4. un outil d'administration de MySQL : PhpMyAdmin

L'application d'installation se présente sous la forme suivante :

L'installation d'EasyPHP ne pose pas de problème et une arborescence est créée dans le système de fichiers :

easyphp.exe

l'exécutable de l'application

apache

l'arborescence du serveur apache

mysql

l'arborescence du SGBD mysql

phpmyadmin

l'arborescence de l'application phpmyadmin

php

l'arborescence de php

www

racine de l'arborescence des pages web délivrées par le serveur apache d'EasyPHP

cgi-bin

arborescence où l'on peut palcer des script CGI pour le serveur Apache

L'intérêt principal d'EasyPHP est que l'application arrive préconfigurée. Ainsi Apache, PHP, MySQL sont déjà configurés pour travailler ensemble. Lorsqu'on lance EasyPhp par son lien dans le menu des programmes, une icône se met en place en bas à droite de l'écran.

C'est le E avec un point rouge qui doit clignoter si le serveur web Apache et la base de données MySQL sont opérationnels. Lorsqu'on clique dessus avec le bouton droit de la souris, on accède à des options de menu :

L'option Administration permet de faire des réglages et des tests de bon fonctionnement :

VII-A-3-a - Administration PHP

Le bouton infos php doit vous permettre de vérifier le bon fonctionnement du couple Apache-PHP : une page d'informations PHP doit apparaître :

Le bouton extensions donne la liste des extensions installées pour php. Ce sont en fait des bibliothèques de fonctions.

L'écran ci-dessus montre par exemple que les fonctions nécessaires à l'utilisation de la base MySQL sont bien présentes.

Le bouton paramètres donne le login/motdepasse de l'administrateur de la base de données MySQL.

L'utilisation de la base MySQL dépasse le cadre de cette présentation rapide mais il est clai ici qu'il faudrait mettre un mot de passe à l'administrateur de la base.

VII-A-3-b - Administration Apache

Toujours dans la page d'administration d'EasyPHP, le lien vos alias permet de définir des alias associés à un répertoire. Cela permet de mettre des pages Web ailleurs que dans le répertoire www de l'arborescence d'easyPhp.

Si dans la page ci-dessus, on met les informations suivantes :

et qu'on utilise le bouton valider les lignes suivantes sont ajoutées au fichier <easyphp>\apache\conf\httpd.conf :

httpd.conf

1. Alias /st/ "e:/data/serge/web/"

3. <Directory "e:/data/serge/web">

4. Options FollowSymLinks Indexes

5. AllowOverride None

6. Order deny,allow

7. allow from 127.0.0.1

8. deny from all

9. </Directory>

<easyphp> désigne le répertoire d'installation d'EasyPHP. httpd.conf est le fichier de configuration du serveur Apache. On peut donc faire la même chose en éditant directement ce fichier. Une modification du fichier httpd.conf est normalement prise en compte immédiatement par Apache. Si ce n'était pas le cas, il faudrait l'arrêter puis le relancer, toujours avec l'icône d'easyphp :

Pour terminer notre exemple, on peut maintenant placer des pages web dans l'arborescence e:\data\serge\web :

1. dos>dir e:\data\serge\web\html\balises.htm

3. 14/07/2002  17:02                3 767 balises.htm

et demander cette page en utilisant l'alias st :

Dans cet exemple, le serveur Apache a été configuré pour travailler sur le port 81. Son port par défaut est 80. Ce point est contrôlé par la ligne suivante du fichier httpd.conf déjà rencontré :

1. Port 81

VII-A-3-c - Le fichier de configuration d'Apache httpd.conf

Lorsqu'on veut configurer un peu finement Apache, on est obligé d'aller modifier "à la main" son fichier de configuration httpd.conf situé ici dans le dossier <easyphp>\apache\conf :

Voici quelques points à retenir de ce fichier de configuration :

ligne(s)

rôle

ServerRoot "D:/Program Files/Apache Group/Apache"

indique le dossier où se trouve l'arborescence de Apache

Port 80

indique sur quel port va travailler le serveur Web. Classiquement c'est 80. En changeant cette ligne, on peut faire travailler le serveur Web sur un autre port

ServerAdmin root@istia.univ-angers.fr

l'adresse email de l'administrateur du serveur Apache

ServerName stahe.istia.uang

le nom de la machine sur laquelle "tourne" le serveur Apache

ServerRoot "E:/Program Files/EasyPHP/apache"

le répertoire d'installation du serveur Apache. Lorsque dans le fichier de configuration, apparaissent des noms relatifs de fichiers, ils sont relatifs par rapport à ce dossier.

DocumentRoot "E:/Program Files/EasyPHP/www"

le dossier racine de l'arborescence des pages Web délivrées par le serveur. Ici, l'url http://machine/rep1/fic1.html correspondra au fichier E:\Program Files\EasyPHP\www \rep1\fic1.html

<Directory "E:/Program Files/EasyPHP/www">

fixe les propriétés du dossier précédent

ErrorLog logs/error.log

dossier des logs, donc en fait <ServerRoot>\logs\error.log : E:\Program Files\EasyPHP\apache\logs\error.log. C'est le fichier à consulter si vous constatez que le serveur Apache ne fonctionne pas.

ScriptAlias /cgi-bin/ "E:/Program Files/EasyPHP/cgi-bin/"

E:\Program Files\EasyPHP\cgi-bin sera la racine de l'arborescence où l'on pourra mettre des scripts CGI. Ainsi l'URL http://machine/cgi-bin/rep1/script1.pl sera l'url du script CGI E:\Program Files\EasyPHP\cgi-bin \rep1\script1.pl.

<Directory "E:/Program Files/EasyPHP/cgi-bin/">

fixe les propriétés du dossier ci-dessus

LoadModule php4_module "E:/Program Files/EasyPHP/php/php4apache.dll"
AddModule mod_php4.c

lignes de chargement des modules permettant à Apache de travailler avec PHP4.

AddType application/x-httpd-php .phtml .pwml .php3 .php4 .php .php2 .inc

fixe les suffixes des fichiers à considérer comme des fichiers comme devant être traités par PHP

VII-A-3-d - Administration de MySQL avec PhpMyAdmin

Sur la page d'administration d'EasyPhp, on clique sur le bouton PhpMyAdmin :

La liste déroulante sous Accueil permet de voir les bases de données actuelles.

Le nombre entre parenthèses est le nombre de tables. Si on choisit une base, les tables de celles-ci s'affichent :

La page Web offre un certain nombre d'opérations sur la base :

Si on clique sur le lien Afficher de user :

Il n'y a ici qu'un seul utilisateur : root, qui est l'administrateur de MySQL. En suivant le lien Modifier, on pourrait changer son mot de passe qui est actuellement vide, ce qui n'est pas conseillé pour un administrateur.

Nous n'en dirons pas plus sur PhpMyAdmin qui est un logiciel riche et qui mériterait un développement de plusieurs pages.

VII-A-4 - PHP

Nous avons vu comment obtenir PHP au travers de l'application EasyPhp. Pour obtenir PHP directement, on ira sur le site http://www.php.net.

PHP n'est pas utilisable que dans le cadre du Web. On peut l'utiliser comme langage de scripts sous Windows. Créez le script suivant et sauvegardez-le sous le nom date.php :

date.php

1. <?

2. // script php affichant l'heure

3. $maintenant=date("j/m/y, H:i:s",time());

4. echo "Nous sommes le $maintenant";

5. ?>

Dans une fenêtre DOS, placez-vous dans le répertoire de date.php et exécutez-le :

1. dos>"e:\program files\easyphp\php\php.exe" date.php

2. X-Powered-By: PHP/4.2.0

3. Content-type: text/html

5. Nous sommes le 18/07/02, 09:31:01

VII-A-5 - PERL

Il est préférable que Internet Explorer soit déjà installé. S'il est présent, Active Perl va le configurer afin qu'il accepte des scripts PERL dans les pages HTML, scripts qui seront exécutés par IE lui-même côté client. Le site de Active Perl est à l'URL http://www.activestate.comA l'installation, PERL sera installé dans un répertoire que nous appelerons <perl>. Il contient l'arborescence suivante :

1. DEISL1   ISU        32 403  23/06/00  17:16 DeIsL1.isu

2. BIN            <REP>        23/06/00  17:15 bin

3. LIB            <REP>        23/06/00  17:15 lib

4. HTML           <REP>        23/06/00  17:15 html

5. EG             <REP>        23/06/00  17:15 eg

6. SITE           <REP>        23/06/00  17:15 site

7. HTMLHELP       <REP>        28/06/00  18:37 htmlhelp

L'exécutable perl.exe est dans <perl>\bin. Perl est un langage de scripts fonctionnant sous Windows et Unix. Il est de plus utilisé dans la programmation WEB. Écrivons un premier script :

1. # script PERL affichant l'heure

3. # modules

4. use strict;

6. # programme

7. my ($secondes,$minutes,$heure)=localtime(time);

8. print "Il est $heure:$minutes:$secondes\n";

Sauvegardez ce script dans un fichier heure.pl. Ouvrez une fenêtre DOS, placez-vous dans le répertoire du script précédent et exécutez-le :

heure.pl

1. dos>e:\perl\bin\perl.exe heure.pl

2. Il est 9:34:21

VII-A-6 - Vbscript, Javascript, Perlscript

Ces langages sont des langages de script pour windows. Ils peuvent fonctionner dans différents conteneurs tels

- Windows Scripting Host pour une utilisation directe sous Windows notamment pour écrire des scripts d'administration système

- Internet Explorer. Il est alors utilisé au sein de pages HTML auxquelles il amène une certaine interactivité impossible à atteindre avec le seul langage HTML.

- Internet Information Server (IIS) le serveur Web de Microsoft sur NT/2000 et son équivalent Personal Web Server (PWS) sur Win9x. Dans ce cas, vbscript est utilisé pour faire de la programmation côté serveur web, technologie appelée ASP (Active Server Pages) par Microsoft.

On récupère le fichier d'installation à l'URL : http://msdn.microsoft.com/scripting et on suit les liens Windows Script. Sont installés :

- le conteneur Windows Scripting Host, conteneur permettant l'utilisation de divers langages de scripts, tels Vbscript et Javascript mais aussi d'autres tel PerlScript qui est amené avec Active Perl.

- un interpréteur VBscript

- un interpréteur Javascript

Présentons quelques tests rapides. Construisons le programme vbscript suivant :

1. ' une classe

2. class personne

3. Dim nom

4. Dim age

5. End class

7. ' création d'un objet personne

8. Set p1=new personne

9. With p1

10. .nom="dupont"

11. .age=18

12. End With

14. ' affichage propriétés personne p1

15. With p1

16. wscript.echo "nom=" & .nom

17. wscript.echo "age=" & .age

18. End With

Ce programme utilise des objets. Appelons-le objets.vbs (le suffixe vbs désigne un fichier vbscript). Positionnons-nous sur le répertoire dans lequel il se trouve et exécutons-le :

1. dos>cscript objets.vbs

2. Microsoft (R) Windows Script Host Version 5.6

3. Copyright (C) Microsoft Corporation 1996-2001. All rights reserved.

5. nom=dupont

6. age=18

Maintenant construisons le programme javascript suivant qui utilise des tableaux :

1. // tableau dans un variant

2. // tableau vide

3. tableau=new Array();

4. affiche(tableau);

5. // tableau croît dynamiquement

6. for(i=0;i<3;i++){

7. tableau.push(i*10);

8. }

9. // affichage tableau

10. affiche(tableau);

11. // encore

12. for(i=3;i<6;i++){

13. tableau.push(i*10);

14. }

15. affiche(tableau);

17. // tableaux à plusieurs dimensions

18. WScript.echo("-----------------------------");

20. tableau2=new Array();

21. for(i=0;i<3;i++){

22. tableau2.push(new Array());

23. for(j=0;j<4;j++){

24. tableau2[i].push(i*10+j);

25. }//for j

26. }// for i

27. affiche2(tableau2);

29. // fin

30. WScript.quit(0);

32. // ---------------------------------------------------------

33. function affiche(tableau){

34. // affichage tableau

35. for(i=0;i<tableau.length;i++){

36. WScript.echo("tableau[" + i + "]=" + tableau[i]);

37. }//for

38. }//function

40. // ---------------------------------------------------------

41. function affiche2(tableau){

42. // affichage tableau

43. for(i=0;i<tableau.length;i++){

44. for(j=0;j<tableau[i].length;j++){

45. WScript.echo("tableau[" + i + "," + j + "]=" + tableau[i][j]);

46. }// for j

47. }//for i

48. }//function

Ce programme utilise des tableaux. Appelons-le tableaux.js (le suffixe js désigne un fichier javascript). Positionnons-nous sur le répertoire dans lequel il se trouve et exécutons-le :

tableaux.js

1. dos>cscript tableaux.js

2. Microsoft (R) Windows Script Host Version 5.6

3. Copyright (C) Microsoft Corporation 1996-2001. Tous droits réservés.

5. tableau[0]=0

6. tableau[1]=10

7. tableau[2]=20

8. tableau[0]=0

9. tableau[1]=10

10. tableau[2]=20

11. tableau[3]=30

12. tableau[4]=40

13. tableau[5]=50

14. -----------------------------

15. tableau[0,0]=0

16. tableau[0,1]=1

17. tableau[0,2]=2

18. tableau[0,3]=3

19. tableau[1,0]=10

20. tableau[1,1]=11

21. tableau[1,2]=12

22. tableau[1,3]=13

23. tableau[2,0]=20

24. tableau[2,1]=21

25. tableau[2,2]=22

26. tableau[2,3]=23

Un dernier exemple en Perlscript pour terminer. Il faut avoir installé Active Perl pour avoir accès à Perlscript.

1. <job id="PERL1">

2. <script language="PerlScript">

3. # du Perl classique

4. %dico=("maurice"=>"juliette","philippe"=>"marianne");

5. @cles= keys %dico;

6. for ($i=0;$i<=$#cles;$i++){

7. $cle=$cles[$i];

8. $valeur=$dico{$cle};

9. $WScript->echo ("clé=".$cle.", valeur=".$valeur);

10. }

11. # du perlscript utilisant les objets Windows Script

12. $dico=$WScript->CreateObject("Scripting.Dictionary");

13. $dico->add("maurice","juliette");

14. $dico->add("philippe","marianne");

15. $WScript->echo($dico->item("maurice"));

16. $WScript->echo($dico->item("philippe"));

17. </script>

18. </job>

Ce programme montre la création et l'utilisation de deux dictionnaires : l'un à la mode Perl classique, l'autre avec l'objet Scripting Dictionary de Windows Script. Sauvegardons ce code dans le fichier dico.wsf (wsf est le suffixe des fichiers Windows Script). Positionnons-nous dans le dossier de ce programme et exécutons-le :

1. dos>cscript dico.wsf

2. Microsoft (R) Windows Script Host Version 5.6

3. Copyright (C) Microsoft Corporation 1996-2001. Tous droits réservés.

5. clé=philippe, valeur=marianne

6. clé=maurice, valeur=juliette

7. juliette

8. marianne

Perlscript peut utiliser les objets du conteneur dans lequel il s'exécute. Ici c'était des objets du conteneur Windows Script. Dans le contexte de la programmation Web, les scripts VBscript, Javascript, Perlscript peuvent être exécutés soit au sein du navigateur IE, soit au sein d'un serveur PWS ou IIS. Si le script est un peu complexe, il peut être judicieux de le tester hors du contexte Web, au sein du conteneur Windows Script comme il a été vu précédemment. On ne pourra tester ainsi que les fonctions du script qui n'utilisent pas des objets propres au navigateur ou au serveur. Même avec cette restriction, cette possibilité reste intéressante car il est en général assez peu pratique de déboguer des scripts s'exécutant au sein des serveurs web ou des navigateurs.

VII-A-7 - JAVA

Java est disponible à l'URL : http://www.sun.com (cf début de ce document) et s'installe dans une arborescence qu'on appellera <java> qui contient les éléments suivants :

1. 22/05/2002  05:51       <DIR>          .

2. 22/05/2002  05:51       <DIR>          ..

3. 22/05/2002  05:51       <DIR>          bin

4. 22/05/2002  05:51       <DIR>          jre

5. 07/02/2002  12:52                8 277 README.txt

6. 07/02/2002  12:52               13 853 LICENSE

7. 07/02/2002  12:52                4 516 COPYRIGHT

8. 07/02/2002  12:52               15 290 readme.html

9. 22/05/2002  05:51       <DIR>          lib

10. 22/05/2002  05:51       <DIR>          include

11. 22/05/2002  05:51       <DIR>          demo

12. 07/02/2002  12:52           10 377 848 src.zip

13. 11/02/2002  12:55       <DIR>          docs

Dans bin, on trouvera javac.exe, le compilateur Java et java.exe la machine virtuelle Java. On pourra faire les tests suivants :

- Écrire le script suivant :

1. //programme Java affichant l'heure

3. import java.io.*;

4. import java.util.*;

6. public class heure{

7. public static void main(String arg[]){

8. // on récupère date & heure

9. Date maintenant=new Date();

10. // on affiche

11. System.out.println("Il est "+maintenant.getHours()+

12. ":"+maintenant.getMinutes()+":"+maintenant.getSeconds());

13. }//main

14. }//class

- Sauvegarder ce programme sous le nom heure.java. Ouvrir une fenêtre DOS. Se mettre dans le répertoire du fichier heure.java et le compiler :

1. dos>c:\jdk1.3\bin\javac heure.java

2. Note: heure.java uses or overrides a deprecated API.

3. Note: Recompile with -deprecation for details.

Dans la commande ci-dessus c:\jdk1.3\bin\javac doit être remplacé par le chemin exact du compilateur javac.exe. Vous devez obtenir dans le même répertoire que heure.java un fichier heure.class qui est le programme qui va maintenant être exécuté par la machine virtuelle java.exe.

- Exécuter le programme :

1. dos>c:\jdk1.3\bin\java heure

2. Il est 10:44:2

VII-A-8 - Serveur Apache

Nous avons vu que l'on pouvait obtenir le serveur Apache avec l'application EasyPhp. Pour l'avoir directement, on ira sur le site d'Apache : http://www.apache.org. L'installation crée une arborescence où on trouve tous les fichiers nécessaires au serveur. Appelons <apache> ce répertoire. Il contient une arborescence analogue à la suivante :

1. UNINST   ISU       118 805  23/06/00  17:09 Uninst.isu

2. HTDOCS         <REP>        23/06/00  17:09 htdocs

3. APACHE~1 DLL       299 008  25/02/00  21:11 ApacheCore.dll

4. ANNOUN~1             3 000  23/02/00  16:51 Announcement

5. ABOUT_~1            13 197  31/03/99  18:42 ABOUT_APACHE

6. APACHE   EXE        20 480  25/02/00  21:04 Apache.exe

7. KEYS                36 437  20/08/99  11:57 KEYS

8. LICENSE              2 907  01/01/99  13:04 LICENSE

9. MAKEFI~1 TMP        27 370  11/01/00  13:47 Makefile.tmpl

10. README               2 109  01/04/98   6:59 README

11. README   NT          3 223  19/03/99   9:55 README.NT

12. WARNIN~1 TXT           339  21/09/98  13:09 WARNING-NT.TXT

13. BIN            <REP>        23/06/00  17:09 bin

14. MODULES        <REP>        23/06/00  17:09 modules

15. ICONS          <REP>        23/06/00  17:09 icons

16. LOGS           <REP>        23/06/00  17:09 logs

17. CONF           <REP>        23/06/00  17:09 conf

18. CGI-BIN        <REP>        23/06/00  17:09 cgi-bin

19. PROXY          <REP>        23/06/00  17:09 proxy

20. INSTALL  LOG         3 779  23/06/00  17:09 install.log

conf

dossier des fichiers de configuration d'Apache

logs

dossier des fichiers de logs (suivi) d'Apache

bin

les exécutables d'Apache

VII-A-8-a - Configuration

Dans le dossier <Apache>\conf, on trouve les fichiers suivants : httpd.conf, srm.conf, access.conf. Dans les dernières versions d'Apache, les trois fichiers ont été réunis dans httpd.conf. Nous avons déjà présenté les points importants de ce fichier de configuration. Dans les exemples qui suivent c'est la version Apache d'EasyPhp qui a servi aux tests et donc son fichier de configuration. Dans celui-ci DocumentRoot qui désigne la racine de l'arborescence des pages Web est e:\program files\easyphp\www.

VII-A-8-b - Lien PHP - Apache

Pour tester, créer le fichier intro.php avec la seule ligne suivante :

1. <? phpinfo() ?>

et le mettre à la racine des pages du serveur Apache(DocumentRoot ci-dessus). Demander l'URL http://localhost/intro.php. On doit voir une liste d'informations php :

Le script PHP suivant affiche l'heure. Nous l'avons déjà rencontré :

1. <?

2. // time : nb de millisecondes depuis 01/01/1970

3. // "format affichage date-heure

4. // d: jour sur 2 chiffres

5. // m: mois sur 2 chiffres

6. // y : année sur 2 chiffres

7. // H : heure 0,23

8. // i : minutes

9. // s: secondes

10. print "Nous sommes le " . date("d/m/y H:i:s",time());

11. ?>

Plaçons ce fichier texte à la racine des pages du serveur Apache (DocumentRoot ) et appelons-le date.php. Demandons avec un navigateur l'URL http://localhost/date.php. On obtient la page suivante :

VII-A-8-c - Lien PERL-APACHE

Il est fait grâce à une ligne de la forme : ScriptAlias /cgi-bin/ "E:/Program Files/EasyPHP/cgi-bin/" du fichier <apache>\conf\httpd.conf. Sa syntaxe est ScriptAlias /cgi-bin/ "<cgi-bin>" où <cgi-bin> est le dossier où on pourra placer des scripts CGI. CGI (Common Gateway Interface) est une norme de dialogue serveur WEB <--> Applications. Un client demande au serveur Web une page dynamique, c.a.d. une page générée par un programme. Le serveur WEB doit donc demander à un programme de générer la page. CGI définit le dialogue entre le serveur et le programme, notamment le mode de transmission des informations entre ces deux entités.

Si besoin est, modifiez la ligne ScriptAlias /cgi-bin/ "<cgi-bin>"et relancez le serveur Apache. Faites ensuite le test suivant :

- Écrire le script :

1. #!c:\perl\bin\perl.exe

3. # script PERL affichant l'heure

5. # modules

6. use strict;

8. # programme

9. my ($secondes,$minutes,$heure)=localtime(time);

10. print <<FINHTML

11. Content-Type: text/html

13. <html>

14. <head>

15. <title>heure</title>

16. </head>

17. <body>

18. <h1>Il est $heure:$minutes:$secondes</h1>

19. </body>

20. FINHTML

21. ;

- Mettre ce script dans <cgi-bin>\heure.pl où <cgi-bin> est le dossier pouvant recevoir des scripts CGI (cf httpd.conf). La première ligne #!c:\perl\bin\perl.exe désigne le chemin de l'exécutable perl.exe. Le modifier si besoin est.

- Lancer Apache si ce n'est fait

- Demander avec un navigateur l'URL http://localhost/cgi-bin/heure.pl. On obtient la page suivante :

VII-A-9 - Le serveur PWS

VII-A-9-a - Installation

Le serveur PWS (Personal Web Server) est une version personnelle du serveur IIS (Internet Information server) de Microsoft. Ce dernier est disponible sur les machines NT et 2000. Sur les machines win9x, PWS est normalement disponible avec le paquetage d'installation Internet Explorer. Cependant il n'est pas installé par défaut. Il faut prendre une installation personnalisée d'IE et demander l'installation de PWS. Il est par ailleurs disponibles dans le NT 4.0 Option pack for Windows 95.

VII-A-9-b - Premiers tests

La racine des pages Web du serveur PWS est lecteur:\inetpub\wwwroot où lecteur est le disque sur lequel vous avez installé PWS. Nous supposons dans la suite que ce lecteur est D. Ainsi l'url http://machine/rep1/page1.html correspondra au fichier d:\inetpub\wwwroot\rep1\page1.html. Le serveur PWS interprète tout fichier de suffixe .asp (Active Server pages) comme étant un script qu'il doit exécuter pour produire une page HTML.

PWS travaille par défaut sur le port 80. Le serveur web Apache aussi… Il faut donc arrêter Apache pour travailler avec PWS si vous avez les deux serveurs. L'autre solution est de configurer Apache pour qu'il travaille sur un autre port. Ainsi dans le fichier httpd.conf de configuration d'Apache, on remplace la ligne Port 80 par Port 81, Apache travaillera désormais sur le port 81 et pourra être utilisé en même temps que PWS. Si PWS ayant été lancé, on demande l'URL http://localhost, on obtient une page analogue à la suivante :

VII-A-9-c - Lien PHP - PWS

- Ci-dessous on trouvera un fichier .reg destiné à modifier la base de registres. Double-cliquer sur ce fichier pour modifier la base. Ici la dll nécessaire se trouve dans d:\php4 avec l'exécutable de php. Modifier si besoin est. Les \ doivent être doublés dans le chemin de la dll.

1. REGEDIT4

3. [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\w3svc\parameters\Script Map]

4. ".php"="d:\\php4\\php4isapi.dll"

- Relancer la machine pour que la modification de la base de registres soit prise en compte.

- Créer un dossier php dans d:\inetpub\wwwroot qui est la racine du serveur PWS. Ceci fait, activez PWS et prendre l'onglet « Avancé ». Sélectionner le bouton « Ajouter » pour créer un dossier virtuel :

Répertoire/Parcourir : d:\inetpub\wwwroot\php

Alias : php

Cocher la case exécuter.

- Valider le tout et relancer PWS. Mettre dans d:\inetpub\wwwroot\php le fichier intro.php ayant la seule ligne suivante :

1. <? phpinfo() ?>

- Demander au serveur PWS l'URL http://localhost/php/intro.php. On doit voir la liste d'informations php déjà présentées avec Apache.

VII-A-10 - Tomcat : servlets Java et pages JSP (Java Server Pages)

Tomcat est un serveur Web permettant de générer des pages HTML grâce à des servlets (programmes Java exécutés par le serveur web) où des pages JSP (Java Server Pages), pages mélangeant code Java et code HTML. C'est l'équivalent des pages ASP (Active Server Pages) du serveur IIS/PWS de Microsoft où là on mélange code VBScript ou Javascript avec du code HTML.

VII-A-10-a - Installation

Tomcat est disponible à l'URL : http://jakarta.apache.org. On récupère un fichier .exe d'installation. Lorsqu'on lance ce programme, il commence par indiquer quel JDK il va utiliser. En effet Tomcat a besoin d'un JDK pour s'installer et ensuite compiler et exécuter les servlets Java. Il faut donc que vous ayez installé un JDK Java avant d'installer Tomcat. Le JD le plus récent est conseillé. L'installation va créer une arborescence <tomcat> :

consiste simplement à décompresser cette archive dans un répertoire. Prenez un répertoire ne contenant dans son chemin que des noms sans espace (pas par exemple "Program Files"), ceci parce qu'il y a un bogue dans le processus d'installation de Tomcat. Prenez par exemple C:\tomcat ou D:\tomcat. Appelons ce répertoire <tomcat>. On y trouvera dedans un dossier appelé jakarta-tomcat et dans celui-ci l'arborescence suivante :

1. LOGS           <REP>        15/11/00   9:04 logs

2. LICENSE              2 876  18/04/00  15:56 LICENSE

3. CONF           <REP>        15/11/00   8:53 conf

4. DOC            <REP>        15/11/00   8:53 doc

5. LIB            <REP>        15/11/00   8:53 lib

6. SRC            <REP>        15/11/00   8:53 src

7. WEBAPPS        <REP>        15/11/00   8:53 webapps

8. BIN            <REP>        15/11/00   8:53 bin

9. WORK           <REP>        15/11/00   9:04 work

VII-A-10-b - Démarrage/Arrêt du serveur Web Tomcat

Tomcat est un serveur Web comme l'est Apache ou PWS. Pour le lancer, on dispose de liens dans le menu des programmes :

Start Tomcat

pour lancer Tomcat

Stop Tomcat

pour l'arrêter

Lorsqu'on lance Tomcat, une fenêtre Dos s'affiche avec le contenu suivant :

On peut mettre cette fenêtre Dos en icône. Elle restera présente pendant tant que Tomcat sera actif. On peut alors passer aux premiers tests. Le serveur Web Tomcat travaille sur le port 8080. Une fois Tomcat lancé, prenez un navigateur Web et demandez l'URL http://localhost:8080. Vous devez obtenir la page suivante :

Suivez le lien Servlet Examples :

Cliquez sur le lien Execute de RequestParameters puis sur celui de Source. Vous aurez un premier aperçu de ce qu'est une servlet Java. Vous pourrez faire de même avec les liens sur les pages JSP.

Pour arrêter Tomcat, on utilisera le lien Stop Tomcat dans le menu des programmes.

VII-A-11 - Jbuilder

Jbuilder est un environnement de développement d'applications Java. Pour construire des servlets Java où il n'y a pas d'interfaces graphiques, il n'est pas indispensable d'avoir un tel environnement. Un éditeur de textes et un JDK font l'affaire. Seulement JBuilder apporte avec lui quelques plus par rapport à la technique précédente :

- facilité de débogage : le compilateur signale les lignes erronées d'un programme et il est facile de s'y positionner

- suggestion de code : lorsqu'on utilise un objet Java, JBuilder donne en ligne la liste des propriétés et méthodes de celui-ci. Cela est très pratique lorsqu'on sait que la plupart des objets Java ont de très nombreuses propriétés et méthodes qu'il est difficile de se rappeler.

On trouvera JBuilder sur le site http://www.borland.com/jbuilder. Il faut remplir un formulaire pour obtenir le logiciel. Une clé d'activation est envoyée par mél. Pour installer JBuilder 7, il a par exemple été procédé ainsi :

- trois fichiers zip ont été obtenus : pour l'application, pour la documentation, pour les exemples. Chacun de ces zip fait l'objet d'un lien séparé sur le site de JBuilder.

- on a installé d'abord l'application, puis la documentation et enfin les exemples

- lorsqu'on lance l'application la première fois, une clé d'activation est demandée : c'est celle qui vous a été envoyée par mél. Dans la version 7, cette clé est en fait la totalité d'un fichier texte que l'on peut placer, par exemple, dans le dossier d'installation de JB7. Au moment où la clé est demandée, on désigne alors le fichier en question. Ceci fait, la clé ne sera plus redemandée.

Il y a quelques configurations utiles à faire si on veut utiliser JBuilder pour construire des servlets Java. En effet, la version dite Jbuilder personnel est une version allégée qui ne vient notamment pas avec toutes les classes nécessaires pour faire du développement web en Java. On peut faire en sorte que JBuilder utilise les bibliothèques de classes amenées par Tomcat. On procède ainsi :

- lancer JBuilder

- activer l'option Tools/Configure JDKs

Dans la partie JDK Settings ci-dessus, on a normalement dans le champ Name un JDK 1.3.1. Si vous avez un JDK plus récent, utilisez le bouton Change pour désigner le répertoire d'installation de ce dernier. Ci-dessus, on a désigné le répertoire E:\Program Files\jdk14 où avait installé un JDK 1.4. Désormais, JBuilder utilisera ce JDK pour ses compilations et exécutions. Dans la partie (Class, Siurce, Documentation) on a la liste de toutes les bibliothèques de classes qui seront explorées par JBuilder, ici les classes du JDK 1.4. Les classes de celui-ci ne suffisent pas pour faire du développement web en Java. Pour ajouter d'autres bibliothèques de classes on utilise le bouton Add et on désigne les fichiers .jar supplémentaires que l'on veut utiliser. Les fichiers .jar sont des bibliothèques de classes. Tomcat 4.x amène avec lui toutes les bibliothèques de classes nécessaires au développement web. Elles se trouvent dans <tomcat>\common\lib où <tomcat> est le répertoire d'installation de Tomcat :

Avec le bouton Add, on va ajouter ces bibliothèques, une à une, à la liste des bibliothèques explorées par JBuilder :

A partir de maintenant, on peut compiler des programmes java conformes à la norme J2EE, notamment les servlets Java. Jbuilder ne sert qu'à la compilation, l'exécution étant ultérieurement assurée par Tomcat selon des modalités expliquées dans le cours.

VII-B - Code source de programmes

VII-B-1 - Le client TCP générique

Beaucoup de services créés à l'origine de l'Internet fonctionnent selon le modèle du serveur d'écho étudié précédemment : les échanges client-serveur se font pas échanges de lignes de texte. Nous allons écrire un client tcp générique qui sera lancé de la façon suivante : java cltTCPgenerique serveur port

Ce client TCP se connectera sur le port port du serveur serveur. Ceci fait, il créera deux threads :

1. un thread chargé de lire des commandes tapées au clavier et de les envoyer au serveur

2. un thread chargé de lire les réponses du serveur et de les afficher à l'écran

Pourquoi deux threads? Chaque service TCP-IP a son protocole particulier et on trouve parfois les situations suivantes :

- le client doit envoyer plusieurs lignes de texte avant d'avoir une réponse

- la réponse d'un serveur peut comporter plusieurs lignes de texte

Aussi la boucle envoi d'une unique ligne au serveur - réception d'une unique ligne envoyée par le serveur ne convient-elle pas toujours. On va donc créer deux boucles dissociées :

- une boucle de lecture des commandes tapées au clavier pour être envoyées au serveur. L'utilisateur signalera la fin des commandes avec le mot clé fin.

- une boucle de réception et d'affichage des réponses du serveur. Celle-ci sera une boucle infinie qui ne sera interrompue que par la fermeture du flux réseau par le serveur ou par l'utilisateur au clavier qui tapera la commande fin.

Pour avoir ces deux boucles dissociées, il nous faut deux threads indépendants. Montrons un exemple d'excécution où notre client tcp générique se connecte à un service SMTP (SendMail Transfer Protocol). Ce service est responsable de l'acheminement du courrier électronique à leurs destinataires. Il fonctionne sur le port 25 et a un protocole de dialogue de type échanges de lignes de texte.

1. Dos>java clientTCPgenerique istia.univ-angers.fr 25

2. Commandes :

3. <-- 220 istia.univ-angers.fr ESMTP Sendmail 8.11.6/8.9.3; Mon, 13 May 2002 08:37:26 +0200

4. help

5. <-- 502 5.3.0 Sendmail 8.11.6 -- HELP not implemented

6. mail from: machin@univ-angers.fr

7. <-- 250 2.1.0 machin@univ-angers.fr... Sender ok

8. rcpt to: serge.tahe@istia.univ-angers.fr

9. <-- 250 2.1.5 serge.tahe@istia.univ-angers.fr... Recipient ok

10. data

11. <-- 354 Enter mail, end with "." on a line by itself

12. Subject: test

14. ligne1

15. ligne2

16. ligne3

17. .

18. <-- 250 2.0.0 g4D6bks25951 Message accepted for delivery

19. quit

20. <-- 221 2.0.0 istia.univ-angers.fr closing connection

21. [fin du thread de lecture des réponses du serveur]

22. fin

23. [fin du thread d'envoi des commandes au serveur]

Commentons ces échanges client-serveur :

1. le service SMTP envoie un message de bienvenue lorsqu'un client se connecte à lui :

1. <-- 220 istia.univ-angers.fr ESMTP Sendmail 8.11.6/8.9.3; Mon, 13 May 2002 08:37:26 +0200

1. certains services ont une commande help donnant des indications sur les commandes utilisables avec le service. Ici ce n'est pas le cas. Les commandes SMTP utilisées dans l'exemple sont les suivantes :

1. mail from: expéditeur, pour indiquer l'adresse électronique de l'expéditeur du message

2. rcpt to: destinataire, pour indiquer l'adresse électronique du destinataire du message. S'il y a plusieurs destinataires, on ré-émet autant de fois que nécessaire la commande rcpt to: pour chacun des destinataires.

3. data qui signale au serveur SMTP qu'on va envoyer le message. Comme indiqué dans la réponse du serveur, celui-ci est une suite de lignes terminée par une ligne contenant le seul caractère point. Un message peut avoir des entêtes séparés du corps du message par une ligne vide. Dans notre exemple, nous avons mis un sujet avec le mot clé Subject:

2. une fois le message envoyé, on peut indiquer au serveur qu'on a terminé avec la commande quit. Le serveur ferme alors la connexion réseau. Le thread de lecture peut détecter cet événement et s'arrêter.

3. l'utilisateur tape alors fin au clavier pour arrêter également le thread de lecture des commandes tapées au clavier.

Si on vérifie le courrier reçu, nous avons la chose suivante (Outlook) :

On remarquera que le service SMTP ne peut détecter si un expéditeur est valide ou non. Aussi ne peut-on jamais faire confiance au champ from d'un message. Ici l'expéditeur machin@univ-angers.fr n'existait pas.

Ce client tcp générique peut nous permettre de découvrir le protocole de dialogue de services internet et à partir de là construire des classes spécialisées pour des clients de ces services. Découvrons le protocole de dialogue du service POP (Post Office Protocol) qui permet de retrouver ses méls stockés sur un serveur. Il travaille sur le port 110.

1. Dos> java clientTCPgenerique istia.univ-angers.fr 110

2. Commandes :

3. <-- +OK Qpopper (version 4.0.3) at istia.univ-angers.fr starting.

4. help

5. <-- -ERR Unknown command: "help".

6. user st

7. <-- +OK Password required for st.

8. pass monpassword

9. <-- +OK st has 157 visible messages (0 hidden) in 11755927 octets.

10. list

11. <-- +OK 157 visible messages (11755927 octets)

12. <-- 1 892847

13. <-- 2 171661

14. ...

15. <-- 156 2843

16. <-- 157 2796

17. <-- .

18. retr 157

19. <-- +OK 2796 octets

20. <-- Received: from lagaffe.univ-angers.fr (lagaffe.univ-angers.fr [193.49.144.1])

21. <--     by istia.univ-angers.fr (8.11.6/8.9.3) with ESMTP id g4D6wZs26600;

22. <--     Mon, 13 May 2002 08:58:35 +0200

23. <-- Received: from jaume ([193.49.146.242])

24. <--     by lagaffe.univ-angers.fr (8.11.1/8.11.2/GeO20000215) with SMTP id g4D6wSd37691;

25. <--     Mon, 13 May 2002 08:58:28 +0200 (CEST)

26. ...

27. <-- ------------------------------------------------------------------------

28. <-- NOC-RENATER2                  Tl.  : 0800 77 47 95

29. <-- Fax : (+33) 01 40 78 64 00 ,  Email : noc-r2@cssi.renater.fr

30. <-- ------------------------------------------------------------------------

31. <--

32. <-- .

33. quit

34. <-- +OK Pop server at istia.univ-angers.fr signing off.

35. [fin du thread de lecture des réponses du serveur]

36. fin

37. [fin du thread d'envoi des commandes au serveur]

Les principales commandes sont les suivantes :

- user login, où on donne son login sur la machine qui détient nos méls

- pass password, où on donne le mot de passe associé au login précédent

- list, pour avoir la liste des messages sous la forme numéro, taille en octets

- retr i, pour lire le message n° i

- quit, pour arrêter le dialogue.

Découvrons maintenant le protocole de dialogue entre un client et un serveur Web qui lui travaille habituellement sur le port 80 :

1. Dos> java clientTCPgenerique istia.univ-angers.fr 80

2. Commandes :

3. GET /index.html HTTP/1.0

5. <-- HTTP/1.1 200 OK

6. <-- Date: Mon, 13 May 2002 07:30:58 GMT

7. <-- Server: Apache/1.3.12 (Unix)  (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21

8. <-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT

9. <-- ETag: "23432-2bf3-3c60f0ca"

10. <-- Accept-Ranges: bytes

11. <-- Content-Length: 11251

12. <-- Connection: close

13. <-- Content-Type: text/html

14. <--

15. <-- <html>

16. <--

17. <-- <head>

18. <-- <meta http-equiv="Content-Type"

19. <-- content="text/html; charset=iso-8859-1">

20. <-- <meta name="GENERATOR" content="Microsoft FrontPage Express 2.0">

21. <-- <title>Bienvenue a l'ISTIA - Universite d'Angers</title>

22. <-- </head>

23. ....

24. <-- face="Verdana"> - Dernire mise  jour le <b>10 janvier 2002</b></font></p>

25. <-- </body>

26. <-- </html>

27. <--

28. [fin du thread de lecture des réponses du serveur]

29. fin

30. [fin du thread d'envoi des commandes au serveur]

Un client Web envoie ses commandes au serveur selon le schéma suivant :

1. commande1

2. commande2

3. ...

4. commanden

5. [ligne vide]

Ce n'est qu'après avoir reçu la ligne vide que le serveur Web répond. Dans l'exemple nous n'avons utilisé qu'une commande :

1. GET /index.html HTTP/1.0

qui demande au serveur l'URL /index.html et indique qu'il travaille avec le protocole HTTP version 1.0. La version la plus récente de ce protocole est 1.1. L'exemple montre que le serveur a répondu en renvoyant le contenu du fichier index.html puis qu'il a fermé la connexion puisqu'on voit le thread de lecture des réponses se terminer. Avant d'envoyer le contenu du fichier index.html, le serveur web a envoyé une série d'entêtes terminée par une ligne vide :

1. <-- HTTP/1.1 200 OK

2. <-- Date: Mon, 13 May 2002 07:30:58 GMT

3. <-- Server: Apache/1.3.12 (Unix)  (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21

4. <-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT

5. <-- ETag: "23432-2bf3-3c60f0ca"

6. <-- Accept-Ranges: bytes

7. <-- Content-Length: 11251

8. <-- Connection: close

9. <-- Content-Type: text/html

10. <--

11. <-- <html>

La ligne <html> est la première ligne du fichier /index.html. Ce qui précède s'appelle des entêtes HTTP (HyperText Transfer Protocol). Nous n'allons pas détailler ici ces entêtes mais on se rappellera que notre client générique y donne accès, ce qui peut être utile pour les comprendre. La première ligne par exemple :

1. <-- HTTP/1.1 200 OK

indique que le serveur Web contacté comprend le protocole HTTP/1.1 et qu'il a bien trouvé le fichier demandé (200 OK), 200 étant un code de réponse HTTP. Les lignes

1. <-- Content-Length: 11251

2. <-- Connection: close

3. <-- Content-Type: text/html

disent au client qu'il va recevoir 11251 octets représentant du texte HTML (HyperText Markup Language) et qu'à la fin de l'envoi, la connexion sera fermée.

On a donc là un client tcp très pratique. Il fait sans doute moins que le programme telnet que nous avons utilisé précédemment mais il était intéressant de l'écrire nous-mêmes. Le programme du client tcp générique est le suivant :

1. // paquetages importés

2. import java.io.*;

3. import java.net.*;

5. public class clientTCPgenerique{

7. // reçoit en paramètre les caractéristiques d'un service sous la forme

8. // serveur port

9. // se connecte au service

10. // crée un thread pour lire des commandes tapées au clavier

11. // celles-ci seront envoyées au serveur

12. // crée un thread pour lire les réponses du serveur

13. // celles-ci seront affichées à l'écran

14. // le tout se termine avec la commande fin tapée au clavier

16. // variable d'instance

17. private static Socket client;

19. public static void main(String[] args){

21. // syntaxe

22. final String syntaxe="pg serveur port";

24. // nombre d'arguments

25. if(args.length != 2)

26. erreur(syntaxe,1);

28. // on note le nom du serveur

29. String serveur=args[0];

31. // le port doit être entier >0

32. int port=0;

33. boolean erreurPort=false;

34. Exception E=null;

35. try{

36. port=Integer.parseInt(args[1]);

37. }catch(Exception e){

38. E=e;

39. erreurPort=true;

40. }

41. erreurPort=erreurPort || port <=0;

42. if(erreurPort)

43. erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);

45. client=null;

46. // il peut y avoir des problèmes

47. try{

48. // on se connecte au service

49. client=new Socket(serveur,port);

50. }catch(Exception ex){

51. // erreur

52. erreur("Impossible de se connecter au service ("+ serveur

53. +","+port+"), erreur : "+ex.getMessage(),3);

54. // fin

55. return;

56. }//catch

58. // on crée les threads de lecture/écriture

59. new ClientSend(client).start();

60. new ClientReceive(client).start();

62. // fin thread main

63. return;

64. }// main

66. // affichage des erreurs

67. public static void erreur(String msg, int exitCode){

68. // affichage erreur

69. System.err.println(msg);

70. // arrêt avec erreur

71. System.exit(exitCode);

72. }//erreur

73. }//classe

75. class ClientSend extends Thread {

76. // classe chargée de lire des commandes tapées au clavier

77. // et de les envoyer à un serveur via un client tcp passé en paramètre

79. private Socket client;    // le client tcp

81. // constructeur

82. public ClientSend(Socket client){

83. // on note le client tcp

84. this.client=client;

85. }//constructeur

87. // méthode Run du thread

88. public void run(){

90. // données locales

91. PrintWriter OUT=null;            // flux d'écriture réseau

92. BufferedReader IN=null;        // flux clavier

93. String commande=null;            // commande lue au clavier

95. // gestion des erreurs

96. try{

97. // création du flux d'écriture réseau

98. OUT=new PrintWriter(client.getOutputStream(),true);

99. // création du flux d'entrée clavier

100. IN=new BufferedReader(new InputStreamReader(System.in));

101. // boucle saisie-envoi des commandes

102. System.out.println("Commandes : ");

103. while(true){

104. // lecture commande tapée au clavier

105. commande=IN.readLine().trim();

106. // fini ?

107. if (commande.toLowerCase().equals("fin")) break;

108. // envoi commande au serveur

109. OUT.println(commande);

110. // commande suivante

111. }//while

112. }catch(Exception ex){

113. // erreur

114. System.err.println("Envoi : L'erreur suivante s'est produite : " + ex.getMessage());

115. }//catch

116. // fin - on ferme les flux

117. try{

118. OUT.close();client.close();

119. }catch(Exception ex){}

120. // on signale la fin du thread

121. System.out.println("[Envoi : fin du thread d'envoi des commandes au serveur]");

122. }//run

123. }//classe

125. class ClientReceive extends Thread{

126. // classe chargée de lire les lignes de texte destinées à un

127. // client tcp passé en paramètre

129. private Socket client;    // le client tcp

131. // constructeur

132. public ClientReceive(Socket client){

133. // on note le client tcp

134. this.client=client;

135. }//constructeur

137. // méthode Run du thread

138. public void run(){

140. // données locales

141. BufferedReader IN=null;        // flux lecture réseau

142. String réponse=null;        // réponse serveur

144. // gestion des erreurs

145. try{

146. // création du flux lecture réseau

147. IN=new BufferedReader(new InputStreamReader(client.getInputStream()));

148. // boucle lecture lignes de texte du flux IN

149. while(true){

150. // lecture flux réseau

151. réponse=IN.readLine();

152. // flux fermé ?

153. if(réponse==null) break;

154. // affichage

155. System.out.println("<-- "+réponse);

156. }//while

157. }catch(Exception ex){

158. // erreur

159. System.err.println("Réception : L'erreur suivante s'est produite : " + ex.getMessage());

160. }//catch

161. // fin - on ferme les flux

162. try{

163. IN.close();client.close();

164. }catch(Exception ex){}

165. // on signale la fin du thread

166. System.out.println("[Réception : fin du thread de lecture des réponses du serveur]");

167. }//run

168. }//classe

VII-B-2 - Le serveur Tcp générique

Maintenant nous nous intéressons à un serveur

- qui affiche à l'écran les commandes envoyées par ses clients

- leur envoie comme réponse les lignes de texte tapées au clavier par un utilisateur. C'est donc ce dernier qui fait office de serveur.

Le programme est lancé par : java serveurTCPgenerique portEcoute, où portEcoute est le port sur lequel les clients doivent se connecter. Le service au client sera assuré par deux threads :

- un thread se consacrant exclusivement à la lecture des lignes de texte envoyées par le client

- un thread se consacrant exclusivement à la lecture des réponses tapées au clavier par l'utilisateur. Celui-ci signalera par la commande fin qu'il clôt la connexion avec le client.

Le serveur crée deux threads par client. S'il y a n clients, il y aura 2n threads actifs en même temps. Le serveur lui ne s'arrête jamais sauf par un Ctrl-C tapé au clavier par l'utilisateur. Voyons quelques exemples.

Le serveur est lancé sur le port 100 et on utilise le client générique pour lui parler. La fenêtre du client est la suivante :

1. dos> java clientTCPgenerique localhost 100

2. Commandes :

3. commande 1 du client 1

4. <-- réponse 1 au client 1

5. commande 2 du client 1

6. <-- réponse 2 au client 1

7. fin

8. L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.

9. [fin du thread de lecture des réponses du serveur]

10. [fin du thread d'envoi des commandes au serveur]

Les lignes commençant par <-- sont celles envoyées du serveur au client, les autres celles du client vers le serveur. La fenêtre du serveur est la suivante :

1. Dos> java serveurTCPgenerique 100

2. Serveur générique lancé sur le port 100

3. Thread de lecture des réponses du serveur au client 1 lancé

4. 1 : Thread de lecture des demandes du client 1 lancé

5. <-- commande 1 du client 1

6. réponse 1 au client 1

7. 1 : <-- commande 2 du client 1

8. réponse 2 au client 1

9. 1 : [fin du Thread de lecture des demandes du client 1]

10. fin

11. [fin du Thread de lecture des réponses du serveur au client 1]

Les lignes commençant par <-- sont celles envoyées du client au serveur. Les lignes N : sont les lignes envoyées du serveur au client n° N. Le serveur ci-dessus est encore actif alors que le client 1 est terminé. On lance un second client pour le même serveur :

1. Dos> java clientTCPgenerique localhost 100

2. Commandes :

3. commande 3 du client 2

4. <-- réponse 3 au client 2

5. fin

6. L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.

7. [fin du thread de lecture des réponses du serveur]

8. [fin du thread d'envoi des commandes au serveur]

La fenêtre du serveur est alors celle-ci :

1. Dos> java serveurTCPgenerique 100

2. Serveur générique lancé sur le port 100

3. Thread de lecture des réponses du serveur au client 1 lancé

4. 1 : Thread de lecture des demandes du client 1 lancé

5. <-- commande 1 du client 1

6. réponse 1 au client 1

7. 1 : <-- commande 2 du client 1

8. réponse 2 au client 1

9. 1 : [fin du Thread de lecture des demandes du client 1]

10. fin

11. [fin du Thread de lecture des réponses du serveur au client 1]

12. Thread de lecture des réponses du serveur au client 2 lancé

13. 2 : Thread de lecture des demandes du client 2 lancé

14. <-- commande 3 du client 2

15. réponse 3 au client 2

16. 2 : [fin du Thread de lecture des demandes du client 2]

17. fin

18. [fin du Thread de lecture des réponses du serveur au client 2]

19. ^C

Simulons maintenant un serveur web en lançant notre serveur générique sur le port 88 :

1. Dos> java serveurTCPgenerique 88

2. Serveur générique lancé sur le port 88

Prenons maintenant un navigateur et demandons l'URL http://localhost:88/exemple.html. Le navigateur va alors se connecter sur le port 88 de la machine localhost puis demander la page /exemple.html :

Regardons maintenant la fenêtre de notre serveur :

1. Dos>java serveurTCPgenerique 88

2. Serveur générique lancé sur le port 88

3. Thread de lecture des réponses du serveur au client 2 lancé

4. 2 : Thread de lecture des demandes du client 2 lancé

5. <-- GET /exemple.html HTTP/1.1

6. <-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/msword, */*

7. <-- Accept-Language: fr

8. <-- Accept-Encoding: gzip, deflate

9. <-- User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705; .NET CLR 1.0.2

10. 914)

11. <-- Host: localhost:88

12. <-- Connection: Keep-Alive

13. <--

On découvre ainsi les entêtes HTTP envoyés par le navigateur. Cela nous permet de découvrir peu à peu le protocole HTTP. Lors d'un précédent exemple, nous avions créé un client Web qui n'envoyait que la seule commande GET. Cela avait été suffisant. On voit ici que le navigateur envoie d'autres informations au serveur. Elles ont pour but d'indiquer au serveur quel type de client il a en face de lui. On voit aussi que les entêtes HTTP se terminent par une ligne vide.

Elaborons une réponse à notre client. L'utilisateur au clavier est ici le véritable serveur et il peut élaborer une réponse à la main. Rappelons-nous la réponse faite par un serveur Web dans un précédent exemple :

1. <-- HTTP/1.1 200 OK

2. <-- Date: Mon, 13 May 2002 07:30:58 GMT

3. <-- Server: Apache/1.3.12 (Unix)  (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21

4. <-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT

5. <-- ETag: "23432-2bf3-3c60f0ca"

6. <-- Accept-Ranges: bytes

7. <-- Content-Length: 11251

8. <-- Connection: close

9. <-- Content-Type: text/html

10. <--

11. <-- <html>

Essayons de donner une réponse analogue :

1. ...

2. <-- Host: localhost:88

3. <-- Connection: Keep-Alive

4. <--

5. 2 : HTTP/1.1 200 OK

6. 2 : Server: serveur tcp generique

7. 2 : Connection: close

8. 2 : Content-Type: text/html

9. 2 :

10. 2 : <html>

11. 2 :   <head><title>Serveur generique</title></head>

12. 2 :   <body>

13. 2 :     <center>

14. 2 :       <h2>Reponse du serveur generique</h2>

15. 2 :     </center>

16. 2 :    </body>

17. 2 : </html>

18. 2 : fin

19. L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.

20. [fin du Thread de lecture des demandes du client 2]

21. [fin du Thread de lecture des réponses du serveur au client 2]

Les lignes commençant par 2 : sont envoyées du serveur au client n° 2. La commande fin clôt la connexion du serveur au client. Nous nous sommes limités dans notre réponse aux entêtes HTTP suivants :

1. HTTP/1.1 200 OK

2. 2 : Server: serveur tcp generique

3. 2 : Connection: close

4. 2 : Content-Type: text/html

5. 2 :

Nous ne donnons pas la taille du fichier que nous allons envoyer (Content-Length) mais nous contentons de dire que nous allons fermer la connexion (Connection: close) après envoi de celui-ci. Cela est suffisant pour le navigateur. En voyant la connexion fermée, il saura que la réponse du serveur est terminée et affichera la page HTML qui lui a été envoyée. Cette dernière est la suivante :

1. 2 : <html>

2. 2 :   <head><title>Serveur generique</title></head>

3. 2 :   <body>

4. 2 :     <center>

5. 2 :       <h2>Reponse du serveur generique</h2>

6. 2 :     </center>

7. 2 :    </body>

8. 2 : </html>

L'utilisateur ferme ensuite la connexion au client en tapant la commande fin. Le navigateur sait alors que la réponse du serveur est terminée et peut alors l'afficher :

Si ci-dessus, on fait View/Source pour voir ce qu'a reçu le navigateur, on obtient :

c'est-à-dire exactement ce qu'on a envoyé depuis le serveur générique.

Le code du serveur TCP générique est le suivant :

1. // paquetages

2. import java.io.*;

3. import java.net.*;

5. public class serveurTCPgenerique{

7. // programme principal

8. public static void main (String[] args){

10. // reçoit le port d'écoute des demandes des clients

11. // crée un thread pour lire les demandes du client

12. // celles-ci seront affichées à l'écran

13. // crée un thread pour lire des commandes tapées au clavier

14. // celles-ci seront envoyées comme réponse au client

15. // le tout se termine avec la commande fin tapée au clavier

17. final String syntaxe="Syntaxe : pg port";

18. // variable d'instance

19. // y-a-t-il un argument

20. if(args.length != 1)

21. erreur(syntaxe,1);

23. // le port doit être entier >0

24. int port=0;

25. boolean erreurPort=false;

26. Exception E=null;

27. try{

28. port=Integer.parseInt(args[0]);

29. }catch(Exception e){

30. E=e;

31. erreurPort=true;

32. }

33. erreurPort=erreurPort || port <=0;

34. if(erreurPort)

35. erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);

37. // on crée le servive d'écoute

38. ServerSocket ecoute=null;

39. int nbClients=0;    // nbre de clients traités

40. try{

41. // on crée le service

42. ecoute=new ServerSocket(port);

43. // suivi

44. System.out.println("Serveur générique lancé sur le port " + port);

46. // boucle de service aux clients

47. Socket client=null;

48. while (true){ // boucle infinie - sera arrêtée par Ctrl-C

49. // attente d'un client

50. client=ecoute.accept();

52. // le service est assuré des threads séparés

53. nbClients++;

55. // on crée les threads de lecture/écriture

56. new ServeurSend(client,nbClients).start();

57. new ServeurReceive(client,nbClients).start();

59. // on retourne à l'écoute des demandes

60. }// fin while

61. }catch(Exception ex){

62. // on signale l'erreur

63. erreur("L'erreur suivante s'est produite : " + ex.getMessage(),3);

64. }//catch

65. }// fin main

67. // affichage des erreurs

68. public static void erreur(String msg, int exitCode){

69. // affichage erreur

70. System.err.println(msg);

71. // arrêt avec erreur

72. System.exit(exitCode);

73. }//erreur

74. }//classe

76. class ServeurSend extends Thread{

77. // classe chargée de lire des réponses tapées au clavier

78. // et de les envoyer à un client via un client tcp passé au constructeur

80. Socket client;    // le client tcp

81. int numClient;        // n° de client

83. // constructeur

84. public ServeurSend(Socket client, int numClient){

85. // on note le client tcp

86. this.client=client;

87. // et son n°

88. this.numClient=numClient;

89. }//constructeur

91. // méthode Run du thread

92. public void run(){

94. // données locales

95. PrintWriter OUT=null;        // flux d'écriture réseau

96. String réponse=null;        // réponse lue au clavier

97. BufferedReader IN=null;    // flux clavier

99. // suivi

100. System.out.println("Thread de lecture des réponses du serveur au client "+ numClient + " lancé");

101. // gestion des erreurs

102. try{

103. // création du flux d'écriture réseau

104. OUT=new PrintWriter(client.getOutputStream(),true);

105. // création du flux clavier

106. IN=new BufferedReader(new InputStreamReader(System.in));

107. // boucle saisie-envoi des commandes

108. while(true){

109. // identification client

110. System.out.print("--> " + numClient + " : ");

111. // lecture réponse tapée au clavier

112. réponse=IN.readLine().trim();

113. // fini ?

114. if (réponse.toLowerCase().equals("fin")) break;

115. // envoi réponse au serveur

116. OUT.println(réponse);

117. // réponse suivante

118. }//while

119. }catch(Exception ex){

120. // erreur

121. System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());

122. }//catch

123. // fin - on ferme les flux

124. try{

125. OUT.close();client.close();

126. }catch(Exception ex){}

127. // on signale la fin du thread

128. System.out.println("[fin du Thread de lecture des réponses du serveur au client "+ numClient+ "]");

129. }//run

130. }//classe

132. class ServeurReceive extends Thread{

133. // classe chargée de lire les lignes de texte envoyées au serveur

134. // via un client tcp passé au constructeur

136. Socket client;    // le client tcp

137. int numClient;        // n° de client

139. // constructeur

140. public ServeurReceive(Socket client, int numClient){

141. // on note le client tcp

142. this.client=client;

143. // et son n°

144. this.numClient=numClient;

145. }//constructeur

147. // méthode Run du thread

148. public void run(){

150. // données locales

151. BufferedReader IN=null;        // flux lecture réseau

152. String réponse=null;        // réponse serveur

154. // suivi

155. System.out.println("Thread de lecture des demandes du client "+ numClient + " lancé");

156. // gestion des erreurs

157. try{

158. // création du flux lecture réseau

159. IN=new BufferedReader(new InputStreamReader(client.getInputStream()));

160. // boucle lecture lignes de texte du flux IN

161. while(true){

162. // lecture flux réseau

163. réponse=IN.readLine();

164. // flux fermé ?

165. if(réponse==null) break;

166. // affichage

167. System.out.println("<-- "+réponse);

168. }//while

169. }catch(Exception ex){

170. // erreur

171. System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());

172. }//catch

173. // fin - on ferme les flux

174. try{

175. IN.close();client.close();

176. }catch(Exception ex){}

177. // on signale la fin du thread

178. System.out.println("[fin du Thread de lecture des demandes du client "+ numClient+"]");

179. }//run

180. }//classe

VII-C - Javascript

Nous montrons dans cette partie trois exemples d'utilisation de Javascript dans les pages WEB. Nous nous centrons sur la gestion des formulaires mais Javascript peut faire bien davantage.

VII-C-1 - Récupérer les informations d'un formulaire

L'exemple ci-dessous montre comment récupérer au sein du navigateur les données entrées par l'utilisateur au sein d'un formulaire. Cela permet en général de faire des prétraitements avant de les envoyer au serveur.

VII-C-1-a - Le formulaire

On a un formulaire rassemblant les composants les plus courants et d'un bouton Afficher permettant d'afficher les saisies faites par l'utilisateur.

VII-C-1-b - Le code

1. <html>

3. <head>

4. <title>Un formulaire traité par Javascript</title>

5. <script language="javascript">

6. function afficher(){

7. // affiche dans une liste les infos du formulaire

9. // on efface d'abord

10. effacerInfos();

12. // on affiche la valeur des # champs

13. with(document.frmExemple){

14. // champ caché

15. ecrire("champ caché="+cache.value);

16. // champ textuel simple

17. ecrire("champ textuel simple="+simple.value);

18. // champ textuel multiple

19. ecrire("champ textuel multiple="+lignes.value);

20. // boutons radio

21. for(i=0;i<radio.length;i++){

22. texte="radio["+i+"]="+radio[i].value;

23. if(radio[i].checked) texte+=", coché";

24. ecrire(texte);

25. }//for

26. //  cases à cocher

27. for(i=0;i<qcm.length;i++){

28. texte="qcm["+i+"]="+qcm[i].value;

29. if(qcm[i].checked) texte+=", coché";

30. ecrire(texte);

31. }//for

32. //liste déroulante

33. ecrire("index sélectionné dans le menu="+menu.selectedIndex);

34. for(i=0;i<menu.length;i++){

35. texte="menu["+i+"]="+menu.options[i].text;

36. if(menu.options[i].selected) texte+=",sélectionné";

37. ecrire(texte);

38. }//for

39. //liste à choix multiple

40. for(i=0;i<lstVoitures.length;i++){

41. texte="lstVoitures["+i+"]="+lstVoitures.options[i].text;

42. if(lstVoitures.options[i].selected) texte+=",sélectionné";

43. ecrire(texte);

44. }//for

45. //mot de passe

46. ecrire("mot de passe="+passwd.value);

47. }//with

48. }//function

50. function ecrire(texte){

51. // écrit texte dans la liste des infos

52. frmInfos.lstInfos.options[frmInfos.lstInfos.length]=new Option(texte);

53. }//écrire

55. function effacerInfos(){

56. frmInfos.lstInfos.length=0;

57. }//effacerInfos

58. </script>

59. </head>

61. <body bgcolor="#C0C0C0" onload="afficher()">

62. <center>

63. <h2>Un formulaire traité par Javascript</h2>

64. <hr>

65. <form method="POST" name="frmExemple">

66. <input type="hidden" name="cache" value="secret">

67. <table border="0">

68. <tr>

69. <td align="center">Un champ textuel simple</td>

70. <td align="center" width="100">&nbsp;</td>

71. <td align="center">Un champ textuel sur plusieurs lignes</td>

72. </tr>

73. <tr>

74. <td align="center"><input type="text" size="20" name="simple"></td>

75. <td align="center" width="100">&nbsp;</td>

76. <td align="center">

77. <textarea name="lignes" rows="2" cols="40">Ce texte est modifiable</textarea>

78. </td>

79. </tr>

80. </table>

81. <table border="0">

82. <tr>

83. <td><strong>Des boutons radio :</strong></td>

84. <td>

85. <input type="radio" checked name="radio" value="FM">FM

86. </td>

87. <td>

88. <input type="radio" name="radio" value="GO">GO

89. </td>

90. <td>

91. <input type="radio" name="radio" value="PO">PO

92. </td>

93. <td>&nbsp;</td>

94. <td><strong>Des choix multiples :</strong></td>

95. <td>

96. <input type="checkbox" name="qcm" value="un">un

97. </td>

98. <td>

99. <input type="checkbox" name="qcm" value="deux">deux

100. </td>

101. <td>

102. <input type="checkbox" name="qcm" value="trois">trois

103. </td>

104. </tr>

105. </table>

106. <table border="0">

107. <tr>

108. <td>Un menu déroulant : </td>

109. <td>

110. <select name="menu" size="1">

111. <option>50 F</option>

112. <option>60 F</option>

113. <option>70 F</option>

114. <option>100 F</option>

115. </select>

116. </td>

117. <td>Une liste :</td>

118. <td>

119. <select name="lstVoitures" multiple size="3">

120. <option>Renault</option>

121. <option>Citroën</option>

122. <option>Peugeot</option>

123. <option>Fiat</option>

124. <option>Audi</option>

125. </select>

126. </td>

127. </tr>

128. </table>

129. <table border="0">

130. <tr>

131. <td>Un mot de passe : </td>

132. <td><input type="password" size="21" name="passwd"></td>

133. <td>&nbsp;</td>

134. <td>Un champ de contexte caché : </td>

135. </tr>

136. </table>

137. </form>

138. <hr>

139. <h2>Informations du formulaire</h2>

140. <form name="frmInfos">

141. <table>

142. <tr>

143. <td><input type="button" value="Effacer" onclick="effacerInfos()"></td>

144. <td>

145. <select name="lstInfos" multiple size="3">

146. </select>

147. </td>

148. <td>

149. <input type="button" name="cmdAfficher" value="Afficher" onclick="afficher()">

150. </td>

151. </tr>

152. </form>

153. </body>

154. </html>

VII-C-2 - Les expressions régulières en Javascript

Côté navigateur, Javascript peut être utilisé pour vérifier la validité des données entrées par l'utilisateur avant de les envoyer au serveur. Voici un programme de test de ces expressions régulières.

VII-C-2-a - La page de test

VII-C-2-b - Le code de la page

1. <html>

3. <head>

4. <title>Les expressions régulières en Javascript</title>

5. <script language="javascript">

6. function afficherInfos(){

7. with(document.frmRegExp){

8. // qq chose à faire ?

9. if (! verifier()) return;

10. // c'est bon - on efface les résultats précédents

11. effacerInfos();

12. // vérification du modèle

13. modele=new RegExp(txtModele.value);

14. champs=modele.exec(txtChaine.value);

15. if(champs==null)

16. // pas de correspondance entre modèle et chaîne

17. ecrireInfos("pas de correspondance");

18. else{

19. // correspondance - on affiche les résultats obtenus

20. ecrireInfos("Il y a correspondance");

21. for(i=0;i<champs.length;i++)

22. ecrireInfos("champs["+i+"]=["+champs[i]+"]");

23. }//else

24. }//with

25. }//function

27. function ecrireInfos(texte){

28. // écrit texte dans la liste des infos

29. document.frmRegExp.lstInfos.options[document.frmRegExp.lstInfos.length]=new Option(texte);

30. }//écrire

32. function effacerInfos(){

33. frmRegExp.lstInfos.length=0;

34. }//effacerInfos

36. function jouer(){

37. // teste le modèle contre la chaîne dans l'exemple choisi

38. with(document.frmRegExp){

39. txtModele.value=lstModeles.options[lstModeles.selectedIndex].text

40. txtChaine.value=lstChaines.options[lstChaines.selectedIndex].text

41. afficherInfos();

42. }//with

43. }//jouer

45. function ajouter(){

46. //ajoute le test courant aux exemples

47. with(document.frmRegExp){

48. // qq chose à faire ?

49. if (! verifier()) return;

50. // ajout

51. lstModeles.options[lstModeles.length]=new Option(txtModele.value);

52. lstChaines.options[lstChaines.length]=new Option(txtChaine.value);

53. // raz saisies

54. txtModele.value="";

55. txtChaine.value="";

56. }//with

57. }//ajouter

59. function verifier(){

60. // vérifie que les champs de saisie sont non vides

61. with(document.frmRegExp){

62. champs=/^\s*$/.exec(txtModele.value);

63. if(champs!=null){

64. alert("Vous n'avez pas indiqué de modèle");

65. txtModele.focus();

66. return false;

67. }//if

68. champs=/^\s*$/.exec(txtChaine.value);

69. if(champs!=null){

70. alert("Vous n'avez pas indiqué de chaîne de test");

71. txtChaine.focus();

72. return false;

73. }//if

74. // c'est bon

75. return true;

76. }//with

77. }//verifier

78. </script>

79. </head>

81. <body bgcolor="#C0C0C0">

82. <center>

83. <h2>Les expressions régulières en Javascript</h2>

84. <hr>

85. <form name="frmRegExp">

86. <table>

87. <tr>

88. <td>Expression régulière</td>

89. <td>Chaîne de test</td>

90. </tr>

91. <tr>

92. <td><input type="text" name="txtModele" size="20"></td>

93. <td><input type="text" name="txtChaine" size="20"></td>

94. </tr>

95. <tr>

96. <td>

97. <input type="button" name="cmdAfficher" value="Jouer le test" onclick="afficherInfos()">

98. </td>

99. <td>

100. <input type="button" name="cmdAjouter" value="Ajouter aux exemples" onclick="ajouter()">

101. </td>

102. </tr>

103. </table>

104. <hr>

105. <h2>Résultats de l'instruction champs=expression régulière.exec(chaine)</h2>

106. <table>

107. <tr>

108. <td>

109. <select name="lstInfos" size="3">

110. </select>

111. </td>

112. </tr>

113. </table>

114. <hr>

115. <h2>Exemples</h2>

116. <table>

117. <tr>

118. <td align="center">Modèles</td>

119. <td align="center">Chaînes</td>

120. </tr>

121. <tr>

122. <td>

123. <select name="lstModeles" size="1">

124. <option>^\d+$</option>

125. <option>^(\d+) (\d+)$</option>

126. <option>^(\d+)(.*)(\d+)$</option>

127. <option>^(\d+)(\s+)(\d+)$</option>

128. </select>

129. </td>

130. <td>

131. <select name="lstChaines" size="1">

132. <option>67</option>

133. <option>56 84</option>

134. <option>45abcd67</option>

135. <option>45   67</option>

136. </select>

137. </td>

138. <td>

139. <input type="button" name="cmdJouer" value="Jouer l'exemple" onclick="jouer()">

140. </td>

141. </tr>

142. </form>

143. </body>

144. </html>

VII-C-3 - Gestion des listes en JavaScript

VII-C-3-a - Le formulaire

VII-C-3-b - Le code

1. <html>

3. <head>

4. <title>Les listes en Javascript</title>

6. <script language="javascript">

7. // ajouter

8. function ajouter(L1,L2,T){

9. // ajoute la valeur du champ T aux listes L1,L2

10. // qq chose à faire ?

11. champs=/^\s*$/.exec(T.value);

12. if(champs!=null){

13. // le champ est vide

14. alert("Vous n'avez pas indiqué la valeur à ajouter");

15. txtElement.focus();

16. return;

17. }//if

18. // on ajoute l'élément

19. L1.options[L1.length]=new Option(T.value);

20. L2.options[L2.length]=new Option(T.value);

21. T.value="";

22. }//ajouter

24. //vider

25. function vider(L){

26. // vide la liste L

27. L.length=0;

28. }//vider

30. //transfert

31. function transfert(L1,L2,simple){

32. //transfére dans L2 les éléments sélectionnés dans la liste L1

34. // qq chose à faire ?

35. // index de l'élément sélectionné dans L1

36. index1=L1.selectedIndex;

37. if(index1==-1){

38. alert("Vous n'avez pas sélectionné d'élément");

39. return;

40. }//if

41. // quel est le mode de sélection des éléments des listes

42. if(simple){ // sélection simple

43. element1=L1.options[index1].text;

44. //ajout dans L2

45. L2.options[L2.length]=new Option(element1);

46. //suppression dans L1

47. L1.options[index1]=null;

48. }//simple

49. if(! simple){ //sélection multiple

50. //on parcourt la liste 1 en sens inverse

51. for(i=L1.length-1;i>=0;i--){

52. //élément sélectionné ?

53. if(L1.options[i].selected){

54. //on l'ajoute à L2

55. L2.options[L2.length]=new Option(L1.options[i].text);

56. //on le supprime de L1

57. L1.options[i]=null;

58. }//if

59. }//for i

60. }//if ! simple

61. }//transfert

62. </script>

63. </head>

65. <body bgcolor="#C0C0C0">

66. <center>

67. <h2>Les listes en Javascript</h2>

68. <hr>

69. <form name="frmListes">

70. <table>

71. <tr>

72. <td>

73. <input type="button" name="cmdAjouter" value="Ajouter" onclick="ajouter(lst1A,lst1B,txtElement)">

74. </td>

75. <td>

76. <input type="text" name="txtElement">

77. </td>

78. </tr>

79. </table>

80. <table>

81. <tr>

82. <td align="center">liste 1</td>

83. <td align="center"><input type="button" value=">>" onclick="transfert(lst1A,lst2A,true)"</td>

84. <td align="center"><input type="button" value="<<" onclick="transfert(lst2A,lst1A,true)"</td>

85. <td align="center">liste 2</td>

86. <td width="30"></td>

87. <td align="center">liste 1</td>

88. <td align="center"><input type="button" value=">>" onclick="transfert(lst1B,lst2B,false)"</td>

89. <td align="center"><input type="button" value="<<" onclick="transfert(lst2B,lst1B,false)"</td>

90. <td align="center">liste 2</td>

91. </tr>

92. <tr>

93. <td></td>

94. <td align="center">

95. <select name="lst1A" size="5">

96. </select>

97. </td>

98. <td align="center">

99. <select name="lst2A" size="5">

100. </select>

101. </td>

102. <td></td>

103. <td></td>

104. <td></td>

105. <td align="center">

106. <select name="lst1B" size="5" multiple >

107. </select>

108. </td>

109. <td align="center">

110. <select name="lst2B" size="5" multiple>

111. </select>

112. </td>

114. </tr>

115. <tr>

116. <td></td>

117. <td align="center"><input type="button" value="Vider" onclick="vider(lst1A)"</td>

118. <td align="center"><input type="button" value="Vider" onclick="vider(lst2A)"</td>

119. <td></td>

120. <td></td>

121. <td></td>

122. <td align="center"><input type="button" value="Vider" onclick="vider(lst1B)"</td>

123. <td align="center"><input type="button" value="Vider" onclick="vider(lst2B)"</td>

124. <td></td>

125. </tr>

126. <tr>

127. <td></td>

128. <td colspan="2"><strong>Sélection simple</strong></td>

129. <td></td>

130. <td></td>

131. <td></td>

132. <td colspan="2"><strong>Sélection multiple</strong></td>

133. <td></td>

134. </tr>

136. </table>

137. <hr>

138. </form>

139. </body>

140. </html>

- Étude de cas -

VII-D - Gestion d'une base d'articles sur le web

Objectifs :

- écrire une classe de gestion d'une base de données d'articles

- écrire une application web s'appuyant sur cette classe

- introduire les feuilles de style

- proposer un début de méthodologie de développement pour des applications web simples

- introduire javascript dans le navigateur client

Crédits : L'essence de cette étude de cas a été trouvée dans le livre "Les cahiers du programmeur - PHP/MySQL" de Jean-Philippe Lebœuf aux éditions Eyrolles.

VII-D-1 - Introduction

Un commerçant désire gérer les articles qu'il vend en magasin. Il a déjà chez lui une application ACCESS qui fait ce travail mais il est tenté par l'aventure du web. Il a un compte chez un fournisseur d'accès internet, fournisseur autorisant ses clients à installer des scripts PHP dans leurs dossiers personnels. Ceci leur permet de créer des sites web dynamiques. Par ailleurs, ces mêmes clients disposent d'un compte MySQL leur permettant de créer des tables pouvant servir des données à leurs scripts PHP. Ainsi le commerçant a un compte MySQL ayant pour login admarticles et mot de passe mdparticles. Il possède une base dbarticles sur laquelle il a tous les droits. Notre commerçant a donc les éléments suffisants pour mettre sa gestion d'articles sur le web. Aidé par vous qui avez des compétences dans le développement Web, il se lance dans l'aventure.

VII-D-2 - La base de données

Notre commerçant fait la maquette suivante de l'interface web d'accueil qu'il souhaiterait :

Il y aurait deux sortes d'utilisateurs :

- des administrateurs qui pourraient tout faire sur la table des articles (ajouter, modifier, supprimer, consulter…). Ceux-là pourront utiliser tous les éléments du menu ci-dessus. En particulier, ils pourront émettre toute requête SQL via l'option [Requête SQL].

- les utilisateurs normaux (pas administrateurs) qui auraient des droits restreints : droits d'ajouter, de modifier, de supprimer, de consulter. Ils peuvent n'avoir que certains de ces droits, le seul droit de consultation par exemple.

Du fait qu'il y a divers types d'utilisateurs de la base n'ayant pas les mêmes droits, il y a nécessité d'une authentification. C'est pourquoi la page d'accueil commence avec celle-ci. Pour savoir qui est qui, et qui a le droit de faire quoi, deux tables USERS et DROITS seront utilisées. La table USERS aurait la structure suivante :

Le contenu de la table pourrait être le suivant :

La table DROITS précise les droits qu'ont les utilisateurs non administrateurs présents dans la table USERS. Sa structure est la suivante :

Le contenu de la table pourrait être le suivant :

Remarques :

- Un utilisateur U se trouvant dans la table USERS et absent de la table DROITS n'a aucun droit.

- Dans notre exemple, les utilisateurs n'auront accès qu'à une seule table, la table ARTICLES. Mais notre commerçant, prévoyant, a néanmoins ajouté le champ table à la structure de la table DROITS afin de se donner la possibilité d'ajouter ultérieurement de nouvelles tables à son application.

- Pourquoi gérer des droits dans nos propres tables alors qu'on fait l'hypothèse qu'on utilisera une base MySQL capable elle-même (et mieux que nous) de gérer ces droits dans ses propres tables ? Simplement parce que notre commerçant n'a pas les droits d'administration sur la base MySQL qui lui permettraient de créer des utilisateurs et de leur donner des droits. N'oublions pas en effet que la base MySQL est hébergée chez un fournisseur d'accès et que le commerçant n'est qu'un simple utilisateur de celle-ci sans aucun droit d'administration (heureusement). Il possède cependant tous les droits sur une base appelée dbarticles à laquelle il accède pour le moment avec le login admarticles et le mot de passe mdparticles. C'est dans cette base que prennent place toutes les tables de l'application.

La table ARTICLES rassemble les informations sur les articles vendus par le commerçant. Sa structure est la suivante :

Son contenu utilisé dans un premier temps comme test pourrait être le suivant :

VII-D-3 - Les contraintes du projet

Le commerçant migre ici une application ACCESS locale en une application Web. Il ne sait ce que deviendra celle-ci et comment elle évoluera. Il voudrait cependant que la nouvelle application soit facile à utiliser et évolutive. C'est pour cette raison que son conseiller informatique a imaginé pour lui, lors de la conception des tables, qu'il pouvait y avoir :

1. divers utilisateurs avec différents droits : cela permettra au commerçant de déléguer certaines tâches à d'autres personnes sans pour autant leur donner des droits d'administration

2. dans le futur d'autres tables que la table ARTICLES

Le même conseiller fait d'autres propositions :

- il sait que dans le développement logiciel, il faut séparer nettement les couches présentation et les couches traitement. L'architecture d'une application web est souvent la suivante :

L'interface utilisateur est ici un navigateur web mais cela pourrait être également une application autonome qui via le réseau enverrait des requêtes HTTP au service web et mettrait en forme les résultats que celui-ci lui envoie. La logique applicative est constituée des scripts traitant les demandes de l'utilisateur, ici des scripts PHP. La source de données est souvent une base de données mais cela peut être aussi un annuaire LDAP ou un service web distant. Le développeur a intérêt à maintenir une grande indépendance entre ces trois entités afin que si l'une d'elles change, les deux autres n'aient pas à changer ou peu. Le conseiller informatique du commerçant fait alors les propositions suivantes :

- On mettra la logique métier de l'application dans une classe PHP. Ainsi le bloc [Logique applicative] ci-dessus sera constitué des éléments suivants :

Dans le bloc [Logique Applicative], on pourra distinguer

- le bloc [IE=Interface d'Entrée] qui est la porte d'entrée de l'application. Elle est la même quelque soit le type de client.

- le bloc [Classes métier] qui regroupe des classes nécessaires à la logique de l'application. Elles sont indépendantes du client.

- le bloc des générateurs des pages réponse [IS1 IS2 … IS=Interface de Sortie]. Chaque générateur est chargé de mettre en forme les résultats fournis par la logique applicative pour un type de client donné : code HTML pour un navigateur ou un téléphone WAP, code XML pour une application autonome…

Ce modèle assure une bonne indépendance vis à vis des clients. Que le client change ou qu'on veuille faire évoluer sa façon de présenter les résultats, ce sont les générateurs de sortie [IS] qu'il faudra créer ou adapter.

- Dans une application web, l'indépendance entre la couche présentation et la couche traitement peut être améliorée par l'utilisation de feuilles de style. Celles-ci gouvernent la présentation d'une page Web au sein d'un navigateur. Pour changer cette présentation, il suffit de changer la feuille de style associée. Il n'y a pas à toucher à la logique de traitement. On utilisera donc ici une feuille de style.

- Dans le diagramme ci-dessus, c'est la classe métier qui fera l'interface avec la source de données. Par hypothèse, cette source est ici une base MySQL. Afin de permettre une évolution vers une autre base de données, on utilisera la bibliothèque PEAR qui offre des classes d'accès aux bases de données indépendantes du type réel de celles-ci. Ainsi si notre commerçant s'enrichit au point de pouvoir installer un serveur web IIS de Microsoft dans son entreprise, il pourra remplacer la base MySQL par SQL Server sans avoir (ou très peu) à changer la classe métier.

VII-D-4 - La classe articles

La classe articles pourrait être définie de la façon suivante :

1. <?php

3. // classe articles travaillant sur une base d'articles composée des tables suivantes

4. // articles : (code, nom, prix, stockActuel, stockMinimum)

5. // users : (login, mdp, admin)

6. // droits : (login, table, ajouter, modifier, supprimer, consulter)

8. // c'est l'utilisateur de la classe qui doit fournir le login/mdp qui permet de faire toute opération sur la base

9. // il a donc déjà tous les droits sur la base. Ceci implique qu'il n'y a pas à prendre

10. // de précautions de sécurité particulières ici

12. // bibliothèques

13. require_once 'DB.php';

15. class articles{

17. // attributs

18. var $sDSN;                        // la chaîne de connexion

19. var $sDatabase;            // le nom de la base

20. var $oDB;                        // connexion à la base

21. var $aErreurs;                // liste d'erreurs

22. var $oRésultats;            // résultat d'une requête select

23. var $connecté;                // booléen qui indique si on est connecté ou non à la base

24. var $sQuery;                    // la dernière requête exécutée

25. var $sUser;                    // identité de l'utilisateur de la connexion

26. var $bAdmin;                    // à vrai si l'utilisateur est administrateur

27. var $dDroits;                // le dictionnaire de ses droits table ->> array(consulter,ajouter,supprimer,modifier)

29. // constructeur

30. function articles($dDSN,$sUser,$sMdp){

32. // $dDSN : dictionnaire définissant la liaison à établir

33. // $dDSN['sgbd'] : le type du SGBD auquel il faut se connecter

34. // $dDSN['host'] : le nom de la machine hôte qui l'héberge

35. // $dDSN['database'] : le nom de la base à laquelle il faut se connecter

36. // $dDSN['admin'] : le login du propiétaire de la base à laquelle il faut se connecter

37. // $dDSN['mdpadmin'] : son mot de passe

38. // $sUser : le login de l'utilisateur qui veut exploiter la base des articles

39. // $sMdp : son mot de passe

41. // crée dans $oDB une connexion à la base définie par $dDSN sous l'identité de $dDSN['admin']

42. // si la connexion réussit et que l'utilisateur $sUser est authentifié

43. // charge les droits dans $bAdmin et $dDroits les droits de l'utilisateur $sUser

44. // met dans $sDSN la chaîne de connexion à la base

45. // met dans $sDataBase le nom de la base à laquelle on se connecte

46. // met $connecté à vrai

47. // si la connexion échoue ou si l'utilisateur $sUser n'est pas identifié correctement

48. // met les msg d'erreurs adéquats dans la liste $aErreurs

49. // ferme la connexion si besoin est

50. // met $connecté à faux

52. ...

53. }//constructeur

55. // ------------------------------------------------------------------

56. function connect(){

57. // (re)connexion à la base

58. ...

59. }//connect

61. // ------------------------------------------------------------------

62. function disconnect(){

63. // on ferme la connexion à la base $sDSN

64. ...

65. }//disconnect

67. // -------------------------------------------------------------------

68. function execute($sQuery,$bAdmin){

69. // $sQuery : requête à exécuter

70. // $bAdmin : vrai si demande d'exécution en tant qu'administrateur

71. ...

72. }//execute

74. // --------------------------------------------------------------------------

75. function addArticle($dArticle){

76. // ajoute un article $dArticle (code, nom, prix, stockActuel, stockMinimum) à la table des articles

77. ...

78. }//add

80. // ----------------------------------------------------------------------

81. function modifyArticle($dArticle){

82. // modifie un article $dArticle (code, nom, prix, stockActuel, stockMinimum) de la table des articles

83. ...

84. }//update

86. // ----------------------------------------------------------------------

87. function deleteArticle($sCode){

88. // supprime un article de la table des articles

89. // dont on a le code $sCode

90. ...

91. }//delete

93. // ----------------------------------------------------------------------

94. function vérifierArticle(&$dArticle){

95. // vérifie la validité d'un article $dArticle (code, nom, prix, stockActuel, stockMinimum)

96. ...

97. }//vérifier

99. // --------------------------------------------------------------------------

100. function selectArticles($dQuery){

101. // exécute une requête select la table des articles

102. // celle-ci a trois composantes

103. // liste des colonnes dans $dQuery['colonnes']

104. // filtrage dans $dQuery['where']

105. // ordre de présentation dans $dQuery['orderby']

106. ...

107. }//selectArticles

109. // --------------------------------

110. function existeArticle($sCode){

111. // rend TRUE si l'article de code $sCode existe dans la table des articles

112. ...

113. }//existeArticle

115. // --------------------------------------

116. function existeUser($sUser,$sMdp){

117. // vérification de l'existence de l'utilisateur $sUser ayant le mot de passe $sMdp

118. // rend (int $iErreur, string $sAdmin, hashtable $dDroits)

119. // $iErreur = -1 pour toute erreur d'exploitation de la base - la liste $aErreurs est alors renseignée

120. // $iErreur = 1 si l'utilisateur n'est pas trouvé (absent ou pas bon mot de passe)

121. // $iErreur = 2 si l'utilisateur existe mais n'a aucun droit dans la table des droits

122. // $iErreur = 3 si l'utilisateur existe et est administrateur

123. // $iErreur = 0 si l'utilisateur existe et n'est pas administrateur

124. // $sAdmin="y" ssi l'utilisateur existe et est administrateur ($iErreur==3), sinon il est égal à la chaîne vide

125. // $dDroits est le dictionnaire des droits de l'utilisateur s'il n'est pas administrateur ($iErreur==0)

126. // sinon c'est un tableau vide

127. // les clés du dictionnaire sont les tables sur lesquelles l'utilisateur a des droits

128. // la valeur associée à cette table est à son tour un dictionnaire où les clés sont les droits

129. // (consulter, ajouter, modifier, supprimer) et les valeurs les chaînes 'y' (yes) ou 'n' (no) selon les cas

130. ...

131. }//existeUser

133. // --------------------------------------

134. function getCodes(){

135. // rend le tableau des codes

136. ....

137. }//getCodes

139. }//classe

140. ?>

Commentaires

- la classe articles utilise la bibliothèque PEAR::DB pour ses accès à la base de données d'où la commande

1. require_once 'DB.php';

Cette inclusion suppose que le script DB.php soit dans l'un des répertoires de l'option include_path du fichier de configuration de PHP.

- le constructeur a besoin de savoir à quelle base on se connecte et sous quelle identité. Ces informations lui sont données dans le dictionnaire $dDSN. Rappelons que l'hypothèse de départ était que la base s'appelait dbarticles et qu'elle appartenait à un utilisateur appelé admarticles ayant le mot de passe mdparticles. Rappelons aussi que cette application autorise plusieurs utilisateurs ayant différents droits. Il y a là une ambiguïté à lever. La connexion est bien ouverte sous l'identité de admarticles et finalement c'est sous cette identité que seront faites toutes les opérations sur la base dbarticles puisque c'est le seul nom que connaît le SGBD MySQL qui ait les droits suffisants pour gérer la base dbarticles. Pour "simuler" l'existence de différents utilisateurs, on fera travailler l'utilisateur admarticles avec les droits de l'utilisateur dont le login ($sUser) et le mot de passe ($sMdp) sont passés en paramètres au constructeur. Ainsi, avant de faire une opération sur la base des articles, on vérifiera que l'utilisateur ($sUser, $sMdp) a bien les droits de la faire. Si oui, c'est l'utilisateur admarticles qui la fera pour lui.

- le login et le mot de passe de l'administrateur de la base des articles doivent être passés au constructeur. C'est une saine précaution. Si on inscrivait en "dur" dans le code de la classe ces deux informations, tout utilisateur de la classe pourrait se faire passer aisément pour administrateur de la base des articles. En effet, une classe PHP n'est pas protégée. Aussi l'attribut $bAdmin de la classe qui indique si l'utilisateur ($sUser, $sMdp) pour lequel on travaille est administrateur ou non pourrait très bien être positionné directement de l'extérieur comme dans l'exemple suivant :

1. $oArticles=new articles($dDSN,$sUser,$sMdp)

2. // ici $sUser a été reconnu comme un utilisateur non administrateur de la base

3. $oArticle->bAdmin=TRUE;

4. // maintenant $sUser est devenu administrateur

PHP n'est pas JAVA ou C# et une classe PHP n'est qu'une structure de données un peu plus évoluée qu'un dictionnaire mais qui n'offre pas la sécurité d'une vraie classe où l'attribut bAdmin aurait été déclaré privé ou protégé rendant impossible sa modification de l'extérieur. Parce que l'utilisateur de la classe doit connaître le login et le mot de passe de l'administrateur de la base des articles, seul ce dernier peut utiliser la classe. L'opération précédente ne présente donc plus aucun intérêt pour lui. La classe est là uniquement pour lui offrir des facilités de développement. Une conséquence importante est qu'il n'y a pas lieu de prendre des précautions de sécurité. Encore une fois, celui qui utilise la classe articles est nécessairement administrateur de la base des articles.

- la classe gère les erreurs de connexion à la base ou tout autre erreur de façon unique en remplissant l'attribut $aErreurs avec le ou des messages d'erreurs. Après chaque opération, l'utilisateur de la classe doit donc vérifier cette liste.

- Les méthodes addArticle, updateArticle, deleteArticle, selectArticles, execute découlent directement de la maquette de l'interface web présentée précédemment. Elles correspondent en effet aux options du menu proposé. Les méthodes addArticle et modifyArticle s'appuient sur la méthode vérifierArticle pour vérifier que l'article qui va être ajouté ou être modifié a des données correctes. Toujours dans le même esprit, la méthode existeArticle permet de vérifier qu'on ne s'apprête pas à ajouter un article qui existe déjà. On pourrait se passer de cette méthode si on utilise une table d'articles où le code est clé primaire. C'est alors le SGBD lui-même qui signalera l'échec de l'ajout pour cause de doublon. Il le dira probablement avec un message d'erreur peu lisible et en anglais.

- Un article à modifier ou à supprimer sera désigné par son code qui est unique. La méthode getCodes permet d'obtenir tous ces codes.

- La méthode disconnect ferme la connexion à la base, connexion ouverte lors de la construction de l'objet. On ne voit pas l'intérêt ici de la méthode connect qui va recréer une connexion avec la base. Cela va permettre d'ouvrir et fermer cette connexion à volonté avec un même objet. L'intérêt n'apparaît qu'en conjonction avec l'application web. Celle-ci va créer un objet articles qu'elle va mémoriser dans une session. Si celle-ci va être capable de conserver la plupart des attributs de l'objet au fil des échanges client-serveur successifs, elle n'est pas capable cependant de garder l'attribut représentant la connexion ouverte. Celle-ci devra donc être réouverte à chaque nouvel échange client-serveur. On demandera une connexion persistante afin que la connexion ouverte soit stockée dans un pool de connexions et reste ouverte de façon permanente. Ainsi lorsque le script demandera une nouvelle connexion, celle-ci sera récupérée dans le pool de connexions. On arrive donc au même résultat que si la session avait pu mémoriser la connexion ouverte.

- la méthode existeUser permet au constructeur de savoir si l'utilisateur $sUser identifié par le mot de passe $sMdp existe bien. Si oui, la méthode permet de savoir s'il est administrateur ou non (indiqué dans la table USERS) et mémorise cette information dans l'attribut $bAdmin. S'il n'est pas administrateur, la méthode va récupérer ses droits dans la table DROITS et les met dans l'attribut $dDroits qui est un dictionnaire à double indexation : $dDroits[$table][$droit] vaut 'y' si l'utilisateur $sUser a le droit $droit sur la table $table et vaut 'n' sinon.

Écrire la classe articles. Les accès à la base de données seront faits à l'aide de la bibliothèque PEAR::DB qui permet de s'affranchir du type exact de la base. On trouvera en annexe les éléments de base de cette bibliothèque.

VII-D-5 - La structure de l'application WEB

Maintenant que nous avons la classe "métier" de gestion de la base d'articles, nous pouvons utiliser celle-ci dans différents environnements. Il est proposé ici de l'utiliser dans une application web. Découvrons celle-ci au-travers de ces différentes pages :

VII-D-5-a - La page type de l'application

Revenons sur la page d'accueil déjà présentée :

Toutes les pages de l'application auront la structure ci-dessus, celle d'un tableau à deux lignes et trois colonnes comprenant quatre zones :

1. la zone 1 forme la première ligne du tableau. Elle est réservée au titre accompagné éventuellement d'une image. Les trois colonnes de la ligne sont ici fusionnées.

2. la seconde ligne a trois zones, une zone par colonne :

1. la zone 2 contient les options du menu. Elle contient à son tour un tableau à une colonne et plusieurs lignes. Les options du menu sont placées dans les lignes du tableau.

2. la zone 3 est vide et ne sert qu'à séparer les zones 2 et 4. On aurait pu procéder différemment pour réaliser cette séparation.

3. la zone 4 est celle qui contient la partie dynamique de la page. C'est cette partie qui change d'une action à l'autre, les autres restant identiques.

Le script PHP générant cette page type s'appellera main.php et pourrait être le suivant :

1. <html>

2. <head>

3. <title>Gestion d'articles</title>

4. <link type="text/css" href="<?php echo $dConfig['urlPageStyle'] ?>" rel="stylesheet" />

5. </head>

6. <body background="<?php echo $dConfig['urlBackGround'] ?>">

7. <table>

8. <tr height="60">

9. <td colspan="3" align="left" valign="top" >

10. <h1><?php echo $main["title"] ?></h1>

11. </td>

12. </tr>

13. <tr>

14. <td>

15. <table>

16. <tr>

17. <td class="menutitle" >

18. <a href="<?php echo  $main["liens"]["login"] ?>" ?>Authentification</a>

19. </td>

20. </tr>

21. <tr>

22. <td><br /></td>

23. </tr>

24. <tr>

25. <td class="menutitle" >

26. Utilisation

27. </td>

28. </tr>

29. <tr height="10"></tr>

30. <tr>

31. <td class="menublock" >

32. <img alt="-" src="../images/radio.gif" />

33. <a href="<?php echo  $main["liens"]["addArticle"] ?>" ?>

34. Ajouter un article

35. </a>

36. </td>

37. </tr>

38. <tr>

39. <td class="menublock" >

40. <img alt="-" src="../images/radio.gif" />

41. <a href="<?php echo $main["liens"]["updateArticle"] ?>">

42. Modifier un article

43. </a>

44. </td>

45. </tr>

46. <td class="menublock" >

47. <img alt="-" src="../images/radio.gif" />

48. <a href="<?php echo $main["liens"]["deleteArticle"] ?>">

49. Supprimer un article

50. </a>

51. </td>

52. </tr>

53. <tr>

54. <td class="menublock" >

55. <img alt="-" src="../images/radio.gif" />

56. <a href="<?php echo $main["liens"]["selectArticle"] ?>">

57. Lister des articles

58. </a>

59. </td>

60. </tr>

61. <tr>

62. <td><br /></td>

63. </tr>

64. <tr>

65. <td class="menutitle" >

66. Administration

67. </td>

68. </tr>

69. <tr height="10"></tr>

70. <tr>

71. <td class="menublock" >

72. <img alt="-" src="../images/radio.gif" />

73. <a href="<?php echo $main["liens"]["sql"] ?>" >

74. Requête SQL

75. </a>

76. </td>

77. </tr>

78. </table>

79. </td>

80. <td>

81. <img alt="/" src="../images/pix.gif" width="10" height="1" />

82. </td>

83. <td>

84. <fieldset>

85. <legend><?php echo $main["légende"] ?></legend>

86. <?php

87. include $main["contenu"];

88. ?>

89. </fieldset>

90. </td>

91. </tr>

92. </table>

93. </body>

94. </html>

Les zones paramétrées de la page ont été mises en relief dans le listing ci-dessus. La page type est paramétrée de plusieurs façons :

- par un dictionnaire $main ayant les clés suivantes :

- title : titre à mettre en zone 1 de la page

- liens : dictionnaires des liens à générer dans la colonne du menu. Ces liens sont associés aux options du menu de la zone 2

- contenu : url de la page à afficher dans la zone 4

- par un dictionnaire $dConfig rassemblant des informations tirées d'un fichier de configuration de l'application appelé config.php

- par des classes faisant partie de la feuille de style utilisée par la page :

1. <link type="text/css" href="<?php echo $dConfig['urlPageStyle'] ?>" rel="stylesheet" />

La page utilise ici les classes de style suivantes :

1. menutitle : pour une option principale du menu

2. menublock : pour une option secondaire du menu

Changer l'un des paramètres change l'aspect de la page. Ainsi changer $main['title'] changera le titre de la zone 1.

VII-D-5-b - Le traitement type d'une demande d'un client

Le client interagit avec l'application grâce aux liens de la zone 2 de la page type. Ces liens seront du type suivant :

1. apparticles.php?action=xx&phase=y&PHPSESSID=zzzzzzzzzzzz

action

désigne l'action en cours parmi les suivantes :

phase

une action peut se faire en plusieurs étapes - désigne l'étape en cours

PHPSESSID

jeton de session lorsque celle-ci a démarré - permet au serveur de récupérer des informations stockées dans la session lors des précédents échanges

De même l'attribut action dans les formulaires aura la même forme. Par exemple, dans la page d'accueil il y a un formulaire de login dans la zone 4. La balise HTML de ce formulaire est définie comme suit :

1. <form name="frmLogin" method="post" action="apparticles.php?action=authentifier&phase=1">

Le traitement de la demande du client est accomplie par le script principal de l'application appelé apparticles.php. Son travail est de construire la réponse au client. Il procèdera toujours de la même façon :

- grâce au nom de l'action et à la phase en cours, il déléguera la demande à une fonction spécialisée. Celle-ci traitera la demande et génèrera la page réponse adéquate. Pour chaque demande du client, il peut y avoir plusieurs pages réponse possibles : page1, page2…, pagen. Ces pages contiennent des informations qui doivent être calculées par la fonction. Ce sont donc des pages paramétrées. Elles seront générées par des scripts page1.php, page2.php…, pagen.php.

- par souci d'homogénéité, les parties variables des pages à afficher dans la zone 4 de la page type seront elles-aussi placées dans le dictionnaire $main.

Supposons qu'en réponse à une demande, le serveur doive envoyer la page pagex.php au client. Il procèdera de la façon suivante :

- il placera dans le dictionnaire $main les valeurs nécessaires à la page pagex.php

- il mettra dans $main['contenu'] qui désigne l'URL de la page à afficher en zone 4 de la page type, l'URL de pagex.php

- il demandera l'affichage de le page type avec l'instruction

1. include "main.php";

La page type sera alors affichée avec dans la zone 4 le code du script pagex.php qui sera évalué pour générer le contenu de la zone 4. On se rappellera que celle-ci est une simple cellule d'un tableau. Il ne faut donc pas que le code HTML généré par pagex.php commence par les balises <HTML>, <HEAD>, <BODY>…. Celles-ci ont déja été émises au début de la page type. Voici par exemple à quoi pourrait ressembler le script login.php qui génère la zone 4 de la page d'accueil :

1. <form name="frmLogin" method="post" action="<?php echo $main["post"] ?>">

2. <table>

3. <tr>

4. <td>login</td>

5. <td><input type="text" value="<?php echo $main["login"] ?>" name="txtLogin" class="text"></td>

6. </tr>

7. <tr>

8. <td>mot de passe</td>

9. <td><input type="password" value="" name="txtMdp" class="text"></td>

10. <td><input type="submit" value="Connexion" class="submit"></td>

11. </tr>

12. </table>

13. </form>

On voit que la page :

- est réduite à un formulaire

- est paramétrée à la fois par le dictionnaire $main et la feuille de style.

VII-D-5-c - Le fichier de configuration

On a toujours intérêt à paramétrer le plus possible les applications afin d'éviter d'aller dans le code simplement parce qu'on a décidé par exemple de changer le chemin d'un script ou d'une image. L'application principale apparticles.php chargera donc un fichier de configuration config.php à son démarrage :

1. // chargement du fichier de configuration

2. include "config.php";

On mettra dans ce fichier, des directives de configuration destinées à PHP et des initialisations de variables globales :

1. <?php

3. // configuration de php

4. ini_set("register_globals","off");

5. ini_set("display_errors","off");

6. ini_set("expose_php","off");

7. ini_set("session.use_cookies","0");    // pas de cookies

9. // configuration base articles

10. $dConfig["DSN"]=array(

11. "sgbd"=>"mysql",

12. "admin"=>"admarticles",

13. "mdpadmin"=>"mdparticles",

14. "host"=>"localhost",

15. "database"=>"dbarticles"

16. );

18. // url des pages

19. $dConfig['urlBackGround']="../images/standard.jpg";

20. $dConfig["urlPageStyle"]="mystyle.css";

21. $dConfig["urlAppArticles"]="apparticles.php";

22. $dConfig["urlPageMain"]="main.php";

23. $dConfig["urlPageLogin"]="login.php";

24. $dConfig["urlPageErreurs"]="erreurs.php";

25. $dConfig["urlPageInfos"]="infos.php";

26. $dConfig["urlPageAddArticle"]="addarticle.php";

27. $dConfig["urlPageUpdateArticle1"]="updatearticle1.php";

28. $dConfig["urlPageUpdateArticle2"]="updatearticle2.php";

29. $dConfig["urlPageDeleteArticle1"]="deletearticle1.php";

30. $dConfig["urlPageDeleteArticle2"]="deletearticle2.php";

31. $dConfig["urlPageSelectArticle1"]="selectarticle1.php";

32. $dConfig["urlPageSelectArticle2"]="selectarticle2.php";

33. $dConfig["urlPageSQL1"]="sql1.php";

34. $dConfig["urlPageSQL2"]="sql2.php";

35. $dConfig["urlPageSQL3"]="sql3.php";

37. // liens de la page principale

38. $main["liens"]["login"]="$sUrlAppArticles?action=authentifier&phase=0";

39. $main["liens"]["addArticle"]="$sUrlAppArticles?action=addArticle&phase=0";

40. $main["liens"]["updateArticle"]="$sUrlAppArticles?action=updateArticle&phase=0";

41. $main["liens"]["deleteArticle"]="$sUrlAppArticles?action=deleteArticle&phase=0";

42. $main["liens"]["selectArticle"]="$sUrlAppArticles?action=selectArticle&phase=0";

43. $main["liens"]["sql"]="$sUrlAppArticles?action=sql&phase=0";

45. // on mémorise $main dans la configuration

46. $dConfig["main"]=$main;

47. ?>

VII-D-5-d - La feuille de style associée à la page type

Nous avons vu que la réponse du serveur avait un format unique celui de main.php. On aura pu remarquer que ce script produit une page brute dépourvue d'effets de présentation. C'est une bonne chose pour plusieurs raisons :

- le développeur n'a pas à se soucier de la présentation graphique de la page qu'il crée. Il n'a en effet pas nécessairement les compétences pour créer des pages graphiques attractives. Il peut ici se concentrer entièrement sur le code.

- la maintenance des scripts est facilitée. Si ceux-ci comportaient des attributs de présentation, ni la structure du code, ni celle de la présentation n'apparaîtraient clairement. L'aspect graphique des feuilles est souvent délégué à un graphiste. Celui-ci n'aimerait probablement pas à devoir chercher dans un script qu'il ne comprend pas, où sont les attributs de présentation qu'il doit modifier.

Il faut pourtant bien se soucier de l'aspect graphique des pages. En effet, c'est cela qui attire les internautes vers un site. Ici, la présentation est déléguée à une feuille de style. La page main.php indique dans son code la feuille de style à utiliser pour l'afficher :

1. <head>

2. <title>Gestion d'articles</title>

3. <link type="text/css" href="<?php echo $dConfig['urlPageStyle'] ?>" rel="stylesheet" />

4. </head>

La feuille de style utilisée dans ce document est la suivante :

1. BODY {

2. background : url(../images/standard.jpg);

3. border : 2px none #FFDAB9;

4. font-family : Garamond;

5. font-size : 16px;

6. margin-left : 0px;

7. padding-left : 20px;

8. }

10. INPUT {

11. background : #EEE8AA;

12. border : 1px solid #EE82EE;

13. font-family : Garamond;

14. font-size : 18px;

15. }

17. INPUT.submit{

18. font-family : "Times New Roman";

19. font-size : 16px;

20. background : #FA8072;

21. border : 2px double Green;

22. font-weight : bold;

23. text-align : center;

24. vertical-align : middle;

25. cursor : pointer;

26. }

28. TD.menutitle{

29. background-image : url(../images/menugelgd.gif);

30. height : 23px;

31. text-align : center;

32. vertical-align : middle;

33. background : url(../images/menugelgd.gif) no-repeat center;

34. }

36. TD.menublock{

37. background : url(../images/bandegrismenugd.gif) repeat-x;

38. text-align : left;

39. vertical-align : middle;

40. }

42. A {

43. font-family : "Comic Sans MS";

44. color : #FF7F50;

45. font-size : 15px;

46. text-decoration : none;

47. }

49. A:HOVER {

50. background : #FFA07A;

51. color : Red;

52. }

54. FIELDSET {

55. border : 1px solid #A0522D;

56. background : #FFE4C4;

57. margin : 10px 10px 10px 10px;

58. padding-left : 10px;

59. padding-right : 10px;

60. padding-bottom : 10px;

61. }

63. LEGEND{

64. background : #FFA500;

65. }

67. TH {

68. background : #228B22;

69. text-align : center;

70. vertical-align : middle;

71. }

73. TD.libellé{

74. border : 1px solid #008B8B;

75. color : #339966;

76. }

78. H1 {

79. font : bold 20px/30px Garamond;

80. color : #FF7F50;

81. background : #D1E1F8;

82. background-attachment : fixed;

83. text-align : center;

84. vertical-align : middle;

85. font-family : Garamond;

86. }

88. SELECT.TEXT {

89. background : #6495ED;

90. text-align : center;

91. color : Aqua;

92. }

Nous n'entrerons pas dans les détails de cette feuille de style. Nous l'accepterons telle-quelle. Un peu plus loin nous verrons comment la construire et la modifier. Il existe des logiciels pour cela. Néanmoins indiquons le rôle des attributs de présentation utilisés dans la feuille :

Attribut :

régit la présentation de la balise HTML :

BODY

<BODY>

H1

<H1> (Header1)

A

<A> (Anchor)

A:HOVER

fixe les attributs de présentation de l'ancre lorsque l'utilisateur passe la souris dessus

FIELDSET

<FIELDSET> - cette balise n'est pas reconnue par tous les navigateurs

LEGEND

<LEGEND> - cette balise n'est pas reconnue par tous les navigateurs

INPUT

<INPUT>

INPUT.TEXT

<INPUT class="TEXT">

INPUT.SUBMIT

<INPUT class="SUBMIT">

TH

<TH> (Table Header)

TD.menutitle

<TD class="menutitle"> (Table Data)

TD.menublock

<TD class="menublock">

TD.libellé

<TD class="libellé">

Voyons sur un exemple comment peuvent être écrites ces règles de présentation. Dans cet exemple, nous utiliserons le logiciel TopStyle Lite disponible gratuitement à l'URL http://www.bradsoft.com. Une fois chargée la feuille de style, on a une fenêtre à trois zones :

- une zone d'édition de texte. Les attributs de présentation peuvent être définis à la main à condition de connaître les règles d'écriture des feuilles de style qui suivent une norme appelée CSS (Cascading Style Sheets).

- la zone 2 présente les propriétés éditables de l'attribut en cours de construction. C'est la méthode la plus simple. Elle évite d'avoir à connaître le nom exact des attributs de présentation qui sont très nombreux

- la zone 3 donne l'aspect visuel de l'attribut en cours de construction

Dans la zone 1 ci-dessus, opérons un copier-coller de l'attribut INPUT.submit vers un attribut INPUT.fantaisie. Cet attribut fixera la présentation de la balise HTML <INPUT class="fantaisie">

Utilisons la zone 2 pour modifier certaines des propriétés de l'attribut INPUT.fantaisie :

Désormais, toute balise <INPUT … class="fantaisie"> trouvée dans une page HTML associée à la feuille de style précédente sera présentée comme l'exemple de la zone 3 ci-dessus.

L'intérêt des feuilles de style est grand. Leur utilisation permet de changer le "look" d'une application web en ne modifiant celle-ci qu'en un seul point : sa feuille de style. Les feuilles de style ne sont pas reconnues par les navigateurs anciens. La directive <link ..> ci-dessous sera ignorée par certains d'entre-eux :

1. <head>

2. <title>Gestion d'articles</title>

3. <link type="text/css" href="<?php echo $dConfig['urlPageStyle'] ?>" rel="stylesheet" />

4. </head>

Dans notre application, cela donnera la page d'accueil suivante :

On a là une page minimale sans recherche graphique. On peut avoir pire. Certaines versions de navigateurs reconnaissent les feuilles de style mais les interprètent mal. On peut alors avoir une page défigurée et inutilisable. Se pose donc la question du type du navigateur client. Il existe des techniques qui aident à déterminer le type du navigateur client. Elles ne sont pas totalement fiables. On peut alors écrire différentes feuilles de style pour différents navigateurs voire écrire une version sans feuille de style pour les navigateurs qui les ignorent. Cela alourdit bien sûr la tâche de développement. Ce problème important a été ici ignoré.

Avec les feuilles de style, on peut imaginer offrir un environnement personnalisé aux utilisateurs de notre application. Nous pourrions leur présenter une page leur offrant plusieurs styles de présentation possibles. Ils pourraient choisir celui qui leur convient le mieux. Ce choix pourrait être enregistré dans une base de données. Lorsque l'utilisateur se connecte de nouveau, on pourrait alors démarrer l'application avec la feuille de style qui a reçu sa préférence.

VII-D-5-e - Le module d'entrée de l'application

Les clients ne connaîtront de l'application que son module d'entrée : apparticles.php. Les grandes lignes de son fonctionnement sont les suivantes :

- La demande du client est récupérée et analysée. Celle-ci est paramétrée ou non. Lorsqu'elle est paramétrée, les paramètres attendus sont les suivants : action=[action]&phase=[phase]&PHPSESSID=[PHPSESSID]

- Si la demande n'est pas paramétrée ou si les paramètres récupérés ne sont pas ceux attendus, le serveur envoie comme réponse la page d'authentification (login, mot de passe). Dès que l'utilisateur se sera identifié correctement, une session est créée. Elle servira à stocker des informations tout au long des échanges client-serveur.

- Si une demande est correctement reconnue, elle est traitée par un module qui dépend et de l'action et de la phase en cours.

- Tous les accès à la base de données se font par l'intermédiaire de la classe métier articles.php.

- Le traitement d'une demande se termine toujours par l'envoi au client de la page main.php dans laquelle on a précisé dans $main['contenu'] l'URL de la page à mettre dans la zone 4 de la page type.

Le squelette du script apparticles.php pourrait être le suivant :

1. <?php

2. // gestion d'une table d'articles

3. include "config.php";

4. include "articles.php";

6. // action à entreprendre

7. $sAction=$_POST["action"] ? $_POST["action"] : $_GET["action"] ? $_GET["action"] : "authentifier";

8. $sAction=strtolower($sAction);

9. // phase éventuelle

10. $sPhase=$_POST["phase"] ? $_POST["phase"] : $_GET["phase"] ? $_GET["phase"] : "0";

12. // session

13. session_start();

14. $dSession=$_SESSION["session"];

16. // y-a-t-il une session en cours ?

17. if(! isset($dSession)){

18. // authentification de l'utilisateur

19. if($sAction=="authentifier" && $sPhase=="0") authentifier_0($dConfig);

20. if($sAction=="authentifier" && $sPhase=="1") authentifier_1($dConfig);

21. if($sAction=="authentifier" && $sPhase=="2") authentifier_2($dConfig);

22. // demande incorrecte

23. authentifier_0($dConfig);

24. }//if - pas de session

26. // on récupère la session

27. $dSession=unserialize($dSession);

29. // traitement de la demande

30. // ----- authentification

31. if($sAction=="authentifier" && $sPhase=="0") authentifier_0($dConfig);

32. if($sAction=="authentifier" && $sPhase=="1") authentifier_1($dConfig);

33. if($sAction=="authentifier" && $sPhase=="2") authentifier_2($dConfig);

34. // ----- ajout d'article

35. if($sAction=="addarticle" && $sPhase=="0") addArticle_0($dConfig,$dSession);

36. if($sAction=="addarticle" && $sPhase=="1") addArticle_1($dConfig,$dSession);

37. if($sAction=="addarticle" && $sPhase=="2") addArticle_2($dConfig,$dSession);

38. // ----- mise à jour d'article

39. if($sAction=="updatearticle" && $sPhase=="0") updateArticle_0($dConfig,$dSession);

40. if($sAction=="updatearticle" && $sPhase=="1") updateArticle_1($dConfig,$dSession);

41. if($sAction=="updatearticle" && $sPhase=="2") updateArticle_2($dConfig,$dSession);

42. if($sAction=="updatearticle" && $sPhase=="3") updateArticle_3($dConfig,$dSession);

43. // ----- suppression d'article

44. if($sAction=="deletearticle" && $sPhase=="0") deleteArticle_0($dConfig,$dSession);

45. if($sAction=="deletearticle" && $sPhase=="1") deleteArticle_1($dConfig,$dSession);

46. if($sAction=="deletearticle" && $sPhase=="2") deleteArticle_2($dConfig,$dSession);

47. // ----- consultation d'articles

48. if($sAction=="selectarticle" && $sPhase=="0") selectArticle_0($dConfig,$dSession);

49. if($sAction=="selectarticle" && $sPhase=="1") selectArticle_1($dConfig,$dSession);

50. if($sAction=="selectarticle" && $sPhase=="2") selectArticle_2($dConfig,$dSession);

51. // ----- émission d'une requête SQL

52. if($sAction=="sql" && $sPhase=="0") sql_0($dConfig,$dSession);

53. if($sAction=="sql" && $sPhase=="1") sql_1($dConfig,$dSession);

54. if($sAction=="sql" && $sPhase=="2") sql_2($dConfig,$dSession);

57. // action erronée - on présente la page d'authentification

58. session_destroy();

59. authentifier_0($dConfig,"0");

60. ...

61. ?>

On notera les points suivants :

- les fonctions traitant une demande particulière du client se terminent par la génération de la page réponse et par une instruction exit qui termine l'exécution du script apparticles.php. Autrement dit, on ne "revient" pas de ces fonctions.

- les fonctions admettent un ou deux paramètres :

- $dConfig est un dictionnaire contenant des informations issues du fichier de configuration config.php. Toutes les fonctions l'utilisent.

- $dSession est un dictionnaire contenant des informations de session. Il n'existe que lorsque la session a été créée c'est-à-dire après que l'authentification de l'utilisateur a réussi. C'est pourquoi les fonctions authentifier n'ont pas ce paramètre.

VII-D-5-f - La page d'erreurs

Toute application logicielle doit savoir gérer correctement les erreurs qui peuvent survenir. Une application web n'échappe pas à cette règle. Ici, lors d'une erreur, nous placerons la page erreurs.php suivante dans la zone 4 de la page type :

erreurs.php

1. Les erreurs suivantes se sont produites :

2. <ul>

3. <?php

4. for($i=0;$i<count($main["erreurs"]);$i++){

5. echo "<li>".$main["erreurs"][$i]."</li>\n";

6. }//for

7. ?>

8. </ul>

9. <a href="<?php echo $main["href"] ?>"><?php echo $main["lien"] ?></a>

Elle présente la liste d'erreurs définie dans $main['erreurs']. Par ailleurs, elle peut proposer un lien de retour, en général vers la page qui a précédé la page d'erreurs. Ce lien sera défini par un libellé $main['lien'] et une URL $main['href']. Pour ne pas avoir ce lien, il suffira de mettre la chaîne vide dans $main['lien']. Voici un exemple de page d'erreurs dans le cas où l'utilisateur s'identifie incorrectement :

VII-D-5-g - La page d'informations

Parfois on voudra donner en réponse à l'utilisateur une simple information, par exemple que son identification a réussi. On utilisera pour cela la page infos.php suivante :

1. <?php echo $main["infos"] ?>

Pour afficher une information en retour à une demande d'un client, on

1. mettra l'information dans $main['infos']

2. mettra l'URL de infos.php dans $main['contenu']

Voici par exemple, l'information retournée lorsque l'utilisateur s'est identifié correctement :

VII-D-6 - Le fonctionnement de l'application

Nous avons maintenant une bonne idée de la structure générale de l'application à écrire. Il nous reste à présenter les cheminement de l'utilisateur dans l'application, les actions qu'il peut faire et les réponses qu'il reçoit du serveur. Ceci fait, nous pourrons écrire les fonctions qui traitent les différentes demandes d'un client. Dans ce qui suit, nous allons présenter le fonctionnement de l'application au travers des pages présentées à l'utilisateur en réponse à certaines de ces actions. Nous préciserons à chaque fois les points suivants :

action utilisateur

action initiale de l'utilisateur qui a amené à la réponse affichée

paramètres envoyés

les paramètres envoyés par le navigateur client au serveur en réponse à l'action manuelle de l'utilisateur

page réponse

le script qui génère la zone 4 de la page type

VII-D-6-a - L'authentification

Avant de pouvoir utiliser l'application, l'utilisateur devra s'identifier à l'aide de la page suivante :

action utilisateur

1 - demande initiale de l'URL apparticles.php
2 - utilisation de l'option Authentification du menu
3 - demande directe de l'URL articles.php avec des paramètres erronés

paramètres envoyés

1 - pas de paramètres
2 - action=authentifier?phase=0
3 - une liste de paramètres erronés

page réponse

login.php

Sur la page d'accueil, le lien [Ajouter un article] est de la forme suivante : action=addarticle?phase=0. Les autres liens sont de la même forme avec action=(authentifier, updatearticle, deletearticle, selectarticle, sql). L'utilisateur remplit le formulaire et utilise le bouton [Connexion] :

La réponse est la suivante :

action utilisateur

bouton [Connexion]

paramètres envoyés

action=authentifier?phase=1

page réponse

infos.php

Le titre de la page a été modifié pour indiquer le login de l'utilisateur et ses droits administrateur/utilisateur. Par ailleurs, tous les liens de la zone 2 ont été modifiés pour refléter le fait qu'une session a démarré. Le paramètre PHPSESSID=[PHPSESSID] leur a été ajouté.

Si le serveur n'a pas pu identifier le client, celui-ci aura une réponse différente :

action utilisateur

bouton [Connexion]

paramètres envoyés

action=authentifier?phase=1

page réponse

erreurs.php

Le lien [Retour à la page de login] est un lien sur l'URL apparticles.php?action=authentifier&phase=2&txtLogin=x. Ce lien ramène le client à la page de login où le champ de login est rempli par la valeur du paramètre txtLogin :

action utilisateur

lien [Retour à la page de login]

paramètres envoyés

action=authentifier?phase=2&txtLogin=x

page réponse

login.php

VII-D-6-b - Ajout d'un article

Le lien du menu [Ajouter un article] amène la page suivante dans la zone 4 de la page type :

action utilisateur

lien [Ajouter un article]

paramètres envoyés

action=addArticle?phase=0&PHPSESSID=[PHPSESSID]

page réponse

addarticle.php

L'utilisateur remplit les champs et envoie le tout au serveur avec le bouton [Ajouter] qui est de type submit. Aucune vérification n'est faite côté client. C'est le serveur qui les fait. Il peut envoyer en réponse une page d'erreurs comme sur l'exemple ci-dessous :

Demande

Réponse

action utilisateur

bouton [Ajouter]

paramètres envoyés

action=addArticle?phase=1&PHPSESSID=[PHPSESSID]

page réponse

erreurs.php

Le lien [Retour à la page d'ajout d'article] permet de revenir à la page de saisie :

Demande

Réponse

action utilisateur

lien [Retour à la page d'ajout d'article]

paramètres envoyés

action=addArticle?phase=2&PHPSESSID=[PHPSESSID]

page réponse

article.php

Si l'ajout se fait sans erreurs, l'utilisateur reçoit un message de confirmation :

Demande

Réponse

action utilisateur

bouton [Ajouter]

paramètres envoyés

action=addArticle?phase=1&PHPSESSID=[PHPSESSID]

page réponse

infos.php

VII-D-6-c - Consultation d'articles

Le lien du menu [Lister des articles] amène la page suivante dans la zone 4 de la page type :

action utilisateur

lien du menu [Lister des articles]

paramètres envoyés

action=selectArticle?phase=0&PHPSESSID=[PHPSESSID]

page réponse

select1.php

Une requête select [colonnes] from articles where [where] orderby [orderby] sera émise sur la table des articles où [colonnes], [where] et [orderby] sont le contenu des champs ci-dessus. Par exemple :

Demande

Réponse

action utilisateur

bouton [Afficher]

paramètres envoyés

action=selectArticle?phase=1&PHPSESSID=[PHPSESSID]

page réponse

select2.php

La demande peut être erronée auquel cas le client reçoit une page d'erreurs :

Demande

Réponse

Dans les deux cas (erreurs ou pas), le lien [Retour à la page de sélection d'articles] permet de revenir à la page select1.php :

Demande

Réponse

action utilisateur

lien [Retour à la page de sélection d'articles]

paramètres envoyés

action=selectArticle?phase=2&PHPSESSID=[PHPSESSID]

page réponse

select1.php

VII-D-6-d - Modification d'articles

Le lien du menu [Modifier un article] amène la page suivante dans la zone 4 de la page type :

action utilisateur

lien de menu [Modifier un article]

paramètres envoyés

action=updateArticle?phase=0&PHPSESSID=[PHPSESSID]

page réponse

updatearticle1.php

On choisit le code de l'article à modifier dans la liste déroulante et on fait [OK] pour modifier l'article ayant ce code :

Demande

Réponse

action utilisateur

bouton [OK]

paramètres envoyés

action=updateArticle?phase=1&PHPSESSID=[PHPSESSID]

page réponse

updatearticle2.php

Une fois obtenue la fiche de l'article à modifier, l'utilisateur peut faire ses modifications :

Demande

Réponse

action utilisateur

bouton [Modifier]

paramètres envoyés

action=updateArticle?phase=2&PHPSESSID=[PHPSESSID]

page réponse

infos.php

L'utilisateur peut faire des erreurs lors de la modification :

Demande

Réponse

Le lien [Retour à la page de modification d'article] permet de revenir à la page de saisie :

action utilisateur

lien [Retour à la page de modification d'article]

paramètres envoyés

action=updateArticle?phase=3&PHPSESSID=[PHPSESSID]

page réponse

updatearticle2.php

VII-D-6-e - Suppression d'un article

Le lien du menu [Supprimer un article] amène la page suivante dans la zone 4 de la page type :

action utilisateur

lien du menu [Supprimer un article]

paramètres envoyés

action=deleteArticle?phase=0&PHPSESSID=[PHPSESSID]

page réponse

deletearticle1.php

L'utilisateur choisit le code de l'article à supprimer dans une liste déroulante :

Demande

Réponse

action utilisateur

bouton [OK]

paramètres envoyés

action=deleteArticle?phase=1&PHPSESSID=[PHPSESSID]

page réponse

deletearticle2.php

L'utilisateur confirme la suppression de l'article avec le bouton [Supprimer] :

Demande

Réponse

action utilisateur

bouton [Supprimer]

paramètres envoyés

action=deleteArticle?phase=2&PHPSESSID=[PHPSESSID]

page réponse

infos.php

VII-D-6-f - Émissions de requêtes administrateur

Le lien du menu [Requête SQL] amène la page suivante dans la zone 4 de la page type :

action utilisateur

lien de menu [Requête SQL]

paramètres envoyés

action=sql?phase=0&PHPSESSID=[PHPSESSID]

page réponse

sql1.php

On tape le texte de la requête SQL dans le champ de saisie et on utilise le bouton [Exécuter] pour l'exécuter. Seul un administrateur peut émettre ces requêtes comme le montre l'exemple suivant :

Demande

Réponse

action utilisateur

bouton [Exécuter]

paramètres envoyés

action=sql?phase=1&PHPSESSID=[PHPSESSID]

page réponse

erreurs.php

Le lien [Retour à la page d'émission de requêtes SQL] permet de revenir à la page de saisie :

action utilisateur

lien [Retour à la page d'émission de requêtes SQL]

paramètres envoyés

action=sql?phase=2&PHPSESSID=[PHPSESSID]

page réponse

sql1.php

Si on est administrateur et que la requête est syntaxiquement correcte :

Demande

on obtient le résultat de la requête :

Réponse

action utilisateur

bouton [Exécuter]

paramètres envoyés

action=sql?phase=1&PHPSESSID=[PHPSESSID]

page réponse

sql2.php

On peut émettre des requêtes de mise à jour des tables :

Demande

Réponse

action utilisateur

bouton [Exécuter]

paramètres envoyés

action=sql?phase=1&PHPSESSID=[PHPSESSID]

page réponse

infos.php

VII-D-6-g - Travail à faire

Écrire les scripts et fonctions nécessaires à l'application :

identifiant

type

rôle

apparticles.php

script

le point d'entrée du traitement des demandes des clients

authentifier_0

fonction

traite la demande paramétrée action=authentifier&phase=0

authentifier_1

fonction

traite la demande paramétrée action=authentifier&phase=1

authentifier_2

fonction

traite la demande paramétrée action=authentifier&phase=2

addarticle_0

fonction

traite la demande paramétrée action=addArticle&phase=0

addarticle_1

fonction

traite la demande paramétrée action=addArticle&phase=1

addarticle_2

fonction

traite la demande paramétrée action=addArticle&phase=2

updatearticle_0

fonction

traite la demande paramétrée action=updatearticle&phase=0

updatearticle_1

fonction

traite la demande paramétrée action=updatearticle&phase=1

updatearticle_2

fonction

traite la demande paramétrée action=updatearticle&phase=2

updatearticle_3

fonction

traite la demande paramétrée action=updatearticle&phase=3

deletearticle_0

fonction

traite la demande paramétrée action=deletearticle&phase=0

deletearticle_1

fonction

traite la demande paramétrée action=deletearticle&phase=1

deletearticle_2

fonction

traite la demande paramétrée action=deletearticle&phase=2

selectarticle_0

fonction

traite la demande paramétrée action=selectarticle&phase=0

selectarticle_1

fonction

traite la demande paramétrée action=selectarticle&phase=1

selectarticle_2

fonction

traite la demande paramétrée action=selectarticle&phase=2

sql_0

fonction

traite la demande paramétrée action=sql&phase=0

sql_1

fonction

traite la demande paramétrée action=sql&phase=1

sql_2

fonction

traite la demande paramétrée action=sql&phase=2

main.php

script

génère la page type

login.php

script

génère la page de login

erreurs.php

script

génère la page d'erreurs

infos.php

script

génère la page d'informations

addarticle.php

script

génère la page d'ajout d'un article

updatearticle1.php

script

génère la page 1 de la modification d'un article

updatearticle2.php

script

génère la page 2 de la modification d'un article

deletearticle1.php

script

génère la page 1 de la suppression d'un article

deletearticle2.php

script

génère la page 2 de la suppression d'un article

select1.php

script

génère la page 1 de la sélection d'articles

select2.php

script

génère la page 2 de la sélection d'articles

sql1.php

script

génère la page 1 de l'émission de requêtes

sql2.php

script

génère la page 2 de l'émission de requêtes

VII-D-7 - Faire évoluer l'application

Nous avons à ce point une application qui fait ce qu'elle doit faire avec une ergonomie acceptable. Nous allons la faire évoluer sur différents points :

- le SGBD

- sa sécurité

- son look

- ses performances

VII-D-7-a - Changer le type de la base de données

Notre étude supposait que le SGBD utilisé était MySQL. Changez de SGBD et montrez que la seule modification à faire est dans la définition de la variable $dDSN dans le fichier de configuration config.php.

VII-D-7-b - Améliorer la sécurité

Lors du développement d'une application web, il ne faut jamais faire l'hypothèse que le client est un navigateur et que la demande qu'il nous envoie est contrôlée par le formulaire qu'on lui a envoyé avant cette demande. N'importe quel programme peut être client d'une application web et donc envoyer n'importe quelle demande paramétrée ou non à l'application. Celle-ci doit donc tout vérifier.

Si on se reporte au code du script apparticles.php, on constate

- qu'aucune action autre que l'authentification ne peut avoir lieu sans session. Celle-ci n'existe que si l'utilisateur a réussi à s'authentifier. Rappelons qu'une session est identifiée par une chaîne de caractères assez longue appelée le jeton de session et qui a la forme suivante : 176a43609572907333118333edf6d1fb. Ce jeton peut être envoyé à l'application de diverses façons, par exemple en utilisant une URL paramétrée :

apparticles.php?PHPSESSID=176a43609572907333118333edf6d1fb.

Un programme qui demanderait, de façon répétée, l'URL précédente en faisant varier le jeton de façon aléatoire dans l'espoir de trouver le bon jeton, a toute chance de mettre de nombreux jours avant de générer la bonne combinaison tellement le nombre des combinaisons possibles est grand. D'ici là, la session étant d'une durée limitée, sera très probablement terminée. Un autre risque serait que le jeton, passant en clair sur le réseau, soit intercepté. Le risque est réel. On peut alors utiliser une connexion cryptée entre le serveur et son client.

- qu'une fois la session lancée, seules certaines actions sont autorisées. Une URL paramétrée par action=tricher&phase=0&PHPSESSID=[PHPSESSID] serait rejetée car l'action 'tricher' n'est pas une action autorisée. Lorsque les paramètres (action, phase) ne sont pas reconnus, notre application répond par la page d'authentification.

Cependant l'application ne vérifie pas si les actions autorisées s'enchaînent correctement. Par exemple, les deux actions suivantes :

- action=addArticle&phase=0&PHPSESSID=[PHPSESSID]

- action=updateArticle&phase=1&PHPSESSID=[PHPSESSID]

sont deux actions autorisées. Cependant l'action 2 n'est pas autorisée à suivre l'action 1.

Comment suivre l'enchaînement des URL demandées par le navigateur client ?

On peut s'aider de deux variables PHP : $_SERVER['REQUEST_URI] et $_SERVER['HTTP_REFERER] qui sont deux informations envoyées par les navigateurs clients dans leurs entêtes HTTP.

$_SERVER['REQUEST_URI] : C'est l'URI demandée par le client. Par exemple

1. /apparticles.php?action=addArticle&phase=0&PHPSESSID=[PHPSESSID]

$_SERVER['HTTP_REFERER] : C'est l'URL qui était visualisée dans le navigateur avant la nouvelle URL qu'est en train de demander le navigateur (l'URI précédente). Par exemple, si le navigateur qui a visualisé l'URI évoquée précédemment fait une nouvelle demande à un serveur, la variable $_SERVER['HTTP_REFERER'] de celui-ci aura pour valeur

1. http://machine:port//apparticles.php?action=addArticle&phase=0&PHPSESSID=[PHPSESSID]

Pour vérifier que deux actions de notre application se suivent dans l'ordre, on peut procéder ainsi :

Lors de l'action 1 :

- on note l'URI demandée (URI1) et on la note dans la session

Lors de l'action 2 :

- on récupère le HTTP-REFERER de l'action 2. On en déduit l'URI (URI2) de l'URL qui était précédemment visualisée dans le navigateur qui fait la demande.

- on récupère l'URI URI1 qui était mémorisée dans la session et qui est l'URI de l'action demandée précédemment au serveur

- Si l'action 2 suit l'action 1, alors on doit avoir URI2=URI1. Si ce n'était pas le cas, on refuserait de faire l'action demandée et on présenterait la page d'authentification.

- on note dans la session l'URI URI2 de l'action en cours pour vérification de l'action suivante. Et ainsi de suite.

Voici un exemple. Après authentification, on choisit le lien [Ajouter un article] :

L'URL de cette page est :

1. http://localhost:81/st/php/articles/gestion/articles8/apparticles.php?action=addArticle&phase=0&PHPSESSID=006a63e6027f16c70b63cdae93405eeb

Directement dans le champ [Adresse] du navigateur, nous modifions l'URL de la façon suivante :

1. http://localhost:81/st/php/articles/gestion/articles8/apparticles.php?action=deleteArticle&phase=1&PHPSESSID=006a63e6027f16c70b63cdae93405eeb

Nous obtenons alors la page d'authentification :

Cela mérite une explication. Lorsqu'on demande une URL en tapant directement son identité dans le champ adresse du navigateur, celui-ci n'envoie pas l'entête HTTP_REFERER. Notre application n'y trouve donc pas l'URI de l'action précédente, URI qu'elle avait mémorisée dans la session. Elle renvoie alors la page d'authentification en réponse.

Ce mécanisme est efficace pour les navigateurs mais nullement pour un client programmé. Celui-ci peut envoyer l'entête HTTP_REFERER qu'il veut. Il peut donc "tricher" en disant qu'il est bien passé par telle étape alors qu'il ne l'a pas fait. Il faut alors s'assurer que l'enchaînement des étapes est respecté. Ainsi si l'action demandée est action=addArticle&phase=1 (saisie) alors l'action précédente doit être forcément action=deleteArticle&phase=0 (demande initiale de la page de saisie) ou action=addArticle&phase=2 (retour en saisie après ajout erroné). De même si l'action demandée est action=addArticle&phase=2 (ajout) alors l'action précédente doit être action=addArticle&phase=1 (saisie). On peut forcer l'utilisateur à respecter ces enchaînements.

Alors que le premier mécanisme est général et peut s'appliquer à toute application, le second nécessite un codage spécifique à chaque application et est plus lourd : il faut passer en revue toutes les actions possibles de l'utilisateur et leurs enchaînements. On peut mémoriser ces derniers dans un dictionnaire comme le montre le code qui suit :

1. // authentification

2. $dPrec['authentifier']['0']=array();

3. $dPrec['authentifier']['1']=array(

4. array('action'=>'authentifier','phase'=>'0'),

5. array('action'=>'authentifier','phase'=>'2')

6. );

7. $dPrec['authentifier']['2']=array(

8. array('action'=>'authentifier','phase'=>'1'),

9. );

11. // ajout d'article

12. $dPrec['addarticle']['0']=array();

13. $dPrec['addarticle']['1']=array(

14. array('action'=>'addarticle','phase'=>'0'),

15. array('action'=>'addarticle','phase'=>'2')

16. );

17. $dPrec['addarticle']['2']=array(

18. array('action'=>'addarticle','phase'=>'1'),

19. );

21. // modification d'article

22. $dPrec['updatearticle']['0']=array();

23. $dPrec['updatearticle']['1']=array(

24. array('action'=>'updatearticle','phase'=>'0'),

25. );

26. $dPrec['updatearticle']['2']=array(

27. array('action'=>'updatearticle','phase'=>'1'),

28. array('action'=>'updatearticle','phase'=>'3')

29. );

30. $dPrec['updatearticle']['3']=array(

31. array('action'=>'updatearticle','phase'=>'2'),

32. );

34. // suppression d'article

35. $dPrec['deletearticle']['0']=array();

36. $dPrec['deletearticle']['1']=array(

37. array('action'=>'deletearticle','phase'=>'0'),

38. );

39. $dPrec['deletearticle']['2']=array(

40. array('action'=>'deletearticle','phase'=>'1'),

41. );

43. // sélection d'articles

44. $dPrec['selectarticle']['0']=array();

45. $dPrec['selectarticle']['1']=array(

46. array('action'=>'selectarticle','phase'=>'0'),

47. array('action'=>'selectarticle','phase'=>'2')

48. );

49. $dPrec['selectarticle']['2']=array(

50. array('action'=>'selectarticle','phase'=>'1'),

51. );

53. // requête administrateur

54. $dPrec['sql']['0']=array();

55. $dPrec['sql']['1']=array(

56. array('action'=>'sql','phase'=>'0'),

57. array('action'=>'sql','phase'=>'2')

58. );

59. $dPrec['sql']['2']=array(

60. array('action'=>'sql','phase'=>'1'),

61. );

$dPrec['action']['phase'] est un tableau qui contient les actions qui peuvent précéder l'action et la phase qui servent d'index au dictionnaire. Ces actions précédentes sont elles aussi représentées par un dictionnaire à deux clés 'action' et 'phase'. Si une action peut être précédée par toute action alors $dPrec['action']['phase'] sera un tableau vide. L'absence d'une action dans le dictionnaire signifie qu'elle n'est pas autorisée. Considérons l'action "authentifier" ci-dessus :

1. // authentification

2. $dPrec['authentifier']['0']=array();

3. $dPrec['authentifier']['1']=array(

4. array('action'=>'authentifier','phase'=>'0'),

5. array('action'=>'authentifier','phase'=>'2')

6. );

7. $dPrec['authentifier']['2']=array(

8. array('action'=>'authentifier','phase'=>'1'),

9. );

Le code ci-dessus signifie que l'action action=authentifier&phase=0 peut être précédée de toute action, que action=authentifier&phase=1 peut être précédée de action=authentifier&phase=0 ou de action=authentifier&phase=2 et que action=authentifier&phase=2 peut être précédée de l'action action=authentifier&phase=1.

Écrire la fonction suivante :

1. // ---------------------------------------------------------------

2. function enchainementOK(&$dConfig, &$dSession, $sAction, $sPhase){

3. // vérifie si l'action en cours ($sAction, $sPhase) peut suivre l'action précédente

4. // mémorisée dans $dSession['précédent']

5. // le dictionnaire des enchaînements autorisés est dans $dConfig['précédents']

6. // rend TRUE si l'enchaînement est possible, FALSE sinon

7. ....

Cette fonction permet à l'application principale de vérifier que l'enchaînement des actions est correct :

1. <?php

2. // gestion d'une table d'articles

3. include "config.php";

4. include "articles.php";

6. // session

7. session_start();

8. $dSession=$_SESSION["session"];

10. // action à entreprendre

11. $sAction=$_POST["action"] ? $_POST["action"] : $_GET["action"] ? $_GET["action"] : "authentifier";

12. $sAction=strtolower($sAction);

13. // phase éventuelle de l'action

14. $sPhase=$_POST["phase"] ? $_POST["phase"] : $_GET["phase"] ? $_GET["phase"] : "0";

16. // y-a-t-il une session en cours ?

17. if(! isset($dSession)){

18. // authentification de l'utilisateur

19. if($sAction=="authentifier" && $sPhase=="0") authentifier_0($dConfig);

20. if($sAction=="authentifier" && $sPhase=="1") authentifier_1($dConfig);

21. if($sAction=="authentifier" && $sPhase=="2") authentifier_2($dConfig);

22. // action anormale

23. authentifier_0($dConfig);

24. }//if - pas de session

26. // on récupère la session

27. $dSession=unserialize($dSession);

29. // l'enchaînement des actions est-il normal ?

30. if( ! enchainementOK($dConfig,$dSession,$sAction,$sPhase)){

31. // enchaînement anormal

32. authentifier_0($dConfig);

33. }//if

35. // traitement des actions

36. if($sAction=="authentifier"){

37. if($sPhase=="0") authentifier_0($dConfig);

38. if($sPhase=="1") authentifier_1($dConfig);

39. if($sPhase=="2") authentifier_2($dConfig);

40. }//if

41. if($sAction=="addarticle"){

42. ...

VII-D-7-c - Faire évoluer le "look"

Rappelons-nous qu'une des conditions posées lors de l'étude de cette application était qu'elle devait être évolutive. Supposons qu'au bout de quelques semaines, on s'aperçoive que l'ergonomie de l'application doit être améliorée. Modifiez l'application de telle façon que la structure et la présentation de la page type soient changées. Les modifications auront lieu à deux endroits :

- dans le script main.php qui définit la structure de la page type. Faites évoluer celle-ci.

- dans la feuille de style qui donne le "look" de l'application. Changez celui-ci.

VII-D-7-d - Améliorer les performances

Pour l'instant, nous avons opté pour un navigateur client léger : il ne fait rien d'autre que de la présentation. On peut lui faire faire du traitement en incluant des scripts dans les pages Web qu'on lui envoie. Ceux-ci peuvent être en différents langages, notablement vbscript et javascript. Internet Explorer et Netscape dominent le marché des navigateurs dans une proportion proche de 60/40. Par ailleurs, IE n'existe que dans le domaine Windows et pas sur Unix par exemple où c'est Netscape qui prédomine. Netscape n'exécute pas nativement les scripts vbscript alors que les deux navigateurs exécutent les scripts javascript. Comme Netscape occupe encore une part significative du marché des navigateurs, les scripts vbscript sont à éviter. C'est donc javascript qui est généralement utilisé dans les scripts côté client.

Sont délégués aux scripts côté client, des traitements où le serveur n'a pas à intervenir. Dans notre application, il serait intéressant que le navigateur client n'envoie une demande au serveur qu'après l'avoir vérifiée. Ainsi il est inutile d'envoyer au serveur une demande d'authentification alors que l'utilisateur a laissé le champ [login] vide dans le formulaire d'authentification. Il est préférable de prévenir l'utilisateur que sa demande est erronée :

On remarquera que cela n'empêchera pas le serveur de vérifier que le champ login est non vide car son client n'est pas forcément un navigateur et alors la vérification précédente a pu ne pas être faite. Faire l'hypothèse que le client est un navigateur est un risque majeur pour la sécurité de l'application.

Reprenez les différents moments où le navigateur envoie des informations au serveur et lorsque celles-ci peuvent être vérifiées, écrivez une ou des fonctions javascript qui permettront au navigateur de vérifier la validité des informations avant leur envoi au serveur.

Pour reprendre l'exemple précédent, le script login.php qui génère la page d'authentification devient le suivant :

1. <script language="javascript">

2. function check(){

3. // on vérifie qu'il y a bien un login

4. with(document.frmLogin){

5. champs=/^\s*$/.exec(txtLogin.value);

6. if(champs!=null){

7. // pas de login

8. alert("Vous n'avez pas indiqué de login");

9. txtLogin.focus();

10. return;

11. }//if

12. // les données sont là - on les envoie au serveur

13. submit();

14. }//with

15. }//check

16. </script>

18. <form name="frmLogin" method="post" action="<?php echo $main["post"] ?>">

19. <table>

20. <tr>

21. <td>login</td>

22. <td><input type="text" value="<?php echo $main["login"] ?>" name="txtLogin" class="text"></td>

23. </tr>

24. <tr>

25. <td>mot de passe</td>

26. <td><input type="password" value="" name="txtMdp" class="text"></td>

27. <td><input type="button" onclick="check()" value="Connexion" class="submit"></td>

28. </tr>

29. </table>

30. </form>

VII-D-8 - Pour aller plus loin

Pour terminer, signalons quelques pistes pour approfondir cette étude de cas :

- il serait intéressant de voir si la page type de cette application ne pourrait pas faire l'objet d'une classe. Celle-ci pourrait être alors utilisée dans d'autres applications.

- notre application est bien adaptée à des clients de type navigateur mais moins à des clients de type "Application autonome". Celles-ci doivent :

- créer une connexion tcp avec le serveur

- lui "parler" HTTP

- analyser ses réponses HTML pour y trouver l'information désirée puisque le client autonome ne sera probablement pas intéressé par le code HTML de présentation destiné aux navigateurs.

Il serait intéressant que notre application génère du XML plutôt que du HTML. Ses clients pourraient alors être indifféremment des navigateurs (assez récents quand même) ou des applications autonomes. Ces dernières n'auraient aucune difficulté à retrouver l'information qu'elles recherchent puisque la réponse XML du serveur ne contiendrait aucune information de présentation, seulement du contenu.

- il faudrait très certainement s'intéresser aux accès simultanés à la base d'articles. Il y a au moins deux points à éclaircir :

- est-ce que le SGBD utilisé par l'application gère correctement l'accès simultané à un même article ? Par exemple, que se passe-t-il si deux utilisateurs modifient le même article au même moment (ils appuient sur le bouton [Modifier] en même temps) ? Cela dépend probablement du SGBD sous-jacent.

- actuellement notre application ne gère pas les accès simultanés. Cependant, la base devrait rester dans un état cohérent même si des surprises sont à attendre. Prenons la séquence d'événements suivante :

- l'utilisateur U1 entre en modification d'un article

- l'utilisateur U2 entre en suppression du même article un peu après

- chacune des deux actions nécessite des échanges client-serveur. Selon la façon de travailler de chacun, l'utilisateur U2 peut terminer son travail avant U1. Lorsque celui-ci va terminer ses modification et les valider par [Modifier], il aura la page d'informations en réponse, le SGBD lui indiquant que [0 ligne(s) ont été modifiées], ceci parce que la page qu'il voulait modifier a été supprimée entre-temps. L'utilisateur sera sans doute surpris. D'un point de vue ergonomie, il serait sans doute préférable d'afficher une page signalant mieux l'erreur. Par ailleurs, on pourrait envisager d'offrir à l'utilisateur un accès exclusif à un article dès qu'il entrerait en mise à jour de celui-ci. Un autre utilisateur voulant entrer en mise à jour du même article se verrait répondre qu'une autre mise à jour est en cours. Cela posera problème si le premier utilisateur tarde à valider sa mise à jour : les autres seront bloqués. Il y a là des solutions à trouver qui dépendront assez largement des capacités du SGBD utilisé. Oracle a, par exemple, davantage de capacités dans ce domaine que MySQL.

VII-E - PEAR DB: a unified API for accessing SQL-databases

This chapter describes how to use the PEAR database abstraction layer.

DSN -- The data source name

Connect -- Connecting and disconnecting

Query -- Performing a query against a database.

Fetch -- Fetching rows from the query

VII-E-1 - DSN

To connect to a database through PEAR::DB, you have to create a valid DSN - data source name. This DSN consists in the following parts:

phptype: Database backend used in PHP (i.e. mysql, odbc etc.)

dbsyntax: Database used with regards to SQL syntax etc.

protocol: Communication protocol to use ( i.e. tcp, unix etc.)

hostspec: Host specification (hostname[:port])

database: Database to use on the DBMS server

username: User name for login

password: Password for login

proto_opts: Maybe used with protocol

The format of the supplied DSN is in its fullest form:

1. phptype(dbsyntax)://username:password@protocol+hostspec/database

Most variations are allowed:

1. phptype://username:password@protocol+hostspec:110//usr/db_file.db

2. phptype://username:password@hostspec/database_name

3. phptype://username:password@hostspec

4. phptype://username@hostspec

5. phptype://hostspec/database

6. phptype://hostspec

7. phptype(dbsyntax)

8. phptype

The currently supported database backends are:

1. mysql  -> MySQL

2. pgsql  -> PostgreSQL

3. ibase  -> InterBase

4. msql   -> Mini SQL

5. mssql  -> Microsoft SQL Server

6. oci8   -> Oracle 7/8/8i

7. odbc   -> ODBC (Open Database Connectivity)

8. sybase -> SyBase

9. ifx    -> Informix

10. fbsql  -> FrontBase

With an up-to-date version of DB, you can use a second DSN format :

1. phptype(syntax)://user:pass@protocol(proto_opts)/database

3. example: Connect to database through a socket

4. mysql://user@unix(/path/to/socket)/pear

6. Connect to database on a non standard port

7. pgsql://user:pass@word@tcp(localhost:5555)/pear

VII-E-2 - Connect

To connect to a database you have to use the function DB::connect(), which requires a valid DSN as parameter and optional a boolean value, which determines wether to use a persistent connection or not. In case of success you get a new instance of the database class. It is strongly recommened to check this return value with DB::isError(). To disconnect use the method disconnect() from your database class instance.

1. <?php

2. require_once 'DB.php';

4. $user = 'foo';

5. $pass = 'bar';

6. $host = 'localhost';

7. $db_name = 'clients_db';

9. // Data Source Name: This is the universal connection string

10. $dsn = "mysql://$user:$pass@$host/$db_name";

12. // DB::connect will return a PEAR DB object on success

13. // or an PEAR DB Error object on error

14. $db = DB::connect($dsn, true);

16. // Alternatively: $db = DB::connect($dsn);

18. // With DB::isError you can differentiate between an error or

19. // a valid connection.

20. if (DB::isError($db)) {

21. die ($db->getMessage());

22. }

23. ....

24. // You can disconnect from the database with:

25. $db->disconnect();

26. ?>

VII-E-3 - Query

To perform a query against a database you have to use the function query(), that takes the query string as an argument. On failure you get a DB Error object, check it with DB::isError(). On succes you get DB_OK (predefined PEAR::DB constant) or when you set a SELECT-statment a DB Result object.

1. <?php

2. // Once you have a valid DB object...

3. $sql = "select * from clients";

4. $result = $db->query($sql);

6. // Always check that $result is not an error

7. if (DB::isError($result)) {

8. die ($result->getMessage());

9. }

10. ....

11. ?>

VII-E-4 - Fetch

The DB_Result object provides two functions to fetch rows: fetchRow() and fetchInto(). fetchRow() returns the row, null on no more data or a DB_Error, when an error occurs. fetchInto() requires a variable, which be will directly assigned by reference to the result row. It will return null when result set is empty or a DB_Error too.

1. <?php

2. ...

3. $db = DB::connect($dsn);

4. $res = $db->query("select * from mytable");

6. // Get each row of data on each iteration until

7. // there is no more rows

8. while ($row = $res->fetchRow()) {

9. $id = $row[0];

10. }

11. // Or:

12. // an example using fetchInto()

13. while ($res->fetchInto($row)) {

14. $id = $row[0];

15. }

16. ?>

VII-E-4-a - Select the format of the fetched row

The fetch modes supported are:

- DB_FETCHMODE_ORDERED (default) : returns an ordered array. The order is taken from the select statment.

1. <?php

2. $res = $db->query('select id, name, email from users');

3. $row = $res->fetchRow(DB_FETCHMODE_ORDERED);

4. /*

5. $row will contain:

6. array (

7. 0 => <column "id" data>,

8. 1 => <column "name" data>,

9. 2 => <column "email" data>

10. )

11. */

12. // Access the data with:

13. $id    = $row[0];

14. $name  = $row[1];

15. $email = $row[2];

16. ?>

1. DB_FETCHMODE_ASSOC : returns an associative array with the column names as the array keys

1. <?php

2. $res = $db->query('select id, name, email from users');

3. $row = $res->fetchRow(DB_FETCHMODE_ASSOC);

4. /*

5. $row will contain:

6. array (

7. 'id'    => <column "id" data>,

8. 'name'  => <column "name" data>,

9. 'email' => <column "email" data>

10. )

11. */

12. // Access the data with:

13. $id    = $row['id'];

14. $name  = $row['name'];

15. $email = $row['email'];

16. ?>

- DB_FETCHMODE_OBJECT  : returns a DB_row object with column names as properties

1. <?php

2. $res = $db->query('select id, name, email from users');

3. $row = $res->fetchRow(DB_FETCHMODE_OBJECT);

4. /*

5. $row will contain:

6. db_row Object

7. (

8. [id]    => <column "id" data>,

9. [name]  => <column "name" data>,

10. [email] => <column "email" data>

11. )

12. */

13. // Access the data with:

14. $id    = $row->id;

15. $name  = $row->name;

16. $email = $row->email;

17. ?>

VII-E-4-b - Set the format of the fetched row

You can set the fetch mode for every call or for your whole DB instance.

1. <?php

2. // 1) Set the mode per call:

3. while ($row = $result->fetchRow(DB_FETCHMODE_ASSOC)) {

4. $id = $row['id'];

5. }

7. // 2) Set the mode for all calls:

8. $db = DB::connect($dsn);

9. // this will set a default fetchmode for this Pear DB instance

10. // (for all queries)

11. $db->setFetchMode(DB_FETCHMODE_ASSOC);

13. $result = $db->query(...);

14. while ($row = $result->fetchRow()) {

15. $id = $row['id'];

16. }

17. ?>

VII-E-4-c - Fetch rows by number

The PEAR DB fetch system also supports an extra parameter to the fetch statement. So you can fetch rows from a result by number. This is especially helpful if you only want to show sets of an entire result (for example in building paginated HTML lists), fetch rows in an special order, etc.

1. <?php

2. ...

3. // the row to start fetching

4. $from = 50;

6. // how many results per page

7. $res_per_page = 10;

9. // the last row to fetch for this page

10. $to = $from + $res_per_page;

12. foreach (range($from, $to) as $rownum) {

13. if (!$row = $res->fetchrow($fetchmode, $rownum)) {

14. break;

15. }

16. $id = $row[0];

17. ....

18. }

19. ?>

VII-E-4-d - Freeing the result set

It is recommended to finish the result set after processing in order to to save memory. Use free() to do this.

1. <?php

2. ...

3. $result = $db->query('SELECT * FROM clients');

4. while ($row = $result->fetchRow()) {

5. ...

6. }

7. $result->free();

8. ?>

VII-E-4-e - Quick data retrieving

PEAR DB provides some special ways to retrieve information from a query without the need of using fetch*() and loop throw results.

getOne() retrieves the first result of the first column from a query

1. $numrows = $db->getOne('select count(id) from clients');

getRow() returns the first row and return it as an array

1. $sql = 'select name, address, phone from clients where id=1';

2. if (is_array($row = $db->getRow($sql))) {

3. list($name, $address, $phone) = $row;

4. }

getCol() returns an array with the data of the selected column. It accepts the column number to retrieve as the second param.

1. $all_client_names = $db->getCol('select name from clients');

The above sentence could return for example: $all_client_names = array('Stig', 'Jon', 'Colin');

getAssoc() fetches the entire result set of a query and return it as an associative array using the first column as the key.

1. $data = getAssoc('SELECT name, surname, phone FROM mytable')

2. /*

3. Will return:

4. array(

5. 'Peter'      => array('Smith', '944679408'),

6. 'Tomas' => array('Cox', '944679408'),

7. 'Richard' => array('Merz', '944679408')

8. )

9. */

getAll() fetches all the rows returned from a query

1. $data = getAll('SELECT id, text, date FROM mytable');

2. /*

3. Will return:

4. array(

5. 1 => array('4', 'four', '2004'),

6. 2 => array('5', 'five', '2005'),

7. 3 => array('6', 'six', '2006')

8. )

9. */

The get*() family methods will do all the dirty job for you, this is: launch the query, fetch the data and free the result. Please note that as all PEAR DB functions they will return a PEAR DB_error object on errors.

VII-E-4-f - Getting more information from query results

With PEAR DB you have many ways to retrieve useful information from query results. These are:

- numRows(): Returns the total number of rows returned from a "SELECT" query.

1. // Number of rows

2. echo $res->numRows();

- numCols(): Returns the total number of columns returned from a "SELECT" query.

1. // Number of cols

2. echo $res->numCols();

- affectedRows(): Returns the number of rows affected by a data manipulation query ("INSERT", "UPDATE" or "DELETE").

1. // remember that this statement won't return a result object

2. $db->query('DELETE * FROM clients');

3. echo 'I have deleted ' . $db->affectedRows() . ' clients';

- tableInfo(): Returns an associative array with information about the returned fields from a "SELECT" query.

1. // Table Info

2. print_r($res->tableInfo());