Skip to content

3. Wprowadzenie do programowania internetowego w PHP

3.1. Programowanie w języku PHP

Warto tutaj przypomnieć, że PHP jest pełnoprawnym językiem programowania i choć stosuje się go głównie przy tworzeniu aplikacji internetowych, można go wykorzystywać również w innych kontekstach. Dokument „PHP na przykładach”, dostępny pod adresem http://shiva.istia.univ-angers.fr/~tahe/pub/php/php.pdf, przedstawia podstawy tego języka. Zakładamy tutaj, że wiedza ta została już przyswojona. Na prostym przykładzie pokażemy procedurę uruchamiania programu PHP w systemie Windows. Poniższy kod został zapisany pod nazwą coucou.php.

<?php
    echo "coucou";
?>

Program ten uruchamia się w oknie DOS systemu Windows:


dos>"e:\program files\easyphp\php\php.exe" coucou.php
X-Powered-By: PHP/4.3.0-dev
Content-type: text/html

coucou

Warto zauważyć, że interpreter PHP domyślnie wysyła:

  • interpreter PHP to php.exe i zazwyczaj znajduje się w katalogu <php> instalacji oprogramowania.
  • nagłówki HTTP X-Powered-By oraz Content-type:
  • pusty wiersz oddzielający nagłówki HTTP od reszty dokumentu
  • dokument utworzony tutaj z tekstu wygenerowanego przez funkcję echo

3.2. Plik konfiguracyjny interpretera PHP

Działanie interpretera PHP jest konfigurowane za pomocą pliku konfiguracyjnego o nazwie php.ini, który w systemie Windows znajduje się w samym katalogu systemu Windows. Jest to plik o znacznej wielkości, ponieważ w systemie Windows i dla wersji 4.2 programu PHP liczy on prawie 1000 wierszy, z których na szczęście trzy czwarte stanowią komentarze. Przyjrzyjmy się niektórym atrybutom konfiguracyjnym programu PHP:

short_open_tag = On
umożliwia umieszczanie instrukcji między tagami <? >. W przypadku off należy je umieścić między tagami <?php ... >
asp_tags = Off
w on umożliwia stosowanie składni <% =zmienna %> używanej w technologii ASP (Active Server Pages)
expose_php = On
umożliwia wysłanie nagłówka HTTP X-Powered-By: PHP/4.3.0-dev. W off nagłówek ten jest usuwany.
error_reporting  =  E_ALL & ~E_NOTICE
określa zakres monitorowania błędów. W tym przypadku zgłaszane będą wszystkie błędy (E_ALL) z wyjątkiem ostrzeżeń pojawiających się podczas wykonywania (~E_NOTICE)
display_errors = Off
przy wartości on umieszcza błędy w strumieniu HTML wysyłanym do klienta. Są one zatem wyświetlane w przeglądarce. Zaleca się ustawienie tej opcji na wartość off.
log_errors = On
błędy zostaną zapisane w pliku
track_errors = On
zapisuje ostatni wystąpiły błąd w zmiennej $php_errormsg
error_log = E:\Program Files\EasyPHP\php\erreurs.log
ustawia plik do zapisywania błędów (jeśli log_errors=on)
register_globals = Off
w on pewna liczba zmiennych staje się globalna. Uznane za lukę w zabezpieczeniach.
default_mimetype = "text/html"
domyślnie generuje nagłówek HTTP: Content-type: text/html
include_path    =".;E:\Program Files\EasyPHP\php\pear\"
lista katalogów, które zostaną przeszukane w poszukiwaniu plików wymaganych przez dyrektywy include lub require
session.save_path = /temp
katalog, w którym będą zapisywane pliki przechowujące informacje o różnych bieżących sesjach. Dotyczy to dysku, na którym zainstalowano PHP. W tym przypadku /temp odnosi się do e:\temp

Ten plik konfiguracyjny wpływa na przenośność napisanego programu w języku PHP. W rzeczywistości, jeśli aplikacja internetowa ma pobrać wartość pola C z formularza internetowego, może to zrobić na różne sposoby, w zależności od tego, czy zmienna konfiguracyjna register_globals ma wartość on czy off:

  • off: wartość zostanie pobrana przez $HTTP_GET_VARS["C"] lub _GET["C"] lub $HTTP_POST_VARS["C"] lub $_POST["C"], w zależności od metody (GET/POST) zastosowanej przez klienta do wysłania wartości formularza
  • : tak samo jak powyżej, plus $C, ponieważ wartość pola C została przypisana do zmiennej globalnej o tej samej nazwie co pole

Jeśli programista napisze program, używając notacji $C, ponieważ serwer WWW/PHP, z którego korzysta, przypisuje zmiennej o nazwie register_globals wartość on, program ten przestanie działać, jeśli zostanie przeniesiony na serwer WWW/PHP, na którym ta sama zmienna ma wartość off. Należy zatem starać się pisać programy, unikając korzystania z funkcji zależnych od konfiguracji serwera WWW/PHP.

3.3. Konfiguracja PHP podczas wykonywania

Aby poprawić przenośność programu PHP, można samodzielnie ustawić niektóre zmienne konfiguracyjne PHP. Zmiany te są wprowadzane na czas wykonywania programu i dotyczą wyłącznie tego konkretnego programu. W tym procesie przydatne są dwie funkcje:

ini_get("confVariable")
zwraca wartość zmiennej konfiguracyjnej confVariable
ini_set("confVariable","valeur")
ustawia wartość zmiennej konfiguracyjnej confVariable

Oto przykład, w którym ustala się wartość zmiennej konfiguracyjnej track_errors:

<?php
     // wartość zmiennej konfiguracyjnej track_errors
    echo "track_errors=".ini_get("track_errors")."\n";
  // zmiana tej wartości
  ini_set("track_errors","off");
   // weryfikacja
  echo "track_errors=".ini_get("track_errors")."\n";
?>

Po uruchomieniu otrzymujemy następujące wyniki:

E:\data\serge\web\php\poly\intro>"E:\Program Files\EasyPHP\php\php.exe" conf1.php
Content-type: text/html

track_errors=1
track_errors=off

Wartość zmiennej konfiguracyjnej track_errors wynosiła początkowo 1 (~on). Zmieniono ją na off. Należy pamiętać, że jeśli nasza aplikacja ma opierać się na określonych wartościach zmiennych konfiguracyjnych, rozsądnie jest zainicjować je w samym programie.

3.4. Kontekst wykonywania przykładów

Przykłady zawarte w niniejszym skryptie będą wykonywane przy następującej konfiguracji:

  • PC w systemie Windows 2000
  • serwer Apache 1.3
  • PHP 4.3

Konfiguracja serwera Apache jest zawarta w pliku httpd.conf. Poniższe wiersze nakazują serwerowi Apache załadowanie pliku PHP jako modułu zintegrowanego z serwerem Apache oraz przekazywanie do interpretera PHP wszystkich żądań dotyczących dokumentów o określonych rozszerzeniach, w tym .php. Jest to domyślny rozszerzenie, którego będziemy używać w naszych programach PHP.

LoadModule php4_module "E:/Program Files/EasyPHP/php/php4apache.dll"
AddModule mod_php4.c
AddType application/x-httpd-php .phtml .pwml .php3 .php4 .php .php2 .inc

Ponadto zdefiniowaliśmy dla serwera Apache alias „poly”:

Alias "/poly/" "e:/data/serge/web/php/poly/"
<Directory "e:/data/serge/web/php/poly">
  Options Indexes FollowSymLinks Includes
  AllowOverride All
   #Polecenie allow,deny
  Allow from all
</Directory>

Nazwijmy ścieżkę <poly> ścieżką e:/data/serge/web/php/poly. Jeśli chcemy za pomocą przeglądarki zażądać od serwera Apache dokumentu o nazwie doc.php, użyjemy adresu http://localhost/poly/doc.php. Serwer Apache rozpozna w URL alias poly, a następnie powiąże ścieżkę URL /poly/doc.php z dokumentem <poly>\doc.php.

3.5. Pierwszy przykład

Napiszmy pierwszą aplikację internetową w PHP. Poniższy tekst jest zapisany w pliku heure.php:

<html>
  <head>
    <title>Une page php dynamique</title>
   </head>
   <body>
     <center>
     <h1>Une page PHP générée dynamiquement</h1>
     <h2>
            <?php
              $maintenant=time();
              echo date("j/m/y, h:i:s",$maintenant);
            ?>
     </h2>
     <br>
     A chaque fois que vous rafraîchissez la page, l'heure change.
   </body>
</html>

Jeśli wywołamy tę stronę w przeglądarce, otrzymamy następujący wynik:

Image

Dynamiczna część strony została wygenerowana przez kod PHP:

            <?php
              $maintenant=time();
              echo date("j/m/y, h:i:s",$maintenant);
            ?>

Co dokładnie się stało? Przeglądarka wysłała żądanie dotyczące pliku URL http://localhost/poly/intro/heure.php. Serwer WWW (w tym przykładzie Apache) odebrał to żądanie i na podstawie rozszerzenia .php żądanego dokumentu stwierdził, że musi przekazać to żądanie do interpretera PHP. Następnie interpreter ten analizuje dokument heure.php i wykonuje wszystkie fragmenty kodu znajdujące się pomiędzy tagami <?php > i zastępuje każdą z nich wierszami napisanymi przez instrukcje PHP, echo lub print. W ten sposób interpreter PHP wykona powyższy fragment kodu i zastąpi go wierszem utworzonym przez instrukcję echo:

2/10/02, 06:13:47

Po wykonaniu wszystkich fragmentów kodu PHP dokument PHP stał się zwykłym dokumentem HTML, który jest następnie wysyłany do klienta.

Należy w miarę możliwości unikać mieszania kodu PHP z kodem HTML. W tym celu poprzednią aplikację można przepisać w następujący sposób:

<!-- kod PHP -->
<?php
     // pobieramy aktualny czas
    $maintenant=time();
    $maintenant=date("j/m/y, h:i:s",$maintenant);
?>
<!-- kod HTML -->
<html>
  <head>
    <title>Une page php dynamique</title>
   </head>
   <body>
     <center>
     <h1>Une page PHP générée dynamiquement</h1>
     <h2>
            <?php echo  $maintenant ?>
     </h2>
     <br>
     A chaque fois que vous rafraîchissez la page, l'heure change.
   </body>
</html>

Wynik wyświetlany w przeglądarce jest identyczny:

Image

Druga wersja jest lepsza od pierwszej, ponieważ w kodzie PHP jest mniej kodu niż w kodzie HTML. Dzięki temu struktura strony jest bardziej przejrzysta. Można pójść o krok dalej, umieszczając kod PHP i kod HTML w dwóch różnych plikach. Kod PHP jest zapisany w pliku heure3.php:

<!-- kod PHP -->
<?php
     // pobieramy aktualny czas
    $maintenant=time();
    $maintenant=date("j/m/y, h:i:s",$maintenant);
   // wyświetla się odpowiedź
  include "heure3-page1.php";
?>

Kod HTML jest natomiast zapisany w pliku heure3-page1.php:

<!-- kod HTML -->
<html>
  <head>
    <title>Une page php dynamique</title>
   </head>
   <body>
     <center>
     <h1>Une page PHP générée dynamiquement</h1>
     <h2>
            <?php echo  $maintenant ?>
     </h2>
     <br>
     A chaque fois que vous rafraîchissez la page, l'heure change.
   </body>
</html>

Gdy przeglądarka zażąda dokumentu heure3.php, zostanie on załadowany i przeanalizowany przez interpreter PHP. Po napotkaniu wiersza

  include "heure3-page1.php";

Interpreter włączy plik heure3-page1.php do kodu źródłowego pliku heure3.php i wykona go. W ten sposób wszystko przebiega tak, jakbyśmy mieli następujący kod PHP:

<!-- kod PHP -->
<?php
     // pobieramy aktualny czas
    $maintenant=time();
    $maintenant=date("j/m/y, h:i:s",$maintenant);
?>
<!-- kod HTML -->
<html>
  <head>
    <title>Une page php dynamique</title>
   </head>
   <body>
     <center>
     <h1>Une page PHP générée dynamiquement</h1>
     <h2>
            <?php echo  $maintenant ?>
     </h2>
     <br>
     A chaque fois que vous rafraîchissez la page, l'heure change.
   </body>
</html>

Uzyskany wynik jest taki sam jak poprzednio:

Image

Rozwiązanie polegające na umieszczeniu kodów PHP i HTML w oddzielnych plikach zostanie przyjęte w dalszej części. Ma ono szereg zalet:

  • struktura stron wysyłanych do klienta nie jest zagubiona w kodzie PHP. Dzięki temu mogą być one utrzymywane przez „projektanta stron internetowych” posiadającego umiejętności graficzne, ale niewielką wiedzę na temat PHP.
  • Kod PHP pełni rolę „interfejsu” dla żądań klientów. Jego zadaniem jest obliczenie danych niezbędnych do wygenerowania strony, która zostanie zwrócona klientowi w odpowiedzi.

Rozwiązanie to ma jednak wadę: zamiast wymagać załadowania jednego dokumentu, wymaga załadowania kilku dokumentów, co może skutkować spadkiem wydajności.

3.6. Pobieranie parametrów przesłanych przez klienta internetowego

3.6.1. za pomocą POST

Rozważmy następujący formularz, w którym użytkownik musi podać dwie informacje: imię i wiek.

Image

Gdy użytkownik wypełni pola Nom i Age, naciska następnie przycisk Envoyer, który jest typu submit. Wartości z formularza są następnie wysyłane na serwer. Serwer zwraca formularz wraz z tabelą zawierającą listę otrzymanych wartości:

Image

Przeglądarka wysyła żądanie dotyczące formularza do kolejnej aplikacji o nazwie nomage.php:

<?php
   // czy mamy oczekiwane parametry?
  $post=isset($_POST["txtNom"]) && isset($_POST["txtAge"]);
  if($post){
    // pobieramy parametry txtNom i txtAge „wysłane” przez klienta
    $nom=$_POST["txtNom"];
    $age=$_POST["txtAge"];
  } else {
      $nom="";
    $age="";
  }//if
   // wyświetlenie strony
  include "nomage-p1.php";
?>

Kilka wyjaśnień:

  • pole formularza o nazwie HTML może zostać przesłane na serwer za pomocą metody GET lub metody POST. Jeśli zostanie przesłane za pomocą metody GET, serwer może je pobrać ze zmiennej $_GET["champ"] oraz ze zmiennej $_POST["champ"], jeślijeśli jest wysyłany metodą POST.
  • Istnienie danych można sprawdzić za pomocą funkcji isset(dane), która zwraca true, jeśli dane istnieją, a false w przeciwnym razie.
  • Aplikacja nomage.php tworzy trzy zmienne: $nom dla nazwy formularza, $age dla wieku oraz $post, aby wskazać, czy wartości zostały „przesłane”, czy nie. Te trzy zmienne są przekazywane do strony nomage-p1.php. Należy zauważyć, że chociaż strona ta bierze udział w tworzeniu odpowiedzi dla klienta, klient nie ma o tym wiedzy. Z jego punktu widzenia to aplikacja nomage.php udziela mu odpowiedzi.
  • Gdy klient po raz pierwszy zwraca się do aplikacji nomage.php, otrzymujemy fałszywy wynik $post. W rzeczywistości podczas tego pierwszego wywołania żadne wartości z formularza nie są przekazywane do serwera.

Strona nomage-p1.php wygląda następująco:

<html>
  <head>
    <title>Formulaire web</title>
  </head>

   <body>
     <center>
       <h3>Un formulaire Web</h3>
       <h4>Récupération des valeurs des champs d'un formulaire</h4>
       <hr>
       <form name="frmPersonne" method="post">
         <table>
           <tr>
             <td>Nom</td>
             <td><input type="text" value="<?php echo $nom ?>" name="txtNom" size="20"></td>
             <td>Age</td>
             <td><input type="text" value="<?php echo $age ?>" name="txtAge" size="3"></td>
           <tr>
         </table>
         <input type="submit" name="cmdEffacer" value="Envoyer">
       </form>
     </center>
     <hr>
     <?php
          // czy przesłano jakieś wartości? 
         if ($post) { 
     ?>
         <h4>Valeurs récupérées</h4>
      <table border="1">
          <tr>
            <td>Nom</td><td><?php echo $nom ?></td>
          <td width="10"></td>
            <td>Age</td><td><?php echo $age ?></td>
        <tr>
      </table>
    <?php } ?>
   </body>
</html>

Aplikacja nomage-p1.php zawiera formularz frmPersonne. Jest on zdefiniowany przez tag:

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

Ponieważ atrybut action tego tagu nie jest zdefiniowany, przeglądarka prześle dane formularza do URL, do którego zwróciła się, aby je uzyskać, czyli do c.a.d. aplikacji nomage.php.

Rozróżnijmy dwa przypadki wywołania aplikacji nomage.php:

  1. Użytkownik wywołuje ją po raz pierwszy. Aplikacja nomage.php wywołuje zatem aplikację nomage-p1.php, przekazując jej wartości ($nom,$age,$post)=("","",false). Aplikacja nomage-p1.php wyświetla wówczas pusty formularz.
  2. Użytkownik wypełnia formularz i korzysta z przycisku Envoyer (typu submit). Wartości formularza (txtNom, txtAge) są następnie „przesyłane” (method="post" w <form>) do aplikacji nomage.php (atrybut action nie jest zdefiniowany w <form>). Aplikacja nomage.php oblicza ($nom, $age,$post) = (txtNom, txtAge, true) i przekazuje je do aplikacji nomage-p1.php, która następnie wyświetla już wypełniony formularz wraz z tabelą pobranych wartości.

3.6.2. przez GET

W przypadku, gdy wartości z formularza są przesyłane na serwer za pomocą GET, aplikacja nomage.php staje się kolejną aplikacją nomage2.php:

<?php
   // czy otrzymano oczekiwane parametry?
  $get=isset($_GET["txtNom"]) && isset($_GET["txtAge"]);
  if($get){
     // klient pobiera parametry txtNom i txtAge „GETTés”
    $nom=$_GET["txtNom"];
    $age=$_GET["txtAge"];
  } else {
      $nom="";
    $age="";
  }//jeśli
   // wyświetlenie strony
  include "nomage-p2.php";
?>

Aplikacja nomage-p2.php jest identyczna z aplikacją nomage-p1.php, z wyjątkiem następujących szczegółów:

  • tag form został zmodyfikowany:
       <form name="frmPersonne" method="get">
  • aplikacja pobiera teraz zmienną $get zamiast $post:
     <?php
          // czy do serwera zostały wysłane jakieś wartości? 
         if ($get) { 
     ?>

Podczas działania, gdy wartości są wprowadzane w formularzu i wysyłane na serwer, przeglądarka odzwierciedla w polu URL fakt, że wartości zostały wysłane metodą GET:

Image

3.7. Pobieranie nagłówków HTTP wysłanych przez klienta internetowego

Kiedy przeglądarka wysyła żądanie do serwera internetowego, przekazuje mu pewną liczbę nagłówków HTTP. Czasami warto mieć do nich dostęp. Na początek można skorzystać z tablicy asocjacyjnej $_SERVER. Zawiera on różne informacje przekazane przez serwer internetowy, w tym między innymi nagłówki HTTP dostarczone przez klienta. Rozważmy następujący program, który wyświetla wszystkie wartości tablicy $_SERVER:

<?php
     // wyświetla zmienne związane z serwerem WWW
   // wysyłanie zwykłego tekstu
  header("Content-type: text/plain");
   // przeglądanie tablicy asocjacyjnej $_SERVER
     reset($_SERVER);
     while (list($clé,$valeur)=each($_SERVER)){
      echo "$clé : $valeur\n";
    }//while
?>

Zapiszmy ten kod w pliku headers.php i wyświetlmy plik URL w przeglądarce:

Image

Otrzymujemy pewną ilość informacji, w tym nagłówki HTTP wysłane przez przeglądarkę. Są to wartości powiązane z kluczami zaczynającymi się od HTTP. Przyjrzyjmy się bliżej niektórym z uzyskanych powyżej informacji:

HTTP_ACCEPT
typy dokumentów akceptowanych przez klienta internetowego
HTTP_ACCEPT_CHARSET
typy znaków akceptowanych w dokumentach
HTTP_ACCEPT_ENCODING
akceptowane typy kodowań dokumentów
HTTP_ACCEPT_LANGUAGE
akceptowane typy języków w dokumentach
HTTP_CONNECTION
typ połączenia z serwerem. Keep-Alive: serwer musi utrzymać otwarte połączenie po wysłaniu odpowiedzi
HTTP_KEEP_ALIVE
? maksymalny czas trwania otwartego połączenia
HOST
komputer hostowy, do którego klient wysłał zapytanie
HTTP_USER_AGENT
tożsamość klienta
REMOTE_ADDR
adres klienta IP
REMOTE_PORT
port komunikacyjny używany przez klienta
SERVER_PROTOCOL
protokół HTTP używany przez serwer
REQUEST_METHOD
metoda zapytania używana przez klienta (GET lub POST)
QUERY_STRING
żądanie ?param1=val1&param2=val2&... umieszczone za żądanym URL (metoda GET)

Nieznacznie modyfikując kod poprzedniego programu, możemy pobrać wyłącznie nagłówki HTTP:

<?php
     // wyświetla zmienne związane z serwerem WWW
   // wysyłamy zwykły tekst
  header("Content-type: text/plain");
   // przeglądanie tablicy asocjacyjnej $_SERVER
     reset($_SERVER);
     while (list($clé,$valeur)=each($_SERVER)){
      // nagłówek HTTP?
    if(strtolower(substr($clé,0,4))=="http")
          echo substr($clé,5)." : $valeur\n";
    }//while
?>

W przeglądarce wyświetla się następujący wynik:

Image

Jeśli chcemy uzyskać konkretny nagłówek HTTP, należy wpisać na przykład $_SERVER["HTTP_ACCEPT"].

3.8. Pobieranie informacji o środowisku

Serwer WWW/PHP działa w środowisku, o którym można uzyskać informacje za pomocą tablicy $_ENV, która przechowuje różne cechy środowiska uruchomieniowego. Rozważmy następującą aplikację env1.php:

<?php
     // wyświetla zmienne powiązane z serwerem WWW
   // wysyłamy zwykły tekst
  header("Content-type: text/plain");
   // przeglądanie tablicy asocjacyjnej $_ENV
     reset($_ENV);
     while (list($clé,$valeur)=each($_ENV)){
      echo "$clé : $valeur\n";
    }//while
?>

W przeglądarce wyświetla się następujący wynik (fragment):

Image

Jak widać na powyższym przykładzie, serwer WWW/PHP działa pod kontrolą systemu Windows OS NT.

3.9. Przykłady

3.9.1. Dynamiczne generowanie formularza – 1

Jako przykład weźmy generowanie formularza zawierającego tylko jeden element sterujący: listę rozwijaną. Zawartość tej listy jest tworzona dynamicznie na podstawie wartości pobranych z tablicy. W rzeczywistości wartości te są często pobierane z bazy danych. Formularz wygląda następująco:

Image

Jeśli w powyższym przykładzie wprowadzimy Envoyer, otrzymamy następującą odpowiedź:

Image

Kod HTML z pierwotnego formularza, po jego wygenerowaniu, wygląda następująco:

<html>
    <head>
      <title>Génération de formulaire</title>
  </head>
  <body>
      <h2>Choisissez un nombre</h2>
    <hr>
    <form name="frmvaleurs" method="post" action="valeurs.php">
        <select name="cmbValeurs" size="1">
          <option>un</option>
<option>deux</option>
<option>trois</option>
<option>quatre</option>
<option>cinq</option>
<option>six</option>
<option>sept</option>
<option>huit</option>
<option>neuf</option>
<option>dix</option>
      </select>
           <input type="submit" value="Envoyer" name="cmdEnvoyer">
       </form>
  </body>
</html>

Aplikacja PHP składa się ze strony głównej valeurs.php, która jest wywoływana zarówno w celu pobrania formularza początkowego (listy wartości), jak i przetworzenia zawartych w nim wartości oraz dostarczenia odpowiedzi (wybranej wartości). Aplikacja generuje dwie różne strony:

  • stronę z formularzem początkowym, która zostanie wygenerowana przez program valeurs-p1.php
  • stronę z odpowiedzią dla użytkownika, która zostanie wygenerowana przez program valeurs-p2.php

Aplikacja valeurs.php wygląda następująco:

<?php

     // tablica wartości
  $valeurs=array("un","deux","trois","quatre","cinq","six","sept","huit","neuf","dix");

     // czy mamy oczekiwane parametry
  $requêteVide=! isset($_POST["cmbValeurs"]);

   // pobieramy wybór użytkownika
  if ($requêteVide){
       // pierwotne zapytanie
    include "valeurs-p1.php";
  }else{
       // odpowiedź na POST 
      $choix=$_POST["cmbValeurs"];
      include "valeurs-p2.php";    
  }
?>

Definiuje on tabelę wartości i wywołuje program valeurs-p1.php w celu wygenerowania formularza początkowego, jeśli zapytanie klienta było puste, lub program valeurs-p2.php w celu wygenerowania odpowiedzi, jeśli zapytanie było prawidłowe. Program valeurs1-php wygląda następująco:

<html>
    <head>
      <title>Génération de formulaire</title>
  </head>
  <body>
      <h2>Choisissez un nombre</h2>
    <hr>
    <form name="frmvaleurs" method="post" action="valeurs.php">
        <select name="cmbValeurs" size="1">
          <?php
            for($i=0;$i<count($valeurs);$i++){
              echo "<option>$valeurs[$i]</option>\n";
          }//dla
        ?>
      </select>
           <input type="submit" value="Envoyer" name="cmdEnvoyer">
       </form>
  </body>
</html>

Lista wartości z listy rozwijanej jest generowana dynamicznie na podstawie tablicy $valeurs przesłanej przez valeurs.php. Program valeurs-p2.php generuje odpowiedź:

<html>
    <head>
      <title>réponse</title>
  </head>
  <body>
      <h2>Vous avez choisi le nombre <?php echo $choix ?></h2>
  </body>
</html>

W tym miejscu wyświetlana jest jedynie wartość zmiennej $choix, przekazanej również przez valeurs.php.

3.9.2. Dynamiczne generowanie formularza – 2

Wracamy do poprzedniego przykładu, modyfikując go w następujący sposób. Proponowany formularz pozostaje ten sam:

Image

Czy odpowiedź jest inna:

Image

W odpowiedzi zwracany jest formularz, a wybrana przez użytkownika liczba jest podana poniżej. Ponadto liczba ta jest tą, która pojawia się jako wybrana na liście wyświetlanej w odpowiedzi.

Kod dla valeurs.php jest następujący:

<?php

     // konfiguracja
  ini_set("register_globals","off");

    // tabela wartości
  $valeurs=array("un","deux","trois","quatre","cinq","six","sept","huit","neuf","dix");

   // pobieramy ewentualny wybór użytkownika
  $choix=$_POST["cmbValeurs"];

   // wyświetla się odpowiedź  
  include "valeurs-p1.php";
?>

Warto zauważyć, że w tym przypadku zadbano o skonfigurowanie PHP tak, aby nie zawierało zmiennych globalnych. Jest to zazwyczaj rozsądny środek ostrożności, ponieważ zmienne globalne stwarzają problemy związane z bezpieczeństwem. Alternatywą jest zainicjowanie wszystkich używanych zmiennych. Spowoduje to „nadpisanie” ewentualnej zmiennej globalnej o tej samej nazwie.

Strona formularza jest wyświetlana przez valeurs-p1.php:

<html>
    <head>
      <title>Génération de formulaire</title>
  </head>
  <body>
      <h2>Choisissez un nombre</h2>
    <hr>
    <form name="frmvaleurs" method="post" action="valeurs.php">
        <select name="cmbValeurs" size="1">
          <?php
            for($i=0;$i<count($valeurs);$i++){
              // jeśli bieżąca opcja jest zgodna z wyborem, wybieramy ją
              if (isset($choix) && $choix==$valeurs[$i])
                  echo "<option selected>$valeurs[$i]</option>\n";
            else echo "<option>$valeurs[$i]</option>\n";
          }//for
        ?>
      </select>
           <input type="submit" value="Envoyer" name="cmdEnvoyer">
       </form>
    <?php
         // kontynuacja strony
      if(isset($choix)){
          echo "<hr>\n";
          echo "<h3>Vous avez choisi le nombre $choix</h3>\n";
      }
    ?>
  </body>
</html>

Program generujący stronę opiera się na zmiennej $choix przekazanej przez program valeurs.php. Warto tutaj zauważyć, że struktura strony HTML zaczyna być poważnie „zanieczyszczona” kodem PHP. Interfejs użytkownika valeurs.php mógłby wykonać więcej pracy, jak pokazuje poniższa nowa wersja:

<?php

     // konfiguracja
  ini_set("register_globals","off");

    // tabela wartości
  $valeurs=array("un","deux","trois","quatre","cinq","six","sept","huit","neuf","dix");

   // pobieramy ewentualny wybór użytkownika
  $choix=$_POST["cmbValeurs"];

   // obliczamy listę wartości do wyświetlenia
  $HTMLvaleurs="";
    for($i=0;$i<count($valeurs);$i++){
        // jeśli bieżąca opcja jest równa wyborowi użytkownika, wybieramy ją
        if (isset($choix) && $choix==$valeurs[$i])
            $HTMLvaleurs.="<option selected>$valeurs[$i]</option>\n";
      else $HTMLvaleurs.="<option>$valeurs[$i]</option>\n";
    }//for

   // obliczamy drugą część strony
  $HTMLpart2="";
     if(isset($choix)){
        $HTMLpart2="<hr>\n";
        $HTMLpart2.="<h3>Vous avez choisi le nombre $choix</h3>\n";
  }//if

   // wyświetla się odpowiedź  
  include "valeurs-p2.php";
?>

Strona została wygenerowana przez program valeurs-p2.php w następujący sposób:

<html>
    <head>
      <title>Génération de formulaire</title>
  </head>
  <body>
      <h2>Choisissez un nombre</h2>
    <hr>
    <form name="frmvaleurs" method="post" action="valeurs.php">
        <select name="cmbValeurs" size="1">
          <?php
             // wyświetlanie listy wartości
          echo $HTMLvaleurs;
        ?>
      </select>
           <input type="submit" value="Envoyer" name="cmdEnvoyer">
       </form>
    <?php
         // wyświetlanie części 2
      echo $HTMLpart2;
    ?>
  </body>
</html>

Kod HTML został teraz w znacznej mierze oczyszczony z kodu PHP. Przypomnijmy jednak cel podziału na program front-end, który analizuje i przetwarza zapytanie klienta, oraz programy odpowiedzialne wyłącznie za wyświetlanie stron skonfigurowanych na podstawie danych przekazanych przez front-end: chodzi o oddzielenie pracy programisty PHP od pracy grafika. Programista PHP pracuje nad interfejsem użytkownika, a grafik nad stronami internetowymi. W naszej nowej wersji grafik nie może już na przykład pracować nad częścią 2 strony, ponieważ nie ma już dostępu do jej kodu HTML. W pierwszej wersji miał taką możliwość. Żadna z tych dwóch metod nie jest więc idealna.

3.9.3. Dynamiczne generowanie formularza – 3

Powracamy do tego samego problemu, co poprzednio, ale tym razem wartości pobierane są z bazy danych. W naszym przykładzie jest to baza o nazwie MySQL:

  • baza nosi nazwę dbValeurs
  • jej właścicielem jest admDbValeurs, a hasło to mdpDbValeurs
  • baza zawiera jedną tabelę o nazwie tvaleurs
  • tabela ta zawiera tylko jedno pole typu całkowitego o nazwie „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)

W aplikacji korzystającej z bazy danych zazwyczaj występują następujące etapy:

  1. Połączenie z SGBD
  2. Wysyłanie zapytań SQL do bazy danych SGBD
  3. Przetwarzanie wyników tych zapytań
  4. Zamknięcie połączenia z SGBD

Kroki 2 i 3 są wykonywane wielokrotnie, a zamknięcie połączenia następuje dopiero po zakończeniu pracy z bazą danych. Jest to schemat stosunkowo typowy dla każdego, kto kiedykolwiek korzystał z bazy danych w trybie interaktywnym. Poniższa tabela zawiera instrukcje PHP dotyczące wykonywania tych różnych operacji za pomocą SGBD i MySQL:

connexion au SGBD
$connexion=mysql_pconnect($hote,$user,$pwd)
$connexion=mysql_connect($hote,$user,$pwd)
$hote: nazwa internetowa komputera, na którym uruchomione są programy SGBD i MySQL. W rzeczywistości możliwe jest korzystanie z programów SGBD i MySQL zdalnie.
$user: nazwa znanego użytkownika programu SGBD MySQL
$pwd: jego hasło
$connexion: utworzone połączenie
mysql_pconnect tworzy trwałe połączenie z SGBD i MySQL. Takie połączenie nie jest zamykane po zakończeniu działania skryptu. Pozostaje otwarte. Dzięki temu, gdy zajdzie potrzeba otwarcia nowego połączenia z SGBD, PHP wyszuka istniejące połączenie należące do tego samego użytkownika. Jeśli je znajdzie, wykorzysta je. Pozwala to zaoszczędzić czas.
mysql_connect tworzy połączenie nietrwałe, które jest zamykane po zakończeniu pracy z SGBD i MySQL.
requêtes SQL
$résultats=mysql_db_query($base,$requête,$connexion)
$base: baza danych MySQL, z którą będziemy pracować
$requête: zapytanie dotyczące SQL (insert, delete, update, select, ...)
$connexion: połączenie z SGBD MySQL
$résultats: wyniki zapytania – różnią się w zależności od tego, czy zapytanie jest zapytaniem typu select, czy operacją aktualizacji (insert, update, delete, ...)
traitement des
 résultats d'un
 select
$résultats=mysql_db_query($base, „select …”, $connexion)
Wynikiem zapytania SELECT jest tabela, a więc zbiór wierszy i kolumn. Dostęp do tej tabeli można uzyskać za pomocą $résultats.
$ligne=mysql_fetch_row($résultats) odczytuje wiersz z tabeli i umieszcza go w $ligne w postaci tablicy. Zatem $ligne[i] stanowi kolumnę i pobranego wiersza. Funkcję mysql_fetch_row można wywoływać wielokrotnie. Za każdym razem odczytuje ona nowy wiersz z tabeli $résultats. Po osiągnięciu końca tabeli funkcja zwraca wartość false. W ten sposób tabela $résultats może być wykorzystywana w następujący sposób:
while($wiersz = mysql_fetch_row($wyniki)) {
// przetwarza bieżący wiersz $wiersz
}//while
traitement des 
résultats d'une 
requête de mise à 
jour
$résultats=mysql_db_query($base, „wstaw ...”, $connexion)
Wartość $résultats jest prawdziwa lub fałszywa w zależności od tego, czy operacja zakończyła się powodzeniem, czy nie. W przypadku powodzenia funkcja mysql_affected_rows pozwala ustalić liczbę wierszy zmodyfikowanych w wyniku operacji aktualizacji.
fermeture de la
 connexion
mysql_close($połączenie)
$connexion: połączenie z SGBD MySQL

Kod interfejsu użytkownika valeurs.php przyjmuje następującą postać:

<?php

     // konfiguracja
  ini_set("register_globals","off");
    ini_set("display_errors","off");
    ini_set("track_errors","on");

    // tabela wartości
  list($erreur,$valeurs)=getValeurs();

   // czy wystąpił błąd?
  if($erreur){
       // wyświetlanie strony błędu
    include "valeurs-err.php";
     // koniec
    return;
  }//if

   // pobieramy ewentualny wybór użytkownika
  $choix=$_POST["cmbValeurs"];

   // obliczamy listę wartości do wyświetlenia
  $HTMLvaleurs="";
    for($i=0;$i<count($valeurs);$i++){
        // jeśli bieżąca opcja jest równa wyborowi, wybieramy ją
        if (isset($choix) && $choix==$valeurs[$i])
            $HTMLvaleurs.="<option selected>$valeurs[$i]</option>\n";
      else $HTMLvaleurs.="<option>$valeurs[$i]</option>\n";
    }//for

   // obliczamy drugą część strony
  $HTMLpart2="";
     if(isset($choix)){
        $HTMLpart2="<hr>\n";
        $HTMLpart2.="<h3>Vous avez choisi le nombre $choix</h3>\n";
  }//if

   // wyświetla się odpowiedź  
  include "valeurs-p1.php";

   // koniec
  return;

  // ------------------------------------------------------------------------
  function getValeurs(){
       // pobiera wartości z bazy danych MySQL
    $user="admDbValeurs";
    $pwd="mdpDbValeurs";
    $db="dbValeurs";
    $hote="localhost";
    $table="tvaleurs";
    $champ="valeur";

     // nawiązanie trwałego połączenia z serwerem MySQL
     // lub w przeciwnym razie nawiązanie zwykłego połączenia
    ($connexion=mysql_pconnect($hote,$user,$pwd)) 
        || ($connexion=mysql_connect($hote,$user,$pwd));
    if(! $connexion)
        return array("Base de données indisponible(".mysql_error()."). Veuillez recommencer ultérieurement.");

     // pobieranie wartości
    $selectValeurs=mysql_db_query($db,"select $champ from $table",$connexion);
    if(! $selectValeurs)
        return array("Base de données indisponible(".mysql_error()."). Veuillez recommencer ultérieurement.");

     // wartości są umieszczane w tablicy
    $valeurs=array();
    while($ligne=mysql_fetch_row($selectValeurs)){
        $valeurs[]=$ligne[0];
    }//while

     // zamknięcie połączenia (jeśli jest trwałe, w rzeczywistości nie zostanie zamknięte)
    mysql_close($connexion);

     // zwrot wyniku
    return array("",$valeurs);
  }//getValeurs

?>

Tym razem wartości, które należy umieścić w polu kombi, nie są dostarczane przez tablicę, lecz przez funkcję getValeurs(). Funkcja ta:

  • otwiera trwałe połączenie (mysql_pconnect) z serwerem mySQL, przekazując zarejestrowaną nazwę użytkownika i hasło.
  • Po nawiązaniu połączenia wysyłane jest zapytanie select w celu pobrania wartości znajdujących się w tabeli tvaleurs z bazy danych dbValeurs.
  • Wynik zapytania select jest umieszczany w tablicy $valeurs, która jest zwracana do programu wywołującego.
  • Funkcja zwraca w rzeczywistości tablicę zawierającą dwa wyniki ($erreur, $valeurs), gdzie pierwszy element to ewentualny komunikat o błędzie lub, w przeciwnym razie, pusty ciąg znaków.
  • Program wywołujący sprawdza, czy wystąpił błąd, a jeśli tak, wyświetla stronę valeurs-err.php. Wygląda ona następująco:
<html>
    <head>
      <title>Erreur</title>
  </head>
  <body>
      <h3>L'erreur suivante s'est produite</h3>
    <font color="red">
        <h4><?php echo $erreur ?></h4>
    </font>
  </body>
</html>
  • jeśli nie wystąpił błąd, program wywołujący ma do dyspozycji wartości w tablicy $valeurs. Wracamy więc do poprzedniego problemu.

Oto dwa przykłady wykonania:

  • z błędem

Image

  • bez błędu

Image

3.9.4. Dynamiczne generowanie formularza – 4

Co by się stało w poprzednim przykładzie, gdybyśmy zmienili SGBD? Na przykład, gdybyśmy przeszli z MySQL na Oracle lub SQL Server? Konieczne byłoby przepisanie funkcji getValeurs(), która dostarcza wartości. Zaletą zebrania kodu niezbędnego do pobrania wartości, które mają być wyświetlone na liście, w jednej funkcji jest to, że kod wymagający modyfikacji jest ściśle określony i nie jest rozproszony po całym programie. Funkcję getValeurs() można przepisać tak, aby była niezależna od używanego modułu SGBD. Wystarczy, aby współpracowała ze sterownikiem ODBC z pakietu SGBD, a nie bezpośrednio z SGBD.

Na rynku dostępnych jest wiele baz danych. Aby ujednolicić dostęp do baz danych w systemie Windows, firma Microsoft opracowała interfejs o nazwie ODBC (Open DataBase Connectivity). Warstwa ta ukrywa specyfikę poszczególnych baz danych pod standardowym interfejsem. W systemie Windows dostępnych jest wiele sterowników ułatwiających dostęp do baz danych. Oto przykładowa lista sterowników zainstalowanych na komputerze z systemem Windows:

Image

Również SGBD i MySQL mają swoje sterowniki ODBC. Aplikacja oparta na sterownikach ODBC może korzystać z dowolnej z powyższych baz danych bez konieczności przepisywania kodu.

Udostępnijmy naszą bazę danych MySQL i dbValeurs za pośrednictwem sterownika ODBC. Poniższa procedura dotyczy systemu Windows 2000. W przypadku systemów Win9x procedura jest bardzo podobna. Uruchamiamy menedżera zasobów ODBC:

Image

Za pomocą przycisku [Add] dodajemy nowe źródło danych ODBC:

Image

Wybieramy sterownik ODBC MySQL, wykonujemy [Terminer], a następnie podajemy parametry źródła danych:

Image

Windows DSN name 
nazwa nadana źródłu danych ODBC (odbc-wartości)
MySQL host 
nazwa komputera, na którym znajduje się SGBD MySQL, zarządzającego źródłem danych (localhost)
MySQL database name 
nazwa bazy danych MySQL, która stanowi źródło danych (dbValeurs)
User 
użytkownik posiadający wystarczające uprawnienia dostępu do bazy danych MySQL, którą ma zarządzać (admDbValeurs)
Password 
swoje hasło (mdpDbValeurs)

PHP może współpracować ze sterownikami ODBC. Poniższa tabela przedstawia funkcje, o których warto wiedzieć:

connexion au SGBD
$connexion = odbc_pconnect($dsn, $user, $pwd)
$connexion=odbc_connect($dsn,$user,$pwd)
$dsn: nazwa DSN (Data Source Name) komputera, na którym uruchomiony jest proces SGBD
$user: nazwa użytkownika znanego programowi SGBD
$pwd: jego hasło
$connexion: utworzone połączenie
odbc_pconnect tworzy trwałe połączenie z SGBD. Takie połączenie nie jest zamykane po zakończeniu działania skryptu. Pozostaje otwarte. Dzięki temu, gdy zajdzie potrzeba otwarcia nowego połączenia z SGBD, PHP wyszuka istniejące połączenie należące do tego samego użytkownika. Jeśli je znajdzie, wykorzysta je. Pozwala to zaoszczędzić czas.
odbc_connect tworzy połączenie nietrwałe, które jest zamykane po zakończeniu pracy z SGBD.
requêtes SQL
$requêtePréparée=odbc_prepare($connexion,$requête)
$requête: zapytanie SQL (wstaw, usuń, zaktualizuj, wybierz, ...)
$connexion: połączenie z SGBD
Analizuje zapytanie $requête i przygotowuje je do wykonania. Tak „przygotowane” zapytanie jest odwołane przez wynik $requêtePréparée. Przygotowanie zapytania do wykonania nie jest obowiązkowe, ale poprawia wydajność, ponieważ analiza zapytania odbywa się tylko raz. Następnie zleca się wykonanie przygotowanego zapytania. Jeśli zleca się wielokrotne wykonanie nieprzygotowanego zapytania, jego analiza jest przeprowadzana za każdym razem, co jest zbędne. Po przygotowaniu zapytanie jest wykonywane przez
$res=odbc_execute($requêtePréparée)
zwraca wartość true lub false w zależności od tego, czy wykonanie zapytania zakończyło się powodzeniem, czy nie
traitement des
 résultats d'un
 select
Wynikiem zapytania SELECT jest tabela, a więc zbiór wierszy i kolumn. Dostęp do tej tabeli można uzyskać za pomocą $requêtePréparée.
$res=odbc_fetch_row($requêtePréparée) odczytuje wiersz z tabeli wynikowej funkcji select. Zwraca wartość true lub false w zależności od tego, czy wykonanie zapytania zakończyło się powodzeniem, czy nie. Elementy pobranego wiersza są dostępne za pośrednictwem funkcji odbc_result:
$val=odbc_result($requêtePréparée,i): kolumna i z właśnie odczytanej linii
$val=odbc_result($requêtePréparée,"nomColonne”): kolumna nomColonne z właśnie odczytanej linii
Funkcję odbc_fetch_row można wywoływać wielokrotnie. Za każdym razem odczytuje ona nowy wiersz z tabeli wyników. Po osiągnięciu końca tabeli funkcja zwraca wartość false. Dzięki temu z tabeli wyników można korzystać w następujący sposób:

while(odbc_fetch_row($résultats)){
    // przetwarza bieżący wiersz za pomocą odbc_result
}//while
fermeture de la 
connexion
odbc_close($połączenie)
$connexion: połączenie z SGBD MySQL

Funkcja getValeurs(), odpowiedzialna za pobieranie wartości z bazy danych ODBC, wygląda następująco:

  // ------------------------------------------------------------------------
  function getValeurs(){
       // pobiera wartości z bazy danych MySQL
    $user="admDbValeurs";
    $pwd="mdpDbValeurs";
    $db="dbValeurs";
    $dsn="odbc-valeurs";
    $table="tvaleurs";
    $champ="valeur";

     // otwarcie trwałego połączenia z serwerem MySQL
     // lub w przeciwnym razie nawiązanie zwykłego połączenia
    ($connexion=odbc_pconnect($dsn,$user,$pwd)) 
        || ($connexion=odbc_connect($dsn,$user,$pwd));
    if(! $connexion)
        return array("1 - Base de données indisponible(".odbc_error()."). Veuillez recommencer ultérieurement.");

     // pobieranie wartości
    $selectValeurs=odbc_prepare($connexion,"select $champ from $table");
    if(! odbc_execute($selectValeurs))
        return array("2 - Base de données indisponible(".odbc_error()."). Veuillez recommencer ultérieurement.");

     // wartości  umieszczane w tablicy
    $valeurs=array();   
    while(odbc_fetch_row($selectValeurs)){
        $valeurs[]=odbc_result($selectValeurs,$champ);
    }//while

     // zamknięcie połączenia (jeśli jest trwałe, w rzeczywistości nie zostanie zamknięte)
    odbc_close($connexion);

     // zwrot wyniku
    return array("",$valeurs);
  }//getValeurs

?>

Jeśli uruchomimy nową aplikację bez aktywacji bazy danych odbc-valeurs, otrzymamy następujący wynik:

Image

Warto zauważyć, że kod błędu zwracany przez sterownik ODBC (odbc_error()=S1000) nie jest zbyt jednoznaczny. Jeśli baza danych odbc-valeurs zostanie udostępniona, otrzymujemy te same wyniki, co poprzednio.

Podsumowując, można stwierdzić, że rozwiązanie to jest korzystne z punktu widzenia konserwacji aplikacji. Jeśli bowiem baza danych ulegnie zmianie, aplikacja nie będzie wymagała modyfikacji. Administrator systemu po prostu utworzy nowe źródło danych o nazwie ODBC dla nowej bazy. Również z myślą o konserwacji dobrze byłoby ustawić parametry dostępu do bazy ($dsn, $user, $pwd) w oddzielnym pliku, który aplikacja ładowałaby przy uruchomieniu (include).

3.9.5. Pobieranie wartości z formularza

Już kilkakrotnie pobieraliśmy wartości z formularza przesłanego przez klienta internetowego. Poniższy przykład przedstawia formularz zawierający najczęściej spotykane komponenty HTML i służy do pobierania parametrów przesłanych przez przeglądarkę klienta. Formularz wygląda następująco:

Image

Formularz jest wstępnie wypełniony. Następnie użytkownik może wprowadzić w nim zmiany:

Image

Jeśli kliknie przycisk [Envoyer], serwer zwróci mu listę wartości z formularza:

Image

Formularz jest statyczną stroną HTML balises.html:

<html>

  <head>
      <title>balises</title>
    <script language="JavaScript">
        function effacer(){
          alert("Vous avez cliqué sur le bouton Effacer");
      }//usunięcie
        </script>
  </head>

  <body background="/images/standard.jpg">
    <form method="POST" action="parameters.php">
      <table border="0">
        <tr>
          <td>Etes-vous marié(e)</td>
          <td>
              <input type="radio" value="oui" name="R1">Oui
              <input type="radio" name="R1" value="non" checked>Non
          </td>
        </tr>
        <tr>
          <td>Cases à cocher</td>
          <td>
              <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
          </td>
        </tr>
        <tr>
          <td>Champ de saisie</td>
          <td>
              <input type="text" name="txtSaisie" size="20" value="qqs mots">
          </td>
        </tr>
        <tr>
          <td>Mot de passe</td>
          <td>
              <input type="password" name="txtMdp" size="20" value="unMotDePasse">
          </td>
        </tr>
        <tr>
          <td>Boîte de saisie</td>
          <td>
               <textarea rows="2" name="areaSaisie" cols="20">
ligne1
ligne2
ligne3
</textarea>
          </td>
        </tr>
        <tr>
          <td>combo</td>
          <td>
              <select size="1" name="cmbValeurs">
                <option>choix1</option>
                <option selected>choix2</option>
                <option>choix3</option>
              </select>
          </td>
        </tr>
        <tr>
          <td>liste à choix simple</td>
          <td>
              <select size="3" name="lst1">
                <option selected>liste1</option>
                <option>liste2</option>
                <option>liste3</option>
                <option>liste4</option>
                <option>liste5</option>
              </select>
          </td>
        </tr>
        <tr>
          <td>liste à choix multiple</td>
          <td>
              <select size="3" name="lst2[]" multiple>
                <option selected>liste1</option>
                <option>liste2</option>
                <option selected>liste3</option>
                <option>liste4</option>
                <option>liste5</option>
              </select>
          </td>
        </tr>
        <tr>
          <td>bouton</td>
          <td>
              <input type="button" value="Effacer" name="cmdEffacer" onclick="effacer()">
          </td>
        </tr>
        <tr>
          <td>envoyer</td>
          <td>
              <input type="submit" value="Envoyer" name="cmdRenvoyer">
          </td>
        </tr>
        <tr>
          <td>rétablir</td>
          <td>
              <input type="reset" value="Rétablir" name="cmdRétablir">
          </td>
        </tr>
      </table>
      <input type="hidden" name="secret" value="uneValeur">
    </form>
  </body>
</html>

Poniższa tabela przedstawia rolę poszczególnych znaczników w tym dokumencie oraz wartość pobieraną przez PHP dla różnych typów elementów formularza. Wartość pola o nazwie HTML C może zostać przesłana przez POST lub GET. W pierwszym przypadku zostanie ona pobrana do zmiennej $_GET["C"], a w drugim przypadku do zmiennej $_POST["C"]. Poniższa tabela zakłada użycie zmiennej POST.

Kontrola
znacznika HTML
wartość pobrana przez PHP
formulaire
<form method="POST" >
 
champ de 
saisie
<input type="text" name="txtSaisie" size="20" value="kilka słów">
$_POST["txtSaisie"]: wartość zawarta w polu txtSaisie formularza
champ de
 saisie cachée
<input type="password" name="txtMdp" size="20" value="unMotDePasse">
$_POST["txtmdp"]: wartość zawarta w polu txtMdp formularza
champ de 
saisie
multilignes
<textarea rows="2" name="areaSaisie" cols="20">
wiersz1
wiersz 2
wiersz 3
</textarea>
$_POST["areaSaisie"]: wiersze zawarte w polu areaSaisie w postaci pojedynczego ciągu znaków: "ligne1\r\nligne2\r\nligne3". Wiersze są oddzielone od siebie sekwencją „\r\n”.
boutons radio
<input type="radio" value="Tak" name="R1">Tak
<input type="radio" name="R1" value="nie" checked>Nie
$_POST["R1"]: wartość (=value) przycisku opcji zaznaczonego jako „tak” lub „nie”, w zależności od sytuacji.
cases à
cocher
<input type="checkbox" name="C1" value="jeden">1
<input type="checkbox" name="C2" value="dwa" checked>2
<input type="checkbox" name="C3" value="trzy">3
$_POST["C1"]: wartość (=value) pola wyboru, jeśli jest zaznaczone; w przeciwnym razie zmienna nie istnieje. Zatem jeśli zaznaczono pole wyboru C1, $_POST["C1"] ma wartość „jeden”, w przeciwnym razie $_POST["C1"] nie istnieje.
Combo
<select size="1" name="cmbValeurs">
<option>opcja1</option>
<option selected>opcja2</option>
<option>opcja3</option>
</select>
$_POST["cmbValeurs"]: opcja wybrana z listy, na przykład „opcja3”.
liste à
sélection
unique
<select size="3" name="lst1">
<option selected>lista1</option>
<option>lista2</option>
<option>lista3</option>
<option>lista4</option>
<option>lista5</option>
</select>
$_POST["lst1"]: opcja wybrana z listy, na przykład „liste5”.
liste à 
sélection
multiple
<select size="3" name="lst2[]" multiple>
<option>lista1</option>
<option>lista2</option>
<option selected>lista3</option>
<option>lista4</option>
<option>lista5</option>
</select>
$_POST["lst2"]: tabela opcji wybranych z listy, na przykład ["liste3,"liste5"]. Należy zwrócić uwagę na szczególną składnię tagu HTML w tym konkretnym przypadku: lst2[].
champ caché
<input type="hidden" name="secret" value="uneValeur">
$_POST["secret"]: wartość (=value) pola, w tym przypadku „uneValeur”.

W naszym przykładzie wartości z formularza są przekazywane do programu parameters.php:

    <form method="POST" action="parameters.php">

Kod tego ostatniego jest następujący:

<?php

   // konfiguracja
  ini_set("register_globals","off");
  ini_set("display_errors","off");

  // metoda wywołania
  $méthode=$_SERVER["REQUEST_METHOD"];

   // pobieranie parametrów
   // zależy to od metody ich wysyłania
  if($méthode=="GET")
      $param=$_GET;
      else $param=$_POST;
  $R1=$param["R1"];
  $C1=$param["C1"];
  $C2=$param["C2"];
  $C3=$param["C3"];
  $txtSaisie=$param["txtSaisie"];    
  $txtMdp=$param["txtMdp"];
  $areaSaisie=implode("<br>",explode("\r\n",$param["areaSaisie"]));
  $cmbValeurs=$param["cmbValeurs"];
  $lst1=$param["lst1"];
  $lst2=implode("<br>",$param["lst2"]);
  $secret=$param["secret"];

     // czy żądanie jest prawidłowe?
    $requêteValide=isset($R1) && (isset($C1) || isset($C2) || isset($C3))
        && isset($txtSaisie) && isset($txtMdp) && isset($areaSaisie)
        && isset($cmbValeurs) && isset($lst1) && isset($lst2) 
        && isset($secret);

   // wyświetlanie strony
    if ($requêteValide)
      include "parameters-p1.php";
    else include "balises.html";                    
?>

Omówmy kilka aspektów tego programu:

  • aplikacja nie zajmuje się sposobem przekazywania wartości z formularza do serwera. W obu możliwych przypadkach (GET i POST) słownik przekazywanych wartości jest określany przez $param.
  if($méthode=="GET")
      $param=$_GET;
      else $param=$_POST;
  • na podstawie zawartości pola areaSaisie „wiersz1\r\nwiersz2\r\n...” tworzy się tablicę ciągów znaków za pomocą funkcji explode("\r\n", $param["areaSaisie"]). Otrzymujemy zatem tablicę [ligne1,ligne2,...]. Na jej podstawie tworzymy ciąg znaków "ligne1<br>ligne2<br>..." za pomocą funkcji implode.
  • Wartość listy wielokrotnego wyboru lst2 jest tablicą, na przykład ["option3","option5"]. Na jej podstawie tworzy się ciąg znaków „option3<br>option5” za pomocą funkcji implode.
  • Aplikacja sprawdza, czy wszystkie parametry zostały ustawione. Należy pamiętać, że dowolna funkcja URL może zostać wywołana ręcznie lub programowo i niekoniecznie są dostępne oczekiwane parametry. Jeśli brakuje parametrów, wyświetlana jest strona balises.html, w przeciwnym razie strona parameters-p1.php. Ta ostatnia wyświetla wartości pobrane i obliczone w funkcji parameters.php w tabeli:
<html>
    <head>
        <title>Récupération des paramètres d'un formulaire</title>
    </head>
    <body>
        <table border="1">
            <tr>
                <td>R1</td>
                <td><?php echo $R1 ?></td>
            </tr>
            <tr>
                <td>C1</td>
                <td><?php echo $C1 ?></td>
            </tr>
            <tr>
                <td>C2</td>
                <td><?php echo $C2 ?></td>
            </tr>
            <tr>
                <td>C3</td>
                <td><?php echo $C3 ?></td>
            </tr>
            <tr>
                <td>txtSaisie</td>
                <td><?php echo $txtSaisie ?></td>
            </tr>
            <tr>
                <td>txtMdp</td>
                <td><?php echo $txtMdp ?></td>
            </tr>
            <tr>
                <td>areaSaisie</td>
                <td><?php echo $areaSaisie ?></td>
            </tr>
            <tr>
                <td>cmbValeurs</td>
                <td><?php echo $cmbValeurs ?></td>
            </tr>
            <tr>
                <td>lst1</td>
                <td><?php echo $lst1 ?></td>
            </tr>
            <tr>
                <td>lst2</td>
                <td><?php echo $lst2 ?></td>
            </tr>
            <tr>
                <td>secret</td>
                <td><?php echo $secret ?></td>
            </tr>
        </table>
    </body>
    </html>    

3.10. Śledzenie sesji

3.10.1. Problem

Aplikacja internetowa może składać się z kilku wymian formularzy między serwerem a klientem. Działa to w następujący sposób:

krok 1

  1. klient C1 nawiązuje połączenie z serwerem i wysyła swoje pierwsze żądanie.
  2. serwer wysyła formularz F1 do klienta C1 i zamyka połączenie nawiązane w kroku 1.

Krok 2

  1. Klient C1 wypełnia formularz i odsyła go do serwera. W tym celu przeglądarka nawiązuje nowe połączenie z serwerem.
  2. Serwer przetwarza dane z formularza 1, oblicza na ich podstawie informacje I1, wysyła formularz F2 do klienta C1 i zamyka połączenie otwarte w kroku 3.

Krok 3

  1. Cykl etapów 3 i 4 powtarza się w etapach 5 i 6. Po zakończeniu etapu 6 serwer otrzyma dwa formularze F1 i F2 i na ich podstawie obliczy informacje I1 oraz I2.

Pojawia się następujący problem: w jaki sposób serwer zachowuje informacje I1 i I2 powiązane z klientem C1? Problem ten nazywamy śledzeniem sesji klienta C1. Aby zrozumieć jego źródło, przyjrzyjmy się schematowi aplikacji serwerowej TCP-IP obsługującej jednocześnie wielu klientów:

W klasycznej aplikacji klient-serwer TCP-IP:

  • klient nawiązuje połączenie z serwerem
  • przez to połączenie wymienia dane z serwerem
  • połączenie zostaje zamknięte przez jedną ze stron

Dwa istotne elementy tego mechanizmu to:

  1. dla każdego klienta tworzone jest jedno połączenie
  2. połączenie to jest wykorzystywane przez cały czas trwania komunikacji serwera z klientem

To, co pozwala serwerowi w danym momencie rozpoznać, z którym klientem współpracuje, to połączenie, czyli inaczej mówiąc „kanał”, który łączy go z klientem. Ponieważ kanał ten jest przypisany do konkretnego klienta, wszystko, co do niego dociera, pochodzi od tego klienta, a wszystko, co jest wysyłane tym kanałem, dociera do tego samego klienta.

Mechanizm klient-serwer HTTP działa zgodnie z powyższym schematem, z tą jednak różnicą, że komunikacja klient-serwer ogranicza się do pojedynczej wymiany danych między klientem a serwerem:

  • klient nawiązuje połączenie z serwerem i wysyła żądanie
  • serwer udziela odpowiedzi i zamyka połączenie

Jeśli w momencie T1 klient C wysyła żądanie do serwera, otrzymuje połączenie C1, które posłuży do jednorazowej wymiany żądania i odpowiedzi. Jeśli w momencie T2 ten sam klient wyśle drugie żądanie do serwera, otrzyma połączenie C2, które różni się od połączenia C1. Z punktu widzenia serwera nie ma zatem żadnej różnicy między tym drugim żądaniem użytkownika C a jego żądaniem początkowym: w obu przypadkach serwer traktuje klienta jako nowego klienta. Aby istniał związek między różnymi połączeniami klienta C z serwerem, klient C musi zostać „rozpoznany” przez serwer jako „stały użytkownik”, a serwer musi pobrać informacje, które posiada na temat tego stałego użytkownika.

Wyobraźmy sobie system zarządzania, który działałby w następujący sposób:

  • Istnieje jedna kolejka
  • Istnieje kilka okienek. Dzięki temu kilku klientów może być obsługiwanych jednocześnie. Gdy okienko się zwalnia, klient opuszcza kolejkę, aby zostać obsłużonym przy tym okienku
  • Jeśli klient pojawia się po raz pierwszy, osoba przy okienku wręcza mu żeton z numerem. Klient może zadać tylko jedno pytanie. Po uzyskaniu odpowiedzi musi opuścić okienko i przejść na koniec kolejki. Pracownik okienka zapisuje informacje o tym kliencie w teczce oznaczonej jego numerem.
  • Kiedy znów nadejdzie jego kolej, klient może zostać obsłużony przez innego pracownika niż poprzednio. Ten prosi go o żeton i pobiera kartotekę o numerze odpowiadającym numerowi żetonu. Klient ponownie zgłasza prośbę, otrzymuje odpowiedź, a informacje są dodawane do jego kartoteki.
  • i tak dalej... Z biegiem czasu klient otrzyma odpowiedzi na wszystkie swoje zapytania. Śledzenie kolejnych zapytań odbywa się za pomocą żetonu i powiązanej z nim teczki.

Mechanizm śledzenia sesji w aplikacji internetowej typu klient-serwer działa analogicznie do powyższego przykładu:

  • przy pierwszym żądaniu klient otrzymuje token od serwera internetowego
  • będzie on przedstawiał ten token przy każdym kolejnym żądaniu w celu identyfikacji

Token może przybierać różne formy:

  • pola ukrytego w formularzu
    • klient wysyła pierwsze żądanie (serwer rozpoznaje go po tym, że klient nie posiada tokenu)
    • serwer generuje odpowiedź (formularz) i umieszcza token w ukrytym polu tego formularza. W tym momencie połączenie zostaje zamknięte (klient opuszcza okienko z tokenem). Serwer zadbał ewentualnie o powiązanie informacji z tym tokenem.
    • Klient wysyła drugie żądanie, odsyłając formularz. Serwer pobiera z niego token. Dzięki tokenowi może wówczas przetworzyć drugie żądanie klienta, mając dostęp do informacji obliczonych podczas pierwszego żądania. Do pliku powiązanego z tokenem dodawane są nowe informacje, do klienta wysyłana jest druga odpowiedź, a połączenie zostaje zamknięte po raz drugi. Token został ponownie umieszczony w formularzu odpowiedzi, aby użytkownik mógł go przedstawić podczas kolejnego żądania.
    • i tak dalej...

Główną wadą tej techniki jest to, że token musi zostać umieszczony w formularzu. Jeśli odpowiedź serwera nie jest formularzem, metoda ukrytego pola nie ma już zastosowania.

  • metoda pliku cookie
    • klient wysyła pierwsze żądanie (serwer rozpoznaje go po tym, że klient nie posiada tokenu)
    • serwer wysyła odpowiedź, dodając plik cookie do nagłówków HTTP tej odpowiedzi. Odbywa się to za pomocą polecenia HTTP Set-Cookie:

Set-Cookie: param1=wartość1;param2=wartość2;....

gdzie parami to nazwy parametrów, a valeursi to ich wartości. Wśród parametrów znajdzie się token. Bardzo często w pliku cookie znajduje się wyłącznie token, a pozostałe informacje są zapisywane przez serwer w folderze powiązanym z tokenem. Przeglądarka, która otrzymuje plik cookie, zapisuje go w pliku na dysku. Po otrzymaniu odpowiedzi od serwera połączenie zostaje zamknięte (klient opuszcza okno z tokenem).

  • (ciąg dalszy)
    • klient wysyła drugie żądanie do serwera. Za każdym razem, gdy wysyłane jest żądanie do serwera, przeglądarka sprawdza wśród wszystkich posiadanych plików cookie, czy znajduje się wśród nich plik pochodzący z serwera, do którego skierowano żądanie. Jeśli tak, wysyła je do serwera zawsze w postaci polecenia HTTP, czyli polecenia „Cookie”, którego składnia jest analogiczna do składni polecenia Set-Cookie używanego przez serwer:

Cookie: param1=wartość1;param2=wartość2;....

Wśród nagłówków HTTP wysyłanych przez przeglądarkę serwer odnajdzie token, który pozwoli mu rozpoznać klienta i odzyskać powiązane z nim informacje.

Jest to najczęściej stosowana forma tokenu. Ma ona jednak wadę: użytkownik może skonfigurować swoją przeglądarkę tak, aby nie akceptowała plików cookie. Taki użytkownik nie ma wówczas dostępu do aplikacji internetowych wykorzystujących pliki cookie.

  • przeróbka URL
    • klient wysyła pierwsze żądanie (serwer rozpoznaje go po tym, że klient nie posiada tokenu)
    • serwer wysyła odpowiedź. Zawiera ona linki, z których użytkownik musi skorzystać, aby kontynuować korzystanie z aplikacji. W adresie URL każdego z tych linków serwer dodaje token w postaci URL;token=wartość.
    • gdy użytkownik kliknie jeden z linków, aby kontynuować korzystanie z aplikacji, przeglądarka wysyła żądanie do serwera WWW, umieszczając w nagłówkach HTTP żądany token w postaci URL URL;token=wartość. Serwer jest wówczas w stanie odzyskać token.

3.10.2. API i PHP do śledzenia sesji

Poniżej przedstawiamy główne metody przydatne do śledzenia sesji:

session_start()
rozpoczyna sesję, do której należy bieżące żądanie. Jeśli żądanie to nie należało jeszcze do żadnej sesji, sesja ta zostaje utworzona.
session_id()
identyfikator bieżącej sesji
$_SESSION[$var]
słownik przechowujący dane sesji. Dostępny do odczytu i zapisu
session_destroy()
usuwa dane zawarte w bieżącej sesji. Dane te pozostają dostępne dla bieżącej wymiany danych między klientem a serwerem, ale nie będą dostępne podczas następnej wymiany.

3.10.3. Przykład 1

Przedstawiamy przykład zaczerpnięty z książki „Programowanie z wykorzystaniem J2EE”, wydanej przez wydawnictwo Wrox i dystrybuowanej przez Eyrolles. Przykład ten pozwala zapoznać się z działaniem sesji PHP. Strona główna wygląda następująco:

Image

Znajdują się na niej następujące elementy:

  • identyfikator sesji ID uzyskany za pomocą funkcji session_id(). Ten identyfikator ID, wygenerowany przez przeglądarkę, jest wysyłany do klienta za pomocą pliku cookie, który przeglądarka odsyła, gdy żąda identyfikatora URL z tej samej struktury drzewa. To właśnie pozwala na utrzymanie sesji.
  • licznik, który jest zwiększany wraz z kolejnymi żądaniami przeglądarki i który wskazuje, że sesja jest prawidłowo utrzymywana.
  • link umożliwiający usunięcie danych powiązanych z bieżącą sesją. Odbywa się to za pomocą funkcji session_destroy()
  • link do ponownego załadowania strony

Kod aplikacji cycledevie.php wygląda następująco:

<?php
     //cycledevie.php

     // konfiguracja
  ini_set("register_globals","off");
  ini_set("display_errors","off");

  // rozpoczynamy sesję
  session_start();

   // czy należy ją unieważnić?
  $action=$_GET["action"];
  if($action=="invalider"){
       // koniec sesji
    session_destroy();
  }//if

     // zarządzanie licznikiem
  if(! isset($_SESSION["compteur"]))
         // licznik nie istnieje – tworzymy go
      $_SESSION["compteur"]=0;
         // licznik istnieje – zwiększamy jego wartość
    else $_SESSION["compteur"]++;

   // pobieramy ID z bieżącej sesji
  $idSession=session_id();

   // pobieramy licznik
  $compteur=$_SESSION["compteur"];

   // przekazujemy kontrolę do strony wyświetlania
  include "cycledevie-p1.php";
?>

Należy zwrócić uwagę na następujące kwestie:

  • od momentu uruchomienia aplikacji rozpoczyna się sesja. Jeśli klient przesłał token sesji, wznawiana jest sesja o tym identyfikatorze, a wszystkie powiązane z nią dane są umieszczane w słowniku $_SESSION. W przeciwnym razie tworzony jest nowy token sesji.
  • jeśli klient przesłał parametr action o wartości „invalider”, dane sesji są oznaczane jako „do usunięcia” na potrzeby następnej wymiany. W przeciwieństwie do normalnej wymiany, nie zostaną one zapisane na serwerze po zakończeniu wymiany.
  • Z słownika $_SESSION pobierany jest licznik powiązany z sesją, a także identyfikator sesji ID (session_id()).
  • Strona, która ma zostać wysłana do klienta, jest generowana przez program cycledevie-p1.php

Strona cycledevie-p1.php wyświetla stronę wysłaną do klienta:

<html>
    <head>
      <title>Gestion de sessions</title>
  </head>
  <body>
      <h3>Cycle de vie d'une session PHP</h3>
    <hr>
    <br>ID session : <?php echo $idSession ?>
    <br>compteur : <?php echo $compteur ?>    
    <br><a href="cycledevie.php?action=invalider">Invalider la session</a>
    <br><a href="cycledevie.php">Recharger la page</a>
  </body>
</html>

Warto zwrócić uwagę na kod URL dołączony do każdego z tych dwóch linków:

  1. cycledevie.php służy do ponownego załadowania strony
  2. cycledevie.php?action=invalider – w celu unieważnienia sesji. W tym przypadku do URL dołączony jest parametr action=invalider. Zostanie on pobrany przez program serwerowy cycledevie.php za pomocą instrukcji $action=$_GET["action"].

Odświeżmy stronę dwa razy z rzędu:

Image

Licznik został prawidłowo zwiększony. Identyfikator sesji ID nie uległ zmianie. Teraz unieważnijmy sesję:

Image

Widać, że utraciliśmy identyfikator sesji ID, ale licznik został ponownie zwiększony. Odświeżmy stronę:

Image

Widać, że zaczynamy od nowa z tym samym identyfikatorem sesji ID, co poprzednio. Licznik natomiast wraca do zera. Funkcja session_destroy() nie ma zatem natychmiastowego efektu. Dane bieżącej sesji są usuwane jedynie na potrzeby wymiany danych między klientem a serwerem następującej po tej, w której dokonano usunięcia. Identyfikator sesji ID nie uległ zmianie, co sugeruje, że funkcja session_destroy() nie uruchamia nowej sesji poprzez utworzenie nowego identyfikatora ID. Plik cookie z tokenem sesji został odesłany przez przeglądarkę klienta, a funkcja PHP odzyskała sesję na podstawie tego tokenu – sesję, która nie zawierała już żadnych danych.

Poprzednie testy przeprowadzono przy użyciu przeglądarki Netscape skonfigurowanej do korzystania z plików cookie. Skonfigurujmy ją teraz tak, aby z nich nie korzystała. Oznacza to, że nie będzie ona ani przechowywać, ani odsyłać plików cookie wysyłanych przez serwer. Można się zatem spodziewać, że sesje przestaną działać. Spróbujmy przeprowadzić pierwszą wymianę danych:

Image

Otrzymaliśmy identyfikator sesji ID, wygenerowany przez funkcję session_start(). Odświeżmy stronę, korzystając z linku:

Image

Co zaskakujące, powyższe wyniki pokazują, że sesja jest aktywna, a licznik działa poprawnie. Jak to możliwe, skoro pliki cookie zostały wyłączone i nie ma już wymiany tokenów między serwerem a przeglądarką? Odpowiedź daje nam identyfikator URL z powyższego zrzutu ekranu:

http://localhost/poly/sessions/1/cycledevie.php?PHPSESSID=587ce26f943a288d8f41212e30fed13c

To jest URL z linku „Odśwież stronę”. Sprawdźmy kod źródłowy strony wyświetlanej przez przeglądarkę:

<a href="cycledevie.php?action=invalider&PHPSESSID=587ce26f943a288d8f41212e30fed13c">Invalider la session</a>
<a href="cycledevie.php?PHPSESSID=587ce26f943a288d8f41212e30fed13c">Recharger la page</a>

Przypomnijmy, że początkowy kod obu linków na stronie cycledevie-p1.php wygląda następująco:

    <a href="cycledevie.php?action=invalider">Invalider la session</a>
    <a href="cycledevie.php">Recharger la page</a>

Interpreter PHP samodzielnie przepisał więc kody URL obu linków, dodając do nich token sesji. Dzięki temu token ten jest prawidłowo przekazywany przez przeglądarkę po kliknięciu linków. To wyjaśnia, dlaczego nawet przy wyłączonych plikach cookie sesja jest nadal prawidłowo zarządzana.

3.10.4. Przykład 3

Zamierzamy napisać aplikację PHP, która będzie klientem poprzedniej aplikacji compteur. Będzie ona wywoływać ją N razy z rzędu, gdzie N będzie przekazywane jako parametr. Naszym celem jest zaprezentowanie zaprogramowanego klienta internetowego oraz sposobu zarządzania tokenem sesji. Naszym punktem wyjścia będzie ogólny klient internetowy wywoływany w następujący sposób:

clientweb URL GET/HEAD

  • URL: żądany adres URL
  • GET/HEAD: GET, aby uzyskać kod HTML ze strony, HEAD, aby ograniczyć się wyłącznie do nagłówków HTTP

Oto przykład z kodem URL http://localhost/poly/sessions/2/cycledevie.php. Jest to ten sam program, co opisany wcześniej, z niewielką różnicą:

   // ustalamy ścieżkę pliku cookie
    session_set_cookie_params(0,"/poly/sessions/2");
  // rozpoczynamy sesję
  session_start();

Funkcja session_set_cookie_params pozwala ustawić pewne parametry pliku cookie, który będzie zawierał token sesji. Pierwszym parametrem jest czas życia pliku cookie. Czas życia równy zero oznacza, że plik cookie zostanie usunięty przez przeglądarkę, która go otrzymała, po jej zamknięciu. Drugim parametrem jest ścieżka do serwera URL, na który przeglądarka ma odesłać plik cookie. W powyższym przykładzie, jeśli przeglądarka otrzymała plik cookie z serwera o nazwie localhost, odeśle ten plik cookie do dowolnego serwera o nazwie URL znajdującego się w drzewie serwerów http://localhost/poly/sessions/2/.

dos>e:\php43\php.exe clientweb.php http://localhost/poly/sessions/2/cycledevie.php GET
HTTP/1.1 200 OK
Date: Wed, 09 Oct 2002 13:58:16 GMT
Server: Apache/1.3.24 (Win32)
Set-Cookie: PHPSESSID=48d5aaa0e99850b17c33a6e22d38e5c4; path=/poly/sessions/2
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Transfer-Encoding: chunked
Content-Type: text/html

<html>
        <head>
        <title>Gestion de sessions</title>
  </head>
  <body>
        <h3>Cycle de vie d'une session PHP</h3>
    <hr>
    <br>ID session : 48d5aaa0e99850b17c33a6e22d38e5c4    <br>compteur : 0
    <br><a href="cycledevie.php?action=invalider&PHPSESSID=48d5aaa0e99850b17c33a6e22d38e5c4">Invalid
er la session</a>
    <br><a href="cycledevie.php?PHPSESSID=48d5aaa0e99850b17c33a6e22d38e5c4">Recharger la page</a>
  </body>
</html>

Program clientweb wyświetla wszystko, co otrzymuje z serwera. Powyżej widoczne jest polecenie HTTP Set-cookie, za pomocą którego serwer wysyła plik cookie do swojego klienta. W tym przypadku plik cookie zawiera dwie informacje:

  • PHPSESSID, czyli token sesji
  • path, który określa adres URL, do którego należy plik cookie. path=/poly/sessions/2 informuje przeglądarkę, że będzie musiała odesłać plik cookie do serwera za każdym razem, gdy zażąda adresu URL zaczynającego się od /poly/sessions/2 z komputera, który wysłał jej ten plik cookie.
  • Plik cookie może również określać okres ważności. W tym przypadku informacja ta nie została podana. Plik cookie zostanie zatem usunięty po zamknięciu przeglądarki. Plik cookie może mieć na przykład okres ważności wynoszący N dni. Dopóki plik jest ważny, przeglądarka będzie go odsyłać za każdym razem, gdy zostanie wywołana jedna z wartości URL z jego domeny (Path). Weźmy na przykład witrynę sklepu internetowego o nazwie CD. Witryna ta może śledzić ścieżkę nawigacji klienta w swoim katalogu i stopniowo określać jego preferencje: na przykład muzykę klasyczną. Preferencje te mogą zostać zapisane w pliku cookie o okresie ważności wynoszącym 3 miesiące. Jeśli ten sam klient powróci na stronę po upływie miesiąca, przeglądarka prześle plik cookie do aplikacji serwerowej. Aplikacja ta, na podstawie informacji zawartych w pliku cookie, będzie mogła dostosować generowane strony do preferencji klienta.

Poniżej znajduje się kod klienta internetowego.

<?php

     // konfiguracja
    dl("php_curl.dll");    // biblioteka CURL

     // składnia: $0 URL GET
   // wymagane są trzy argumenty
  if($argc != 3){
       // komunikat o błędzie
    fputs(STDERR,"Syntaxe : $argv[0] URL GET/HEAD");
    // zatrzymanie
    exit(1);
  }//if

   // trzeci argument musi mieć postać GET lub HEAD
  $header=strtolower($argv[2]);
  if($header!="get" && $header!="head"){
       // komunikat o błędzie
    fputs(STDERR,"Syntaxe : $argv[0] URL GET/HEAD");
     // zatrzymanie
    exit(2);
  }//if

   // pierwszym argumentem jest URL
  $URL=strtolower($argv[1]);

  // przygotowywanie połączenia  
  $connexion=curl_init($URL);
   // konfiguracja połączenia
  curl_setopt($connexion,CURLOPT_HEADER,1);  
  if($header=="head") curl_setopt($connexion,CURLOPT_NOBODY,1);
  // realizacja połączenia
  curl_exec($connexion);
   // zamknięcie połączenia
  curl_close($connexion);
   // koniec
  exit(0);  
?>

Poprzedni program korzysta z biblioteki CURL:

$connexion=curl_init($URL)
inicjuje obiekt CURL z docelowym obiektem URL
curl_setopt ($connexion,option,valeur)
ustawia wartości niektórych opcji połączenia. Oto dwie użyte w programie:
CURLOPT_HEADER=1: umożliwia uzyskanie nagłówków HTTP wysyłanych przez serwer
CURLOPT_NOBODY=1: pozwala zignorować dokument wysyłany przez serwer za nagłówkami HTTP
curl_exec($connexion)
nawiązuje połączenie z $URL z żądanymi opcjami. Wyświetla na ekranie wszystko, co wysyła serwer
curl_close($connexion)
zamyka połączenie

Poprzedni program jest dość prosty. Jednak biblioteka CURL nie pozwala na precyzyjną obsługę odpowiedzi serwera, na przykład na analizowanie jej wiersz po wierszu. Poniższy program wykonuje to samo co poprzedni, ale z wykorzystaniem podstawowych funkcji sieciowych biblioteki PHP. Będzie on punktem wyjścia do napisania klienta dla naszej aplikacji cycledevie.php.

<?php

     // składnia: $0 URL GET/HEAD
  // wymagane są trzy argumenty
  if($argc != 3){
       // komunikat o błędzie
    fputs(STDERR,"Syntaxe : $argv[0] URL GET/HEAD");
    // zatrzymanie
    exit(1);
  }//if

   // połączenie i wyświetlenie wyniku
  $résultats=getURL($argv[1],$argv[2]);
  if(isset($résultats->erreur)){
       // błąd
    echo "L'erreur suivante s'est produite : $résultats->erreur\n";
  }else{
       // wyświetlenie odpowiedzi serwera
    echo $résultats->réponse;
  }//if  

   // koniec
  exit(0);

  //-----------------------------------------------------------------------
  function getURL($URL,$header){

      // łączy się z $URL
     // tworzy GET lub HEAD w zależności od wartości nagłówka
     // odpowiedź serwera stanowi wynik funkcji

     // analiza URL
    $url=parse_url($URL);
     // protokół
    if(strtolower($url["scheme"])!="http"){
        $résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
      return $résultats;
    }//if
     // urządzenie
    $hote=$url["host"];
    if(! isset($hote)){
        $résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
      return $résultats;
    }//if
     // port
    $port=$url["port"];
    if(! isset($port)) $port=80;
     // ścieżka
    $chemin=$url["path"];
     // żądanie
    if(isset($url["query"])){
        $résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
      return $résultats;
    }//if

     // analiza $header
    $header=strtoupper($header);
      if($header!="GET" && $header!="HEAD"){
          // komunikat o błędzie
        $résultats->erreur="méthode [$header] doit être GET ou HEAD";
         // zatrzymanie
        return $résultats;
      }//if

     // nawiązanie połączenia na porcie $port z $hote
    $connexion=fsockopen($hote,$port,&$errno,&$erreur);
     // powrót w przypadku błędu
    if(! $connexion){
      $résultats->erreur="Echec de la connexion au site ($hote,$port) : $erreur";
      return $résultats;
    }//if

     // $connexion reprezentuje dwukierunkowy strumień komunikacji
     // między klientem (tym programem) a serwerem WWW, z którym nawiązano połączenie
     // kanał ten służy do wymiany poleceń i informacji
     // protokołem komunikacji jest HTTP

     // klient wysyła polecenie GET, aby zażądać pliku URL /
    // składnia polecenia GET: URL HTTP/1.0
    // nagłówki (headers) protokołu HTTP muszą kończyć się pustym wierszem
    fputs($connexion, "$header $chemin HTTP/1.0\n\n");

     // serwer odpowie teraz na kanale $connexion. Wyśle wszystkie
     // te dane, a następnie zamknie kanał. Klient odczytuje zatem wszystko, co nadchodzi z $connexion
     // aż do zamknięcia kanału
    $résultats->réponse="";
    while($ligne=fgets($connexion,10000))
      $résultats->réponse.=$ligne;

     // klient z kolei zamyka połączenie
    fclose($connexion);
     // powrót
    return $résultats;
  }//getURL

?>

Omówmy kilka aspektów tego programu:

  • program przyjmuje dwa parametry:
    • adres HTTP w formacie URL, którego zawartość ma zostać wyświetlona na ekranie.
    • metodę GET lub HEAD, którą należy zastosować w zależności od tego, czy chcemy wyświetlić tylko nagłówki HTTP (HEAD), czy też również treść dokumentu powiązaną z URL (GET).
  • Oba parametry są przekazywane do funkcji getURL. Funkcja ta zwraca obiekt $résultats. Obiekt ten zawiera pole erreur w przypadku wystąpienia błędu, a w przeciwnym razie pole réponse. Pole erreur służy do przechowywania ewentualnego komunikatu o błędzie. Pole réponse zawiera odpowiedź serwera WWW, z którym nawiązano połączenie.
  • Funkcja getURL analizuje URL i $URL za pomocą funkcji parse_url. Instrukcja $url=parse_url($URL) utworzy tablicę asocjacyjną $url z następującymi ewentualnymi kluczami:
    • scheme: protokół URL (http, ftp, ...)
    • host: komputer, na którym znajduje się URL
    • port: port URL
    • path: ścieżka do serwera URL
    • querystring: parametry URL

Adres URL będzie poprawny, jeśli ma postać http://machine[:port][/chemin].

  • Sprawdzany jest również parametr $header
  • po sprawdzeniu i potwierdzeniu poprawności parametrów tworzone jest połączenie TCP na serwerze ($hote, $port), następnie wysyłane jest polecenie HTTP, GET lub HEAD, w zależności od parametru $header.
  • następnie odczytuje się odpowiedź serwera i umieszcza ją w zmiennej $résultats->odpowiedź.

Wykonanie programu daje następujące wyniki:

dos>"e:\php43\php.exe" geturl.php http://localhost/poly/sessions/2/cycledevie.php get

HTTP/1.1 200 OK
Date: Wed, 09 Oct 2002 14:56:55 GMT
Server: Apache/1.3.24 (Win32)
Set-Cookie: PHPSESSID=ea0d2673811ed069e7289d86933a4c0a; path=/poly/sessions/2
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Connection: close
Content-Type: text/html

<html>
        <head>
        <title>Gestion de sessions</title>
  </head>
  <body>
        <h3>Cycle de vie d'une session PHP</h3>
    <hr>
    <br>ID session : ea0d2673811ed069e7289d86933a4c0a    <br>compteur : 0
    <br><a href="cycledevie.php?action=invalider&PHPSESSID=ea0d2673811ed069e7289d86933a4c0a">Invalid
er la session</a>
    <br><a href="cycledevie.php?PHPSESSID=ea0d2673811ed069e7289d86933a4c0a">Recharger la page</a>
  </body>
</html>

Uważny czytelnik zauważył zapewne, że odpowiedź serwera różni się w zależności od używanego programu klienckiego. W pierwszym przypadku serwer wysłał nagłówek HTTP: Transfer-Encoding: chunked, który nie został wysłany w drugim przypadku. Wynika to z faktu, że drugi klient wysłał nagłówek HTTP: get URL HTTP/1.0, który żąda URL i wskazuje, że korzysta z protokołu HTTP w wersji 1.0, co zobowiązuje serwer do udzielenia odpowiedzi przy użyciu tego samego protokołu. Natomiast nagłówek HTTP „Transfer-Encoding: chunked” należy do protokołu HTTP w wersji 1.1. Dlatego serwer nie wykorzystał go w swojej odpowiedzi. To pokazuje nam, że pierwszy klient wysłał swoje żądanie, wskazując, że korzysta z protokołu HTTP w wersji 1.1.

Teraz tworzymy program o nazwie clientCompteur, wywoływany w następujący sposób:

clientCompteur URL N [JSESSIONID]

  • URL: adres URL aplikacji cycledevie
  • N: liczba wywołań tej aplikacji
  • PHPSESSID: parametr opcjonalny – token sesji

Celem programu jest wywołanie aplikacji cycledevie.php N razy, zarządzając plikiem cookie sesji i wyświetlając za każdym razem wartość licznika zwróconą przez serwer. Po zakończeniu N wywołań wartość licznika musi wynosić N-1. Oto pierwszy przykład działania:

dos>"e:\php43\php.exe" clientCompteur2.php http://localhost/poly/sessions/2/cycledevie.php 3
--> GET /poly/sessions/2/cycledevie.php HTTP/1.1
--> Host: localhost:80
--> Connection: close
-->
<-- HTTP/1.1 200 OK
<-- Date: Thu, 10 Oct 2002 06:27:48 GMT
<-- Server: Apache/1.3.24 (Win32)
<-- Set-Cookie: PHPSESSID=2425e00d1d65c2bdcbafc1ce6244f7ea; path=/poly/sessions/2
<-- Expires: Thu, 19 Nov 1981 08:52:00 GMT
<-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
<-- Pragma: no-cache
<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html
<--

[Le compteur est égal à 0]

--> GET /poly/sessions/2/cycledevie.php HTTP/1.1
--> Host: localhost:80
--> Connection: close
--> Cookie: PHPSESSID=2425e00d1d65c2bdcbafc1ce6244f7ea
-->
<-- HTTP/1.1 200 OK
<-- Date: Thu, 10 Oct 2002 06:27:48 GMT
<-- Server: Apache/1.3.24 (Win32)
<-- Expires: Thu, 19 Nov 1981 08:52:00 GMT
<-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
<-- Pragma: no-cache
<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html
<--

[Le compteur est égal à 1]

--> GET /poly/sessions/2/cycledevie.php HTTP/1.1
--> Host: localhost:80
--> Connection: close
--> Cookie: PHPSESSID=2425e00d1d65c2bdcbafc1ce6244f7ea
-->
<-- HTTP/1.1 200 OK
<-- Date: Thu, 10 Oct 2002 06:27:48 GMT
<-- Server: Apache/1.3.24 (Win32)
<-- Expires: Thu, 19 Nov 1981 08:52:00 GMT
<-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
<-- Pragma: no-cache
<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html
<--

[Le compteur est égal à 2]

Program wyświetla:

  • nagłówki HTTP, które wysyła do serwera w postaci --> entêteEnvoyé
  • nagłówki HTTP, które otrzymuje w postaci <-- entêteReçu
  • wartość licznika po każdym wywołaniu

Widać, że podczas pierwszego wywołania:

  • klient nie wysyła pliku cookie
  • serwer wysyła plik cookie (Set-Cookie:)

W przypadku kolejnych wywołań:

  • klient zawsze odsyła plik cookie, który otrzymał od serwera podczas pierwszego wywołania. To właśnie pozwala serwerowi rozpoznać go i zwiększyć wartość licznika.
  • serwer natomiast nie wysyła już pliku cookie

Uruchamiamy ponownie poprzedni program, przekazując powyższy token jako trzeci parametr:

dos>"e:\php43\php.exe" clientCompteur2.php http://localhost/poly/sessions/2/cycledevie.php 1 2425e00d1d65c2bdcbafc1ce6244f7ea

--> GET /poly/sessions/2/cycledevie.php HTTP/1.1
--> Host: localhost:80
--> Connection: close
--> Cookie: PHPSESSID=2425e00d1d65c2bdcbafc1ce6244f7ea
-->
<-- HTTP/1.1 200 OK
<-- Date: Thu, 10 Oct 2002 06:32:03 GMT
<-- Server: Apache/1.3.24 (Win32)
<-- Expires: Thu, 19 Nov 1981 08:52:00 GMT
<-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
<-- Pragma: no-cache
<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html
<--

[Le compteur est égal à 3]

Widać tutaj, że już przy pierwszym wywołaniu ze strony klienta serwer otrzymuje ważny plik cookie sesji. Być może wskazuje to na potencjalną lukę w zabezpieczeniach. Jeśli uda mi się przechwycić w sieci token sesji, będę w stanie podszyć się pod osobę, która ją zainicjowała. W naszym przykładzie pierwsze wywołanie (bez tokenu sesji) reprezentuje osobę inicjującą sesję (być może za pomocą loginu i hasła, które uprawniają ją do otrzymania tokenu), a drugie wywołanie (z tokenem sesji) reprezentuje osobę, która „przejęła” token sesji z pierwszego wywołania. Jeśli bieżąca operacja dotyczy bankowości, może to stać się kłopotliwe...

Kod klienta wygląda następująco:

<?php

     // składnia: $0 URL N [PHPSESSID]
   // potrzebne są trzy argumenty
  if($argc!=3 && $argc!=4){
       // komunikat o błędzie
    fputs(STDERR,"Syntaxe : $argv[0] URL N [PHPSESSID]");
    // zatrzymanie
    exit(1);
  }//if

   // pobieranie parametrów
  $URL=$argv[1];
  $N=$argv[2];
  $PHPSESSID=$argv[3];

  // połączenie i wyświetlenie wyniku
  $résultats=getURL($URL,$N,$PHPSESSID);
  if(isset($résultats->erreur)){
       // błąd
    echo "L'erreur suivante s'est produite : $résultats->erreur\n";
  } 

   // koniec
  exit(0);

  //-----------------------------------------------------------------------
  function getURL($URL,$N,$PHPSESSID){

       // łączy się z URL
     // tworzy GET lub HEAD w zależności od wartości nagłówka
     // odpowiedź serwera stanowi wynik funkcji

     // analiza URL
    $url=parse_url($URL);
     // protokół
    if(strtolower($url["scheme"])!="http"){
        $résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
      return $résultats;
    }//if
     // urządzenie
    $hote=$url["host"];
    if(! isset($hote)){
        $résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
      return $résultats;
    }//if
     // port
    $port=$url["port"];
    if(! isset($port)) $port=80;
     // ścieżka
    $chemin=$url["path"];
     // zapytanie
    if(isset($url["query"])){
        $résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
      return $résultats;
    }//if

         // weryfikacja $N
    if (! preg_match("/^\d+$/",$N)){
        // błąd
      $résultats->erreur="nombre [$N] erroné";
       // koniec
      return $résultats;
    }//if

     // wykonuje się wywołania $N do $URL
    for($i=0;$i<$N;$i++){
      // nawiązanie połączenia na porcie $port serwera $hote
      $connexion=fsockopen($hote,$port,&$errno,&$erreur);
       // powrót w przypadku błędu
      if(! $connexion){
        $résultats->erreur="Echec de la connexion au site ($hote,$port) : $erreur";
        return $résultats;
      }//if

       // $connexion reprezentuje dwukierunkowy strumień komunikacji
       // między klientem (tym programem) a serwerem WWW, z którym nawiązano połączenie
       // kanał ten służy do wymiany poleceń i informacji
       // protokołem komunikacji jest HTTP

       // klient wysyła nagłówki HTTP
      // get URL HTTP/1.1
      envoie($connexion, "GET $chemin HTTP/1.1\n");
       // host: host:port
      envoie($connexion, "Host: $hote:$port\n");
       // Połączenie: zamknięte
      envoie($connexion, "Connection: close\n");
       // Cookie: $PHPSESSID
      if($PHPSESSID) envoie($connexion, "Cookie: PHPSESSID=$PHPSESSID\n");
       // pusta linia
      envoie($connexion,"\n");      

       // serwer będzie teraz odpowiadał na kanale $connexion. Wyśle wszystkie
       // swoje dane, a następnie zamknie kanał.
       // Klient zaczyna od odczytania nagłówków HTTP zakończonych pustą linią
      $CHUNKED=0;
      while(($ligne=fgets($connexion,10000)) && (($ligne=rtrim($ligne))!="")){
           // echo linii
        echo "<-- $ligne\n";
         // wyszukiwanie tokenu, jeśli jeszcze go nie znaleziono
        if(! $PHPSESSID){
             // wyszukiwanie wiersza „set-cookie”
          if(preg_match("/^Set-Cookie: PHPSESSID=(.*?);/i",$ligne,$champs)){
              // znaleziono token – zapisujemy go
            $PHPSESSID=$champs[1];            
          }//if
        }//if
        // wyszukiwanie trybu przesyłania dokumentu
        if(! $CHUNKED){
             // wyszukiwanie wiersza Transfer-Encoding: chunked
          if(preg_match("/^Transfer-Encoding: chunked/i",$ligne,$champs)){
              // przesyłanie fragmentami
            $CHUNKED=1;                        
          }//if
        }//if
      }//następny wiersz

       // wyświetlenie wiersza
      echo "<-- $ligne\n";

       // odczyt dokumentu zależy od sposobu, w jaki został wysłany
      if($CHUNKED) $document=getChunkedDoc($connexion);
          else $document=getDoc($connexion);

       // wyszukiwanie licznika w dokumencie
      if(preg_match("/<br>compteur : (\d+)/i",$document,$champs)){
           // znaleziono licznik – wyświetlamy go
        echo "\n[Le compteur est égal à $champs[1]]\n\n";
      }//if

       // klient zamyka połączenie
      fclose($connexion);
    }//for i
    }//getURL

  //--------------------------
  function getDoc($connexion){
      // odczyt dokumentu na $connexion
    $doc="";
    while($ligne=fread($connexion,10000))
        $doc.=$ligne;
     // koniec
    return $doc;
  }//getDoc

  //--------------------------
  function getChunkedDoc($connexion){
       // odczyt dokumentu na $connexion
     // dokument ten jest wysyłany fragmentami w postaci
     // liczba znaków w fragmencie w systemie szesnastkowym
     // kontynuacja fragmentu
         // pusta linia

     // odczytuje się rozmiar fragmentu z pierwszego wiersza
    $taille=hexdec(rtrim(fgets($connexion,10000)));    
     // odczytuje się następujący dokument
    $doc="";
    while($taille!=0){
         // odczyt fragmentu o długości $taille znaków
      $doc.=fread($connexion,$taille);
       // pusty wiersz
      fgets($connexion,10000);
       // kolejny fragment
       // odczyt rozmiaru fragmentu
      $taille=hexdec(rtrim(fgets($connexion,10000)));      
    }//while
     // koniec
    return $doc;
  }// getChunkedDoc  

  //--------------------------
  function envoie($flux,$msg){
       // wysyła $msg na $flux
    fwrite($flux,$msg);
     // wyświetl na ekranie
    echo "--> $msg";
  }//wysyła
?>

Przeanalizujmy najważniejsze elementy tego programu:

  • musimy wykonać N wymian danych między klientem a serwerem. Dlatego też są one umieszczone w pętli
            for($i=0;$i<$N;$i++){
  • przy każdej wymianie klient nawiązuje połączenie TCP-IP z serwerem. Po nawiązaniu połączenia wysyła do serwera nagłówki HTTP swojego żądania:
<?php
...
       // klient wysyła nagłówki HTTP
      // get URL HTTP/1.1
      envoie($connexion, "GET $chemin HTTP/1.1\n");
       // host: host:port
      envoie($connexion, "Host: $hote:$port\n");
       // Połączenie: zamknięte
      envoie($connexion, "Connection: close\n");
       // Cookie: $PHPSESSID
      if($PHPSESSID) envoie($connexion, "Cookie: PHPSESSID=$PHPSESSID\n");
       // pusta linia
      envoie($connexion,"\n");      

Jeśli token PHPSESSID jest dostępny, jest wysyłany w postaci pliku cookie, w przeciwnym razie nie jest wysyłany. Należy zauważyć, że klient wskazał, iż korzysta z protokołu HTTP/1.1. To wyjaśnia, dlaczego później serwer wyśle mu nagłówek HTTP: Transfer-Encoding: chunked, który należy do protokołu HTTP/1.1, ale nie do protokołu HTTP/1.0.

  • Po wysłaniu żądania klient oczekuje na odpowiedź serwera. Najpierw analizuje nagłówki HTTP zawarte w tej odpowiedzi. Szuka w niej dwóch wierszy:
Cookie : 
Transfer-Encoding: chunked

Wiersz „Cookie:” to nagłówek HTTP, który zawiera token sesji PHPSESSID. Klient musi go pobrać, aby odesłać go do serwera podczas kolejnej wymiany danych. Wiersz „Transfer-Encoding: chunked”, jeśli występuje, oznacza, że serwer wyśle dokument w fragmentach. Każdy fragment jest wówczas wysyłany do klienta w następującej postaci:

[nombre de caractères du document en hexadécimal]
[document]
[ligne vide]

Jeśli wiersz Transfer-Encoding: chunked nie występuje, dokument jest wysyłany jednorazowo po pustym wierszu nagłówków HTTP. Zatem w zależności od obecności lub braku tego wiersza sposób odbioru dokumentu będzie się różnił. Kod interpretacji nagłówków HTTP jest następujący:

<?php
...
       // Klient zaczyna od odczytania nagłówków HTTP zakończonych pustym wierszem
      $CHUNKED=0;
      while(($ligne=fgets($connexion,10000)) && (($ligne=rtrim($ligne))!="")){
           // echo linii
        echo "<-- $ligne\n";
         // wyszukiwanie tokenu, jeśli jeszcze go nie znaleziono
        if(! $PHPSESSID){
             // wyszukiwanie wiersza „set-cookie”
          if(preg_match("/^Set-Cookie: PHPSESSID=(.*?);/i",$ligne,$champs)){
              // znaleziono token – zapisujemy go
            $PHPSESSID=$champs[1];            
          }//if
        }//if
        // wyszukiwanie trybu przesyłania dokumentu
        if(! $CHUNKED){
             // wyszukiwanie wiersza Transfer-Encoding: chunked
          if(preg_match("/^Transfer-Encoding: chunked/i",$ligne,$champs)){
              // przesyłanie fragmentami
            $CHUNKED=1;                        
          }//if
        }//if
      }//następny wiersz
  • gdy token zostanie znaleziony po raz pierwszy, nie będzie już wyszukiwany podczas kolejnych wywołań serwera. Po przetworzeniu nagłówków odpowiedzi HTTP przechodzi się do dokumentu następującego po nagłówkach HTTP. Dokument ten jest odczytywany w różny sposób w zależności od trybu transferu:
<?php
...

       // odczyt dokumentu zależy od sposobu, w jaki został wysłany
      if($CHUNKED) $document=getChunkedDoc($connexion);
          else $document=getDoc($connexion);
  • W otrzymanym dokumencie $document szukamy wiersza zawierającego wartość licznika. Wyszukiwanie to również odbywa się za pomocą wyrażenia regularnego:
<?php
...
       // wyszukiwanie licznika w dokumencie
      if(preg_match("/<br>compteur : (\d+)/i",$document,$champs)){
           // znaleziono licznik – wyświetlamy go
        echo "\n[Le compteur est égal à $champs[1]]\n\n";
      }//if
  • W przypadku, gdy serwer wysyła dokument w całości, jego odbiór jest prosty:
<?php
...

  //--------------------------
  function getDoc($connexion){
       // odczyt dokumentu o numerze $connexion
    $doc="";
    while($ligne=fgets($connexion,10000))
        $doc.=$ligne;
     // koniec
    return $doc;
  }//getDoc
  • W przypadku, gdy serwer wysyła dokument w kilku częściach, jego odczytanie jest bardziej skomplikowane:
<?php
...
  function getChunkedDoc($connexion){
       // odczyt dokumentu na $connexion
     // dokument ten jest wysyłany fragmentami w postaci
     // liczba znaków w fragmencie w systemie szesnastkowym
     // kontynuacja fragmentu

     // w pierwszym wierszu odczytuje się rozmiar fragmentu
    $taille=hexdec(rtrim(fgets($connexion,10000)));    
     // odczytuje się następujący dokument
    $doc="";
    while($taille!=0){
         // odczyt fragmentu o długości $taille znaków
      $doc.=fread($connexion,$taille);
       // pusty wiersz
      fgets($connexion,10000);
       // kolejny fragment
       // odczyt rozmiaru fragmentu
      $taille=hexdec(rtrim(fgets($connexion,10000)));      
    }//while
     // koniec
    return $doc;
  }// getChunkedDoc   

Przypomnijmy, że fragment dokumentu jest wysyłany w postaci

[taille]
[partie de document]
[ligne vide]

Zaczynamy więc od odczytania rozmiaru dokumentu. Znając już ten rozmiar, wywołujemy funkcję fread, aby odczytała $taille znaków ze strumienia $connexion, a następnie następujący po nim pusty wiersz. Powtarzamy tę czynność, dopóki serwer nie poinformuje, że wyśle dokument o rozmiarze 0.

3.10.5. Przykład 4

W poprzednim przykładzie klient internetowy zwraca token w postaci pliku cookie. Widzieliśmy, że może on również zwrócić token w samym żądaniu URL w postaci URL;PHPSESSID=xxx. Sprawdźmy to. Program clientCompteur.php zostaje przekształcony w clientCompteur2.php i zmodyfikowany w następujący sposób:

<?php
...
....
       // klient wysyła nagłówki HTTP
      // get URL HTTP/1.1
      if($PHPSESSID)
          envoie($connexion, "GET $chemin?PHPSESSID=$PHPSESSID HTTP/1.1\n");
        else envoie($connexion, "GET $chemin HTTP/1.1\n");
       // host: host:port
      envoie($connexion, "Host: $hote:$port\n");
       // Połączenie: zamknięte
      envoie($connexion, "Connection: close\n");
       // pusta linia
      envoie($connexion,"\n");      
....

Klient żąda zatem odczytu licznika o numerze URL za pomocą GET i URL;PHPSESSID=xx HTTP/1.1 i nie wysyła już pliku cookie. To jedyna zmiana. Oto wyniki pierwszego wywołania:


dos>"e:\php43\php.exe" clientCompteur2.php http://localhost/poly/sessions/2/cycledevie.php 2

--> GET /poly/sessions/2/cycledevie.php HTTP/1.1
--> Host: localhost:80
--> Connection: close
-->
<-- HTTP/1.1 200 OK
<-- Date: Thu, 10 Oct 2002 07:21:19 GMT
<-- Server: Apache/1.3.24 (Win32)
<-- Set-Cookie: PHPSESSID=573212ba82303d7903caf8944ee7a86f; path=/poly/sessions/2
<-- Expires: Thu, 19 Nov 1981 08:52:00 GMT
<-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
<-- Pragma: no-cache
<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html
<--

[Le compteur est égal à 0]

--> GET /poly/sessions/2/cycledevie.php?PHPSESSID=573212ba82303d7903caf8944ee7a86f HTTP/1.1
--> Host: localhost:80
--> Connection: close
-->
<-- HTTP/1.1 200 OK
<-- Date: Thu, 10 Oct 2002 07:21:19 GMT
<-- Server: Apache/1.3.24 (Win32)
<-- Expires: Thu, 19 Nov 1981 08:52:00 GMT
<-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
<-- Pragma: no-cache
<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html
<--

[Le compteur est égal à 1]

Podczas pierwszego wywołania klient żąda zasobu URL bez tokenu sesji. Serwer odpowiada, wysyłając mu token. Następnie klient ponownie żąda tego samego zasobu URL, dołączając do niego otrzymany token. Widać, że licznik został prawidłowo zwiększony, co świadczy o tym, że serwer prawidłowo rozpoznał, iż chodzi o tę samą sesję.

3.10.6. Przykład 5

Ten przykład przedstawia aplikację składającą się z trzech stron, które nazwiemy page1, page2 i page3. Użytkownik musi je wyświetlić w następującej kolejności:

  • strona1 to formularz wymagający podania informacji: imienia
  • strona2 to formularz wyświetlany w odpowiedzi na wysłanie formularza ze strony1. Wymaga on podania drugiej informacji: wieku
  • strona3 to dokument o nazwie HTML, który wyświetla imię uzyskane na stronie1 oraz wiek uzyskany na stronie2.

Mają miejsce trzy wymiany danych między klientem a serwerem:

  • podczas pierwszej wymiany klient żąda formularza ze strony 1, a serwer go wysyła
  • podczas drugiej wymiany klient wysyła formularz ze strony 1 (imię) do serwera. W odpowiedzi otrzymuje formularz ze strony 2 lub ponownie formularz ze strony 1, jeśli był on błędny.
  • podczas trzeciej wymiany klient wysyła formularz ze strony 2 (wiek) do serwera. W odpowiedzi otrzymuje formularz ze strony 3 lub ponownie formularz ze strony 2, jeśli był on błędny. Dokument ze strony 3 wyświetla imię i wiek. Imię zostało uzyskane przez serwer podczas drugiej wymiany danych i od tego momentu zostało „zapomniane”. Wykorzystuje się sesję do zapisania imienia podczas wymiany nr 2, aby było ono dostępne podczas wymiany nr 3.

Strona „page1” uzyskana podczas pierwszej wymiany wygląda następująco:

Image

Wypełniamy pole imienia:

Image

Klikamy przycisk [Suite] i otrzymujemy następującą stronę page2:

Image

Wpisujemy wiek w polu:

Image

Klikamy przycisk [Suite], po czym wyświetla się następująca strona page3:

Image

Po przesłaniu strony page1 do serwera, serwer może zwrócić ją z kodem błędu, jeśli pole „imię” jest puste:

Image

Po przesłaniu strony page2 do serwera może on zwrócić ją z kodem błędu, jeśli wiek jest nieprawidłowy:

Image

Aplikacja składa się z sześciu programów:

etape1.php
wykorzystuje page1.php
page1.php
wyświetla stronę1. Formularz ze strony1 jest przetwarzany przez etape2.php.
etape2.php
przetwarza wartości z formularza strony1. Jeśli wystąpią błędy, strona1 jest ponownie wyświetlana przez page1.php, w przeciwnym razie strona2 jest wyświetlana przez page2.php.
page2.php
wyświetla stronę 2. Formularz ze strony 2 jest przetwarzany przez etape3.php.
etape3.php
przetwarza wartości z formularza na stronie 2. Jeśli wystąpią błędy, strona 2 jest ponownie wyświetlana przez page2.php, w przeciwnym razie strona 3 jest wyświetlana przez page3.php.
page3.php
wyświetla stronę 3.

Krok 1 aplikacji jest obsługiwany przez następujący program etape1.php:

<?php
   // etape1.php

   // konfiguracja
  ini_set("register_globals","off");
  ini_set("display_errors","off");

  // rozpoczęcie sesji
  session_start();
  $_SESSION["session"]="";    //zresetowanie zmiennej sesji

   // przygotowanie strony 1
    $requête->nom="";
  $requête->erreurs=array();
   // wyświetlenie strony 1
    include "page1.php";
   // koniec  
  exit(0);
?>    

Należy zwrócić uwagę na następujące kwestie:

  • aplikacja wymaga śledzenia sesji. Dlatego każdy etap sesji rozpoczyna nową sesję.
  • Informacje dotyczące sesji, które należy zachować, będą przechowywane w obiekcie $session.
  • Informacje niezbędne do wyświetlenia poszczególnych (trzech) stron aplikacji zostaną umieszczone w obiekcie $requête.

Program page1.php wyświetla informacje zawarte w obiekcie $requête:

<? // page1.php ?>

<html>
  <head>
    <title>page 1</title>
  </head>
  <body>
    <h3>Page 1/3</h3>
    <form name="frmNom" method="POST" action="etape2.php">
      <table>
        <tr>
          <td>Votre nom</td>
          <td><input type="text" name="nom" value="<? echo $requête->nom ?>"></td>
        </tr>
      </table>
      <input type="submit" value="Suite">
    </form>
    <? // błędy?
      if (count($requête->erreurs)!=0){
    ?>
      <hr>
      <font color="red">
        Les erreurs suivantes se sont produites
        <ul>
        <? for($i=0;$i<count($requête->erreurs);$i++){ ?>
            <li><? echo $requête->erreurs[$i] ?>
        <? }//for ?>
        </ul>
     <? }//if ?>
  </body>
</html>
  • Strona otrzymuje obiekt $requête zawierający dwa pola: nom i erreurs. Wyświetla wartości tych dwóch pól.
  • Ponadto zawiera formularz. Jego wartości (nazwa) są przesyłane metodą POST do programu etape2.php:
    <form name="frmNom" method="POST" action="etape2.php">

Aplikacja etape2.php ma za zadanie przetworzyć wartości z formularza na stronie 1 i ponownie wyświetlić stronę 1 w przypadku wystąpienia błędów (błędne imię), a w przeciwnym razie wyświetlić stronę 2 w celu uzyskania wieku.

<?php
   // etape2.php

   // konfiguracja
  ini_set("register_globals","off");
  ini_set("display_errors","off");

  // rozpoczęcie sesji
  session_start();

   // zazwyczaj powinien występować parametr „nazwa”
   // zapisany w zapytaniu
  $requête->nom=$_POST["nom"];

   // jeśli nie ma parametrów, wysyłamy stronę 1 bez błędów
  if (! isset($requête->nom)){
      $requête->nom="";
    $requête->erreurs=array();
    include "page1.php";
    exit(0);
  }//if

  // jeśli parametr „nazwa” rzeczywiście występuje – sprawdzamy jego poprawność
  $page=calculerPage($requête);

   // czy wystąpiły błędy?
  if(count($page->erreurs)!=0){
     // strona 1 z błędami
    $requête->erreurs=$page->erreurs;
    include "page1.php";
    exit(0);
  }//if

  // brak błędów – zapisujemy nazwę w sesji
  unset($session);
  $session->nom=$requête->nom;
  $_SESSION["session"]=$session;

   // wyświetlanie strony 2
  $requête->age="";
  $requête->erreurs=array();
  include "page2.php";

   // koniec
  exit(0);

   // ---------calculerPage
  function calculerPage($requête){
     // sprawdza poprawność zapytania $requête
     // zwraca tablicę błędów w $page->błędy

     // na początku nie ma błędów
    $page->erreurs=array();
     // nazwa nie może być pusta
    if (preg_match("/^\s*$/",$requête->nom)){
      $page->erreurs[]="Vous n'avez pas indiqué de nom";
    }
     // powrót do strony
    return $page;
  }//calculerPage
?>
  • etape2 najpierw sprawdza, czy posiada oczekiwany parametr nom. Jeśli tak nie jest, powoduje ponowne wyświetlenie pustej strony1. Taka sytuacja może wystąpić, jeśli funkcja etape2 jest wywoływana bezpośrednio przez klienta, który nie przekazuje jej żadnych parametrów.
  • Jeśli parametr nom jest obecny, sprawdzana jest jego poprawność. Odbywa się to za pośrednictwem procedury o nazwie calculerPage, której zadaniem jest utworzenie obiektu $page z polem erreurs, które jest tablicą błędów. Możliwy jest tylko jeden błąd, ale chcieliśmy pokazać, że można obsługiwać listę błędów.
  • Jeśli wystąpią błędy, strona page1 jest wyświetlana ponownie wraz z listą błędów.
  • Jeśli nie ma błędów, nazwa jest zapisywana w obiekcie $session, który przechowuje dane związane z bieżącą sesją. Następnie wyświetlana jest strona page2.

Program page2.php wyświetla stronę 2:

<? // page2.php ?>

<html>
  <head>
    <title>page 2</title>
  </head>
  <body>
    <h3>Page 2/3</h3>
    <form name="frmAge" method="POST" action="etape3.php">
      <table>
        <tr>
          <td>Nom</td>
          <td><font color="green"><? echo $requête->nom ?></font></td>
        </tr>
        <tr>
          <td>Votre âge</td>
          <td><input type="text" name="age" size="3" value="<? echo $requête->age ?>"></td>
        </tr>
      </table>
      <input type="submit" value="Suite">
    </form>
    <? // błędy?
      if (count($requête->erreurs)!=0){
    ?>
      <hr>
      <font color="red">
        Les erreurs suivantes se sont produites
        <ul>
        <? for($i=0;$i<count($requête->erreurs);$i++){
            echo "<li>".$requête->erreurs[$i];
           }//dla 
        ?>
        </ul>
      </font>
     <? } ?>
  </body>
</html>

Zasada działania tej strony jest bardzo podobna do tej opisanej w przypadku page2.php. Wyświetla zawartość obiektu $requête zawierającego pola nom, age i erreurs. Wyświetla formularz, którego wartości będą przetwarzane przez etape3.php.

    <form name="frmAge" method="POST" action="etape3.php">

Program etape3.php przetwarza zatem wartości z formularza na stronie 2, ograniczone w tym przypadku do wieku:

<?php
   // etape3.php

   // konfiguracja
  ini_set("register_globals","off");
  ini_set("display_errors","off");

  // rozpoczęcie sesji
  session_start();

   // pobieramy parametry „nazwa” i „wiek”
  $requête->age=$_POST["age"];
  $session=$_SESSION["session"];
  $requête->nom=$session->nom;

   // zazwyczaj powinny być podane imię i wiek
  if (! isset($requête->age) || ! isset($requête->nom)){
     // jeśli wywołanie jest nieprawidłowe, wysyłamy stronę 1
    $_SESSION["session"]="";    // na wszelki wypadek
    $requête->nom="";
    $requête->erreurs=array();
    include "page1.php";
    exit(0);
  }//if

  // parametr „age” jest obecny – sprawdzamy jego poprawność
  $page=calculerPage($requête);

   // czy wystąpiły błędy?
  if(count($page->erreurs)!=0){
     // strona 2 z błędami
    $requête->erreurs=$page->erreurs;
    include "page2.php";
    exit(0);
  }//if

  // brak błędu – zapisanie wieku w sesji
  $session->age=$requête->age;
  $_SESSION["session"]=$session;

   // wyświetlenie strony 3
  include "page3.php";
   // koniec
  exit(0);

   // ---------calculerPage
  function calculerPage($requête){
     // sprawdza poprawność zapytania $requête
     // zwraca tablicę błędów w $page->błędy

     // na początku nie ma błędów
    $page->erreurs=array();
     // wiek musi mieć prawidłowy format
    if (! preg_match("/^\s*\d{1,3}\s*$/",$requête->age)){
      $page->erreurs[]="âge incorrect";
    }
     // powrót do strony
    return $page;
  }//calculerPage
?>
  • Program rozpoczyna się od pobrania nazwy z sesji (pochodzącej ze strony 1) oraz wieku z formularza na stronie 2. Jeśli brakuje którejkolwiek z tych informacji, wyświetlana jest strona 1.
  • Następnie sprawdzana jest poprawność wieku. Jeśli wiek jest nieprawidłowy, ponownie wyświetlana jest strona 2 wraz z listą błędów. Jeśli wiek jest prawidłowy, wyświetlana jest strona 3. Strona ta wyświetla jedynie dwie wartości (imię, wiek) uzyskane z obu formularzy (strona 1, strona 2).
<? // page3.php ?>

<html>
  <head>
    <title>page 3</title>
  </head>
  <body>
    <h3>Page 3/3</h3>
      <table>
        <tr>
          <td>Nom</td>
          <td><font color="green"><? echo $requête->nom ?></font></td>
        </tr>
        <tr>
          <td>Votre âge</td>
          <td><font color="green"><? echo $requête->age ?></font></td>
        </tr>
      </table>
  </body>
</html>