Skip to content

7. Studium przypadku: zarządzanie bazą artykułów w Internecie

Kody tego studium przypadku są dostępne pod numerem |ICI|.

Cele:

  • napisać klasę do zarządzania bazą danych artykułów
  • stworzenie aplikacji internetowej opartej na tej klasie
  • wprowadzenie arkuszy stylów
  • zaproponować wstępną metodologię tworzenia prostych aplikacji internetowych
  • wprowadzenie języka JavaScript w przeglądarce klienckiej

Źródła: Istota niniejszego studium przypadku została zaczerpnięta z książki „Les cahiers du programmeur – PHP/MySQL” autorstwa Jean-Philippe’a Leboeufa, wydanej przez wydawnictwo Eyrolles.

7.1. Wprowadzenie

Właściciel sklepu chce zarządzać artykułami, które sprzedaje w swoim sklepie. Posiada już aplikację ACCESS, która wykonuje tę pracę, ale kusi go przygoda z internetem. Posiada konto u dostawcy usług internetowych, który zezwala swoim klientom na instalowanie skryptów PHP w ich folderach osobistych. Dzięki temu mogą tworzyć dynamiczne strony internetowe. Ponadto ci sami klienci dysponują kontem MySQL, które pozwala im tworzyć tabele dostarczające dane do ich skryptów PHP. W ten sposób sprzedawca posiada konto MySQL z loginem „adarticles” i hasłem „mdparticles”. Dysponuje on bazą danych „dbarticles”, do której ma pełne uprawnienia. Nasz sprzedawca ma zatem wystarczające elementy, aby przenieść swoje zarządzanie artykułami do sieci. Z pomocą Państwa, którzy posiadają umiejętności w zakresie tworzenia stron internetowych, rozpoczyna tę przygodę.

7.2. Baza danych

Nasz sprzedawca przygotowuje następujący szkic interfejsu strony głównej, jaki chciałby uzyskać:

Image

Będą dwa rodzaje użytkowników:

  • administratorzy, którzy mogliby wykonywać wszystkie czynności związane z tabelą produktów (dodawać, modyfikować, usuwać, przeglądać itp.). Będą oni mogli korzystać ze wszystkich elementów powyższego menu. W szczególności będą mogli wysyłać dowolne zapytania SQL za pomocą opcji [Requête SQL].
  • zwykli użytkownicy (niebędący administratorami), którzy mieliby ograniczone uprawnienia: prawo do dodawania, edytowania, usuwania i przeglądania. Mogą oni posiadać tylko niektóre z tych uprawnień, na przykład wyłącznie prawo do przeglądania.

Ponieważ w bazie danych występują różne typy użytkowników, którzy nie mają takich samych uprawnień, konieczne jest uwierzytelnianie. Dlatego strona główna rozpoczyna się od tego etapu. Aby ustalić, kto jest kim i kto ma prawo do jakich czynności, wykorzystane zostaną dwie tabele: USERS i DROITS. Tabela USERS miałaby następującą strukturę:

login
login użytkownika, który jednoznacznie go identyfikuje. Pole to jest kluczem głównym tabeli.
mdp
hasło użytkownika w postaci niezaszyfrowanej
admin 
znak „y” (yes), jeśli użytkownik jest administratorem, w przeciwnym razie znak „n” (no).

Zawartość tabeli mogłaby wyglądać następująco:

Image

Tabela DROITS określa uprawnienia użytkowników niebędących administratorami, znajdujących się w tabeli USERS. Jej struktura jest następująca:

login
nazwa użytkownika, która jednoznacznie go identyfikuje.
Pole to jest kluczem obcym tabeli DROITS i odwołuje się
do kolumny „login” w tabeli USERS.
table
nazwa tabeli, do której użytkownik ma uprawnienia.
ajouter
znak „y” (yes), jeśli użytkownik ma uprawnienie do dodawania w tabeli,
w przeciwnym razie znak „n” (no).
modifier
uprawnienie do modyfikacji: „y” lub „n”
supprimer
uprawnienie do usuwania: „y” lub „n”
consulter
uprawnienie do przeglądania: „y” lub „n”

Zawartość tabeli mogłaby wyglądać następująco:

Image

Uwagi:

  • Użytkownik U, który znajduje się w tabeli USERS, a nie występuje w tabeli DROITS, nie ma żadnych uprawnień.
  • W naszym przykładzie użytkownicy będą mieli dostęp tylko do jednej tabeli, a mianowicie do tabeli ARTICLES. Jednak nasz przedsiębiorczy sprzedawca, przewidując przyszłe potrzeby, dodał pole „table” do struktury tabeli DROITS, aby zapewnić sobie możliwość dodania w przyszłości nowych tabel do swojej aplikacji.
  • Po co zarządzać uprawnieniami w naszych własnych tabelach, skoro zakładamy, że będziemy korzystać z bazy MySQL, która sama (i to lepiej niż my) jest w stanie zarządzać tymi uprawnieniami w swoich własnych tabelach? Po prostu dlatego, że nasz sprzedawca nie posiada uprawnień administracyjnych do bazy MySQL, które pozwoliłyby mu tworzyć użytkowników i przyznawać im uprawnienia. Nie zapominajmy bowiem, że baza MySQL jest hostowana u dostawcy usług internetowych, a sprzedawca jest jedynie zwykłym użytkownikiem tej bazy, pozbawionym jakichkolwiek uprawnień administracyjnych (na szczęście). Posiada on jednak pełne uprawnienia do bazy o nazwie dbarticles, do której obecnie uzyskuje dostęp przy użyciu loginu admarticles i hasła mdparticles. To właśnie w tej bazie znajdują się wszystkie tabele aplikacji.

Tabela ARTICLES zawiera informacje o artykułach sprzedawanych przez sprzedawcę. Jej struktura jest następująca:

code
kod artykułu – klucz główny tabeli
– dokładnie 4 znaki
nom
nazwa artykułu
prix
jego cena
stockActuel
aktualny stan magazynowy
stockMinimum
poziom, poniżej którego należy złożyć
uzupełniające

Jego zawartość, wykorzystana początkowo w celach testowych, mogłaby wyglądać następująco:

Image

7.3. Ograniczenia projektu

Właściciel sklepu przenosi tutaj lokalną aplikację ACCESS do aplikacji internetowej. Nie wie, jak ta aplikacja będzie wyglądać w przyszłości i jak się będzie rozwijać. Chciałby jednak, aby nowa aplikacja była łatwa w obsłudze i skalowalna. Z tego powodu jego doradca informatyczny, podczas projektowania tabel, przewidział, że mogą istnieć:

  • różni użytkownicy z różnymi uprawnieniami: pozwoli to przedsiębiorcy delegować niektóre zadania innym osobom bez przyznawania im uprawnień administracyjnych
  • w przyszłości inne tabele oprócz tabeli ARTICLES

Ten sam konsultant przedstawia również inne propozycje:

  • wie, że w tworzeniu oprogramowania należy wyraźnie oddzielić warstwę prezentacji od warstwy przetwarzania. Architektura aplikacji internetowej często wygląda następująco:

Interfejsem użytkownika jest tutaj przeglądarka internetowa, ale mogłaby to być również aplikacja autonomiczna, która poprzez sieć wysyłałaby zapytania HTTP do serwisu internetowego i formatowała wyniki, które ten jej przesyła. Logika aplikacji składa się ze skryptów przetwarzających żądania użytkownika, w tym przypadku skryptów PHP. Źródłem danych jest często baza danych, ale może to być również katalog LDAP lub zdalna usługa internetowa. W interesie programisty leży zachowanie dużej niezależności między tymi trzema elementami, tak aby w przypadku zmiany jednego z nich pozostałe dwa nie musiały ulegać zmianom lub zmiany te były minimalne. Doradca informatyczny przedsiębiorcy przedstawia zatem następujące propozycje:

  • Logikę biznesową aplikacji umieścimy w klasie PHP. W ten sposób powyższy blok [Logique applicative] będzie składał się z następujących elementów:

W bloku [Logique Applicative] można wyróżnić

  • blok [IE=Interface d'Entrée], który stanowi punkt wejścia do aplikacji. Jest on taki sam niezależnie od typu klienta.
  • blok [Classes métier], który zawiera klasy niezbędne do działania logiki aplikacji. Są one niezależne od klienta.
  • blok generatorów stron odpowiedzi [IS1 IS2 ... IS=Interface de Sortie]. Każdy generator odpowiada za formatowanie wyników dostarczanych przez logikę aplikacji dla danego typu klienta: kod HTML dla przeglądarki lub telefonu WAP, kod XML dla aplikacji autonomicznej itp.

Model ten zapewnia dużą niezależność od klientów. Niezależnie od tego, czy klient się zmienia, czy też chcemy zmodyfikować sposób prezentacji wyników, należy stworzyć lub dostosować generatory wyjściowe [IS].

  • W aplikacji internetowej niezależność między warstwą prezentacji a warstwą przetwarzania można zwiększyć poprzez zastosowanie arkuszy stylów. Arkusze te regulują wygląd strony internetowej w przeglądarce. Aby zmienić ten wygląd, wystarczy zmienić powiązany arkusz stylów. Nie ma potrzeby ingerowania w logikę przetwarzania. W tym przypadku zastosujemy więc arkusz stylów.
  • Na powyższym diagramie to klasa biznesowa będzie pełnić rolę interfejsu z źródłem danych. Zgodnie z założeniem źródłem tym jest tutaj baza danych MySQL. Aby umożliwić przejście na inną bazę danych, wykorzystamy bibliotekę PEAR, która oferuje klasy dostępu do baz danych niezależne od ich rzeczywistego typu. Jeśli więc nasz przedsiębiorca wzbogaci się na tyle, by móc zainstalować w swojej firmie serwer internetowy IIS firmy Microsoft, będzie mógł zastąpić bazę MySQL serwerem SQL bez konieczności (lub przy minimalnej konieczności) wprowadzania zmian w klasie biznesowej.

7.4. Klasa artykułów

Klasę artykułów można zdefiniować w następujący sposób:

<?php

     // klasa artykułów działająca w oparciu o bazę artykułów złożoną z następujących tabel
     // artykuły: (kod, nazwa, cena, stockActuel, stockMinimum)
     // użytkownicy: (login, hasło, admin)
     // uprawnienia: (login, tabela, dodawanie, edycja, usuwanie, przeglądanie)

     // jest to użytkownik klasy, który musi podać login/hasło umożliwiające wykonywanie wszelkich operacji w bazie
     // ma więc już wszystkie uprawnienia do bazy. Oznacza to, że nie ma potrzeby stosowania
     // żadnych szczególnych środków bezpieczeństwa

     // biblioteki
  require_once 'DB.php';

  class articles{

           // atrybuty
    var $sDSN;                        // łańcuch połączenia
          var $sDatabase;            // nazwa bazy danych
    var $oDB;                        // połączenie z bazą danych
    var $aErreurs;                // lista błędów
    var $oRésultats;            // wynik zapytania SELECT
        var $connecté;                // wartość logiczna wskazująca, czy nawiązano połączenie z bazą danych
        var $sQuery;                    // ostatnie wykonane zapytanie
    var $sUser;                    // identyfikator użytkownika, który nawiązał połączenie
    var $bAdmin;                    // ma wartość „prawda”, jeśli użytkownik jest administratorem
    var $dDroits;                // słownik uprawnień użytkownika: tabela ->> tablica (przeglądaj, dodaj, usuń, edytuj)

     // konstruktor
    function articles($dDSN,$sUser,$sMdp){

             // $dDSN: słownik określający połączenie, które należy ustanowić
       // $dDSN['sgbd']: typ obiektu SGBD, z którym należy się połączyć
       // $dDSN['host']: nazwa komputera hosta, na którym się znajduje      
       // $dDSN['database']: nazwa bazy danych, z którą należy się połączyć      
       // $dDSN['admin']: nazwa użytkownika właściciela bazy danych, z którą należy się połączyć
       // $dDSN['mdpadmin']: jego hasło
       // $sUser: nazwa użytkownika, który chce korzystać z bazy artykułów
       // $sMdp: jego hasło

       // tworzy w $oDB połączenie z bazą zdefiniowaną przez $dDSN pod tożsamością $dDSN['admin']
       // jeśli połączenie się powiedzie, a użytkownik $sUser zostanie uwierzytelniony  
           // ładuje uprawnienia do $bAdmin oraz $dDroits – uprawnienia użytkownika $sUser
           // zapisuje w $sDSN ciąg połączenia z bazą danych
           // umieszcza w $sDataBase nazwę bazy danych, z którą nawiązywane jest połączenie
         // ustawia $connecté na wartość „prawda”
       // jeśli połączenie nie powiedzie się lub jeśli użytkownik $sUser nie zostanie poprawnie zidentyfikowany
           // umieszcza odpowiednie komunikaty o błędach na liście $aErreurs
         // w razie potrzeby zamyka połączenie
         // ustawia $connecté na „fałsz” 

  ...
    }//konstruktor

    // ------------------------------------------------------------------
    function connect(){
             // (ponowne) połączenie z bazą
...
    }//połączenie

    // ------------------------------------------------------------------
    function disconnect(){
      // zamykanie połączenia z bazą $sDSN
...
    }//rozłącz

    // -------------------------------------------------------------------
    function execute($sQuery,$bAdmin){
            // $sQuery: żądanie do wykonania
       // $bAdmin: wartość „prawda”, jeśli żądanie ma zostać wykonane jako administrator
...
    }//execute

    // --------------------------------------------------------------------------
    function addArticle($dArticle){
         // dodaje artykuł $dArticle (kod, nazwa, cena, stockActuel, stockMinimum) do tabeli artykułów
   ...
    }//dodaj          

    // ----------------------------------------------------------------------
    function modifyArticle($dArticle){
             // zmienia artykuł $dArticle (kod, nazwa, cena, stockActuel, stockMinimum) z tabeli artykułów
...
    }//aktualizacja

    // ----------------------------------------------------------------------
    function deleteArticle($sCode){
             // usuwa artykuł z tabeli artykułów
       //, którego kod to $sCode
...
    }//usunięcie

    // ----------------------------------------------------------------------
    function vérifierArticle(&$dArticle){
         // sprawdza poprawność pozycji $dArticle (kod, nazwa, cena, stockActuel, stockMinimum)
...
    }//sprawdź

    // --------------------------------------------------------------------------
    function selectArticles($dQuery){
             // wykonuje zapytanie SELECT z tabeli artykułów
       // tabela ta ma trzy składniki
       // lista kolumn w $dQuery['colonnes']
       // filtrowanie w $dQuery['where']
       // kolejność wyświetlania w $dQuery['orderby']
...
    }//selectArticles            

        // --------------------------------
    function existeArticle($sCode){
         // zwraca TRUE, jeśli pozycja o kodzie $sCode istnieje w tabeli pozycji
...
    }//existeArticle

    // --------------------------------------
    function existeUser($sUser,$sMdp){
             // sprawdza, czy istnieje użytkownik o nazwie $sUser z hasłem $sMdp
       // zwraca (int $iErreur, string $sAdmin, tablica skrótów $dDroits)
       // $iErreur = -1 w przypadku jakiegokolwiek błędu podczas obsługi bazy – wówczas wypełniana jest lista $aErreurs
       // $iErreur = 1, jeśli użytkownik nie został znaleziony (nie istnieje lub podano nieprawidłowe hasło)
       // $iErreur = 2, jeśli użytkownik istnieje, ale nie ma żadnych uprawnień w tabeli uprawnień
       // $iErreur = 3, jeśli użytkownik istnieje i jest administratorem
       // $iErreur = 0, jeśli użytkownik istnieje, ale nie jest administratorem
       // $sAdmin = „y”, jeśli użytkownik istnieje i jest administratorem ($iErreur == 3), w przeciwnym razie jest równy pustemu ciągowi znaków
       // $dDroits to słownik uprawnień użytkownika, jeśli nie jest on administratorem ($iErreur==0)
       // w przeciwnym razie jest to pusta tablica
       // klucze słownika to tabele, do których użytkownik ma uprawnienia
       // wartość powiązana z tą tabelą jest z kolei słownikiem, w którym klucze to uprawnienia
       // (przeglądanie, dodawanie, modyfikowanie, usuwanie), a wartościami są ciągi znaków „y” (tak) lub „n” (nie) w zależności od przypadku
...
    }//existeUser

    // --------------------------------------
    function getCodes(){
         // zwraca tabelę kodów
....
    }//getCodes    

  }//klasyfikuje
?>      

Komentarze

  • Klasa artykułów korzysta z biblioteki PEAR::DB w celu uzyskania dostępu do bazy danych, stąd polecenie
require_once 'DB.php';

To włączenie zakłada, że skrypt DB.php znajduje się w jednym z katalogów określonych przez opcję include_path w pliku konfiguracyjnym PHP.

  • Konstruktor musi wiedzieć, z którą bazą danych nawiązuje połączenie i pod jaką tożsamością. Informacje te są podane w słowniku $dDSN. Przypomnijmy, że początkowym założeniem było, iż baza danych nosi nazwę dbarticles i należy do użytkownika o nazwie admarticles, którego hasło to mdparticles. Przypomnijmy również, że ta aplikacja zezwala na dostęp wielu użytkownikom posiadającym różne uprawnienia. Istnieje tu niejasność, którą należy wyjaśnić. Połączenie jest rzeczywiście nawiązane pod tożsamością admarticles i ostateczniewłaśnie pod tą tożsamością będą wykonywane wszystkie operacje na bazie danych dbarticles, ponieważ jest to jedyna nazwa znana użytkownikowi SGBD MySQL, który posiada wystarczające uprawnienia do zarządzania bazą danych dbarticles. Aby „symulować” istnienie różnych użytkowników, użytkownik admarticles będzie działał z uprawnieniami użytkownika, którego login ($sUser) i hasło ($sMdp) zostały przekazane jako parametry do konstruktora. W ten sposób przed wykonaniem operacji na bazie artykułów należy sprawdzić, czy użytkownik ($sUser, $sMdp) posiada uprawnienia do jej wykonania. Jeśli tak, to operację wykona za niego użytkownik admarticles.
  • Nazwa użytkownika i hasło administratora bazy artykułów muszą zostać przekazane do konstruktora. Jest to rozsądny środek ostrożności. Gdyby te dwie informacje zostały „na stałe” zapisane w kodzie klasy, każdy użytkownik tej klasy mógłby z łatwością podszywać się pod administratora bazy artykułów. Klasa PHP nie jest bowiem chroniona. Również atrybut klasy $bAdmin, który wskazuje, czy użytkownik ($sUser, $sMdp), dla którego pracujemy, jest administratorem, czy nie, mógłby zostać ustawiony bezpośrednio z zewnątrz, tak jak w poniższym przykładzie:
$oArticles=new articles($dDSN,$sUser,$sMdp)
// tutaj $sUser został rozpoznany jako użytkownik niebędący administratorem bazy
$oArticle->bAdmin=TRUE;
// teraz $sUser został administratorem

PHP nie jest JAVA ani w języku C# a klasa PHP jest jedynie strukturą danych nieco bardziej zaawansowaną niż słownik, która jednak nie zapewnia bezpieczeństwa prawdziwej klasy, w której atrybut bAdmin zostałby zadeklarowany jako prywatny lub chroniony, uniemożliwiając jego modyfikację z zewnątrz. Ponieważ użytkownik klasy musi znać login i hasło administratora bazy artykułów, tylko ten ostatni może korzystać z klasy. Poprzednia operacja nie ma więc dla niego żadnego znaczenia. Klasa służy wyłącznie ułatwieniu mu programowania. Ważną konsekwencją jest to, że nie ma potrzeby stosowania środków bezpieczeństwa. Ponownie, osoba korzystająca z klasy articles jest z konieczności administratorem bazy artykułów.

  • Klasa obsługuje błędy połączenia z bazą danych lub wszelkie inne błędy w jednolity sposób, wypełniając atrybut $aErreurs jednym lub kilkoma komunikatami o błędach. Po każdej operacji użytkownik klasy musi zatem sprawdzić tę listę.
  • Metody addArticle, updateArticle, deleteArticle, selectArticles oraz execute wynikają bezpośrednio z przedstawionego wcześniej szablonu interfejsu internetowego. Odpowiadają one bowiem opcjom dostępnym w menu. Metody addArticle i modifyArticle opierają się na metodzie vérifierArticle w celu sprawdzenia, czy artykuł, który ma zostać dodany lub zmodyfikowany, zawiera poprawne dane. Podobnie metoda existeArticle pozwala sprawdzić, czy nie zamierza się dodać artykułu, który już istnieje. Można by zrezygnować z tej metody, gdybyśmy korzystali z tabeli artykułów, w której kod jest kluczem głównym. Wówczas to sama metoda SGBD zgłosi niepowodzenie dodania z powodu duplikatu. Prawdopodobnie poinformuje o tym za pomocą mało czytelnego komunikatu o błędzie w języku angielskim.
  • Artykuł przeznaczony do modyfikacji lub usunięcia będzie identyfikowany za pomocą unikalnego kodu. Metoda getCodes pozwala uzyskać wszystkie te kody.
  • Metoda disconnect zamyka połączenie z bazą danych, które zostało otwarte podczas tworzenia obiektu. Nie widać tutaj sensu stosowania metody connect, która ponownie nawiąże połączenie z bazą danych. Pozwoli to na dowolne otwieranie i zamykanie tego połączenia przy użyciu tego samego obiektu. Korzyść z tego wynika dopiero w połączeniu z aplikacją internetową. Aplikacja ta utworzy obiekt artykułów, który zostanie zapisany w sesji. Chociaż aplikacja ta będzie w stanie zachować większość atrybutów obiektu w trakcie kolejnych wymian danych między klientem a serwerem, nie jest jednak w stanie zachować atrybutu reprezentującego otwarte połączenie. Połączenie to będzie zatem musiało być ponownie otwierane przy każdej nowej wymianie danych między klientem a serwerem. Wymagane jest połączenie trwałe, aby otwarte połączenie zostało zapisane w puli połączeń i pozostawało otwarte na stałe. W ten sposób, gdy skrypt zażąda nowego połączenia, zostanie ono pobrane z puli połączeń. Osiągamy zatem ten sam rezultat, jak gdyby sesja mogła zapamiętać otwarte połączenie.
  • Metoda existeUser pozwala producentowi sprawdzić, czy użytkownik $sUser, zidentyfikowany hasłem $sMdp, rzeczywiście istnieje. Jeśli tak, metoda pozwala ustalić, czy jest on administratorem (co jest wskazane w tabeli USERS) i zapisuje tę informację w atrybucie $bAdmin. Jeśli nie jest administratorem, metoda pobiera jego uprawnienia z tabeli DROITS i umieszcza je w atrybucie $dDroits, który jest słownikiem z podwójnym indeksowaniem: $dDroits[$table][$droit] ma wartość „y', jeśli użytkownik $sUser posiada uprawnienie $droit do tabeli $table, a w przeciwnym razie przyjmuje wartość 'n'.

Należy napisać klasę „articles”. Dostęp do bazy danych będzie realizowany za pomocą biblioteki PEAR::DB, która pozwala na pominięcie dokładnego typu bazy.

7.5. Struktura aplikacji WEB

Teraz, gdy mamy już klasę „biznesową” do zarządzania bazą artykułów, możemy z niej korzystać w różnych środowiskach. Proponujemy tutaj wykorzystanie jej w aplikacji internetowej. Przyjrzyjmy się jej na przykładzie poniższych stron:

7.5.1. Typowa strona aplikacji

Wróćmy do przedstawionej już strony głównej:

1234

Image

Wszystkie strony aplikacji będą miały powyższą strukturę – tabelę o dwóch wierszach i trzech kolumnach, składającą się z czterech obszarów:

  • obszar 1 tworzy pierwszy wiersz tabeli. Jest on zarezerwowany na tytuł, ewentualnie wraz z obrazkiem. Trzy kolumny tego wiersza są tutaj połączone.
  • drugi wiersz ma trzy obszary, po jednym na kolumnę:
    • obszar 2 zawiera opcje menu. Zawiera on z kolei tabelę o jednej kolumnie i kilku wierszach. Opcje menu są umieszczone w wierszach tabeli.
    • obszar 3 jest pusty i służy wyłącznie do oddzielenia obszarów 2 i 4. Można było postąpić inaczej, aby uzyskać to oddzielenie.
    • Obszar 4 zawiera dynamiczną część strony. To właśnie ta część zmienia się w zależności od wykonywanej czynności, podczas gdy pozostałe pozostają niezmienne.

Skrypt PHP generujący ten szablon strony będzie nosił nazwę main.php i może wyglądać następująco:


<html>
  <head>
      <title>Gestion d'articles</title>
      <link type="text/css" href="<?php echo $dConfig['urlPageStyle'] ?>" rel="stylesheet" />
   </head>
  <body background="<?php echo $dConfig['urlBackGround'] ?>">
    <table>
      <tr height="60">
        <td colspan="3" align="left" valign="top" >
          <h1><?php echo $main["title"] ?></h1>
        </td>
      </tr>
      <tr>
        <td>
          <table>
            <tr>
              <td class="menutitle" >
                                    <a href="<?php echo  $main["liens"]["login"] ?>" ?>Authentification</a>
              </td>
            </tr>
            <tr>
                <td><br /></td>
            </tr>
            <tr>
              <td class="menutitle" >
                Utilisation
              </td>
            </tr>
            <tr height="10"></tr>
            <tr>
              <td class="menublock" >
                <img alt="-" src="../images/radio.gif" />
                <a href="<?php echo  $main["liens"]["addArticle"] ?>" ?>
                  Ajouter un article
                </a>
                 </td>
            </tr>
            <tr>
              <td class="menublock" >                    
                <img alt="-" src="../images/radio.gif" />
                <a href="<?php echo $main["liens"]["updateArticle"] ?>">
                  Modifier un article
                </a>
                    </td>
            </tr>
              <td class="menublock" >                
                <img alt="-" src="../images/radio.gif" />
                <a href="<?php echo $main["liens"]["deleteArticle"] ?>">
                  Supprimer un article
                </a>
              </td>
            </tr>
            <tr>
              <td class="menublock" >
                <img alt="-" src="../images/radio.gif" />
                <a href="<?php echo $main["liens"]["selectArticle"] ?>">
                  Lister des articles
                </a>
              </td>
            </tr>
            <tr>
                <td><br /></td>
            </tr>                
            <tr>
              <td class="menutitle" >
                Administration
              </td>
            </tr>
            <tr height="10"></tr>
            <tr>
              <td class="menublock" >                
                <img alt="-" src="../images/radio.gif" />
                <a href="<?php echo $main["liens"]["sql"] ?>" >
                  Requête SQL
                </a>
              </td>
            </tr>
          </table>
        </td>
            <td>  
            <img alt="/" src="../images/pix.gif" width="10" height="1" />
            </td>
            <td>
            <fieldset>
              <legend><?php echo $main["légende"] ?></legend>
            <?php
                include $main["contenu"];
            ?>
          </fieldset>
        </td>
      </tr>
    </table>
  </body>
</html>

Pola parametryczne strony zostały wyróżnione w powyższym wykazie. Strona szablonowa jest skonfigurowana na kilka sposobów:

  • za pomocą słownika $main zawierającego następujące klucze:
    • title: tytuł, który ma zostać umieszczony w polu 1 strony
    • linki: słowniki linków, które mają zostać wygenerowane w kolumnie menu. Linki te są powiązane z opcjami menu w polu 2
    • contenu: adres URL strony, która ma zostać wyświetlona w strefie 4
  • poprzez słownik $dConfig zawierający informacje pobrane z pliku konfiguracyjnego aplikacji o nazwie config.php
  • przez klasy należące do arkusza stylów używanego przez stronę:
      <link type="text/css" href="<?php echo $dConfig['urlPageStyle'] ?>" rel="stylesheet" />

Strona wykorzystuje tutaj następujące klasy stylów:

  • menutitle: dla głównej opcji menu
  • menublock: dla opcji podrzędnej menu

Zmiana jednego z parametrów powoduje zmianę wyglądu strony. Zatem zmiana $main['title'] spowoduje zmianę tytułu obszaru 1.

7.5.2. Typowy przebieg obsługi zapytania klienta

Klient wchodzi w interakcję z aplikacją za pomocą linków w obszarze 2 strony wzorcowej. Linki te będą miały następujący format:

apparticles.php?action=xx&phase=y&PHPSESSID=zzzzzzzzzzzz
action
oznacza bieżącą akcję spośród następujących:
authentifier
uwierzytelnianie klienta
selectArticles
wybór artykułów (przeglądanie)
updateArticle
zmiana pozycji
deleteArticle
usunięcie artykułu
sql
wysłanie dowolnego zapytania SQL (administrator)
phase
czynność może składać się z kilku etapów – oznacza bieżący etap
PHPSESSID
token sesji od momentu jej rozpoczęcia – umożliwia serwerowi odzyskanie informacji zapisanych w sesji podczas poprzednich wymian danych

Podobnie atrybut „action” w formularzach będzie miał tę samą postać. Na przykład na stronie głównej w obszarze 4 znajduje się formularz logowania. Tag HTML tego formularza jest zdefiniowany w następujący sposób:

<form name="frmLogin" method="post" action="apparticles.php?action=authentifier&phase=1">

Przetwarzanie żądania klienta odbywa się za pomocą głównego skryptu aplikacji o nazwie apparticles.php. Jego zadaniem jest skonstruowanie odpowiedzi dla klienta. Zawsze będzie postępował w ten sam sposób:

  • na podstawie nazwy akcji i bieżącej fazy przekieruje żądanie do wyspecjalizowanej funkcji. Funkcja ta przetworzy żądanie i wygeneruje odpowiednią stronę odpowiedzi. Dla każdego żądania klienta może istnieć kilka możliwych stron odpowiedzi: strona1, strona2, …, strona n. Strony te zawierają informacje, które muszą zostać obliczone przez funkcję. Są to zatem strony parametryczne. Zostaną one wygenerowane przez skrypty page1.php, page2.php, ..., pagen.php.
  • W celu zachowania spójności zmienne części stron, które mają być wyświetlane w obszarze 4 strony wzorcowej, również zostaną umieszczone w słowniku $main.

Załóżmy, że w odpowiedzi na żądanie serwer musi wysłać do klienta stronę pagex.php. Postąpi on w następujący sposób:

  • umieści w słowniku $main wartości niezbędne dla strony pagex.php
  • umieści w $main['contenu'], co oznacza URL strony do wyświetlenia w strefie 4 strony wzorcowej, URL z pagex.php
  • spowoduje wyświetlenie strony szablonowej za pomocą instrukcji
include "main.php";

Strona szablonowa zostanie wówczas wyświetlona z kodem skryptu pagex.php w polu 4, który zostanie przetworzony w celu wygenerowania zawartości tego pola. Należy pamiętać, że jest to zwykła komórka tabeli. Dlatego kod HTML wygenerowany przez pagex.php nie może zaczynać się od znaczników <HTML>, <HEAD>, <BODY>, ... Zostały one już wygenerowane na początku strony szablonu. Oto przykładowy wygląd skryptu login.php, który generuje obszar 4 strony głównej:


<form name="frmLogin" method="post" action="<?php echo $main["post"] ?>">
    <table>
        <tr>
            <td>login</td>
            <td><input type="text" value="<?php echo $main["login"] ?>" name="txtLogin" class="text"></td>
        </tr>
        <tr>
            <td>mot de passe</td>
            <td><input type="password" value="" name="txtMdp" class="text"></td>
      <td><input type="submit" value="Connexion" class="submit"></td>      
        </tr>
    </table>
</form>

Widać, że strona:

  • jest ograniczona do formularza
  • jest konfigurowana zarówno przez słownik $main, jak i arkusz stylów.

7.5.3. Plik konfiguracyjny

Zawsze warto jak najdokładniej skonfigurować aplikacje, aby uniknąć konieczności ingerencji w kod tylko dlatego, że zdecydowaliśmy się na przykład zmienić ścieżkę do skryptu lub obrazu. Główna aplikacja apparticles.php załaduje zatem plik konfiguracyjny config.php podczas uruchamiania:

     // ładowanie pliku konfiguracyjnego
  include "config.php";

W pliku tym umieścimy dyrektywy konfiguracyjne przeznaczone dla PHP oraz inicjalizacje zmiennych globalnych:

<?php

     // konfiguracja PHP
  ini_set("register_globals","off");
  ini_set("display_errors","off");
  ini_set("expose_php","off");
    ini_set("session.use_cookies","0");    // brak plików cookie

     // podstawowa konfiguracja artykułów
    $dConfig["DSN"]=array(
        "sgbd"=>"mysql",
        "admin"=>"admarticles",
        "mdpadmin"=>"mdparticles",
        "host"=>"localhost",
        "database"=>"dbarticles"
    );

   // adresy URL stron
    $dConfig['urlBackGround']="../images/standard.jpg";  
  $dConfig["urlPageStyle"]="mystyle.css";  
  $dConfig["urlAppArticles"]="apparticles.php";
  $dConfig["urlPageMain"]="main.php";
  $dConfig["urlPageLogin"]="login.php";
  $dConfig["urlPageErreurs"]="erreurs.php";
  $dConfig["urlPageInfos"]="infos.php";
  $dConfig["urlPageAddArticle"]="addarticle.php";
  $dConfig["urlPageUpdateArticle1"]="updatearticle1.php";
  $dConfig["urlPageUpdateArticle2"]="updatearticle2.php";
  $dConfig["urlPageDeleteArticle1"]="deletearticle1.php";
  $dConfig["urlPageDeleteArticle2"]="deletearticle2.php";
  $dConfig["urlPageSelectArticle1"]="selectarticle1.php";
  $dConfig["urlPageSelectArticle2"]="selectarticle2.php";
  $dConfig["urlPageSQL1"]="sql1.php";
  $dConfig["urlPageSQL2"]="sql2.php";
  $dConfig["urlPageSQL3"]="sql3.php";

   // linki na stronie głównej
  $main["liens"]["login"]="$sUrlAppArticles?action=authentifier&phase=0";  
  $main["liens"]["addArticle"]="$sUrlAppArticles?action=addArticle&phase=0";
  $main["liens"]["updateArticle"]="$sUrlAppArticles?action=updateArticle&phase=0";
  $main["liens"]["deleteArticle"]="$sUrlAppArticles?action=deleteArticle&phase=0";
  $main["liens"]["selectArticle"]="$sUrlAppArticles?action=selectArticle&phase=0";
  $main["liens"]["sql"]="$sUrlAppArticles?action=sql&phase=0";

   // zapisujemy $main w konfiguracji
  $dConfig["main"]=$main;    
?>

7.5.4. Arkusz stylów powiązany ze stroną wzorcową

Zauważyliśmy, że odpowiedź serwera miała jednolity format: main.php. Można było zauważyć, że skrypt ten generuje surową stronę pozbawioną efektów wizualnych. Jest to korzystne z kilku powodów:

  • programista nie musi martwić się o wygląd graficzny tworzonej przez siebie strony. Nie zawsze bowiem posiada umiejętności niezbędne do tworzenia atrakcyjnych graficznie stron. W ten sposób może w pełni skupić się na kodzie.
  • ułatwiona jest konserwacja skryptów. Gdyby zawierały one atrybuty prezentacyjne, ani struktura kodu, ani struktura prezentacji nie byłyby jasno widoczne. Aspekt graficzny stron jest często powierzany grafikowi. Ten prawdopodobnie nie chciałby szukać w skrypcie, którego nie rozumie, gdzie znajdują się atrybuty prezentacyjne, które musi zmodyfikować.

Należy jednak zadbać o wygląd graficzny stron. To właśnie on przyciąga internautów do witryny. W tym przypadku prezentacja jest przekazana do arkusza stylów. Strona main.php wskazuje w swoim kodzie arkusz stylów, którego należy użyć do jej wyświetlenia:

  <head>
      <title>Gestion d'articles</title>
      <link type="text/css" href="<?php echo $dConfig['urlPageStyle'] ?>" rel="stylesheet" />
   </head>

W niniejszym dokumencie zastosowano następujący arkusz stylów:

BODY {
    background : url(../images/standard.jpg);
    border : 2px none #FFDAB9;
    font-family : Garamond;
    font-size : 16px;
    margin-left : 0px;
    padding-left : 20px;
}

INPUT {
    background : #EEE8AA;
    border : 1px solid #EE82EE;
    font-family : Garamond;
    font-size : 18px;
}

INPUT.submit{
    font-family : "Times New Roman";
    font-size : 16px;
    background : #FA8072;
    border : 2px double Green;
    font-weight : bold;
    text-align : center;
    vertical-align : middle;
    cursor : pointer;
}

TD.menutitle{
    background-image : url(../images/menugelgd.gif);
    height : 23px;
    text-align : center;
    vertical-align : middle;
    background : url(../images/menugelgd.gif) no-repeat center;
}

TD.menublock{
    background : url(../images/bandegrismenugd.gif) repeat-x;
    text-align : left;
    vertical-align : middle;
}

A {
    font-family : "Comic Sans MS";
    color : #FF7F50;
    font-size : 15px;
    text-decoration : none;
}

A:HOVER {
    background : #FFA07A;
    color : Red;
}

FIELDSET {
    border : 1px solid #A0522D;
    background : #FFE4C4;
    margin : 10px 10px 10px 10px;
    padding-left : 10px;
    padding-right : 10px;
    padding-bottom : 10px;
}

LEGEND{
    background : #FFA500;
}

TH {
    background : #228B22;
    text-align : center;
    vertical-align : middle;
}

TD.libellé{
    border : 1px solid #008B8B;
    color : #339966;
}

H1 {
    font : bold 20px/30px Garamond;
    color : #FF7F50;
    background : #D1E1F8;
    background-attachment : fixed;
    text-align : center;
    vertical-align : middle;
    font-family : Garamond;
}

SELECT.TEXT {
    background : #6495ED;
    text-align : center;
    color : Aqua;
}

Nie będziemy zagłębiać się w szczegóły tego arkusza stylów. Przyjmiemy go takim, jakim jest. Nieco dalej zobaczymy, jak go stworzyć i zmodyfikować. Istnieją programy służące do tego celu. Niemniej jednak wskazujmy rolę atrybutów prezentacji użytych w arkuszu:

Atrybut:
reguluje wygląd tagu HTML:
BODY
<BODY>
H1
<H1> (Nagłówek 1)
A
<A> (Anchor)
A:HOVER
ustala atrybuty wyświetlania linku, gdy użytkownik najeżdża na niego kursorem myszy
FIELDSET
<FIELDSET> – ten tag nie jest rozpoznawany przez wszystkie przeglądarki
LEGEND
<LEGEND> – ten tag nie jest rozpoznawany przez wszystkie przeglądarki
INPUT
<INPUT>
INPUT.TEXT
<INPUT class="TEXT">
INPUT.SUBMIT
<INPUT class="SUBMIT">
TH
<TH> (Nagłówek tabeli)
TD.menutitle
<TD class="menutitle"> (Dane tabeli)
TD.menublock
<TD class="menublock">
TD.libellé
<TD class="libellé">

Zobaczmy na przykładzie, jak można zapisać te reguły formatowania. W tym przykładzie wykorzystamy oprogramowanie TopStyle Lite, dostępne bezpłatnie na stronie URL http://www.bradsoft.com. Po załadowaniu arkusza stylów pojawia się okno podzielone na trzy obszary:

  1. obszar edycji tekstu. Atrybuty formatowania można definiować ręcznie, pod warunkiem znajomości zasad pisania arkuszy stylów, które są zgodne ze standardem o nazwie CSS (Cascading Style Sheets).
  2. Obszar 2 przedstawia edytowalne właściwości aktualnie tworzonego atrybutu. Jest to najprostsza metoda. Pozwala ona uniknąć konieczności znajomości dokładnych nazw atrybutów prezentacyjnych, których jest bardzo wiele
  3. Obszar 3 przedstawia wygląd atrybutu, który jest właśnie tworzony

W obszarze 1 powyżej skopiujmy i wklejmy atrybut INPUT.submit do atrybutu INPUT.fantaisie. Ten atrybut określi wygląd tagu HTML <INPUT class="fantaisie">

Wykorzystajmy obszar 2, aby zmodyfikować niektóre właściwości atrybutu INPUT.fantaisie:

Od tej pory każdy tag <INPUT ... class="fantaisie"> znaleziony na stronie HTML powiązanej z poprzednim arkuszem stylów będzie wyświetlany tak, jak w przykładzie z obszaru 3 powyżej.

Arkusze stylów mają ogromne znaczenie. Ich zastosowanie pozwala zmienić „wygląd” aplikacji internetowej poprzez modyfikację tylko jednego elementu: arkusza stylów. Starsze przeglądarki nie obsługują arkuszy stylów. Poniższa dyrektywa <link ..> zostanie zignorowana przez niektóre z nich:

  <head>
      <title>Gestion d'articles</title>
      <link type="text/css" href="<?php echo $dConfig['urlPageStyle'] ?>" rel="stylesheet" />
   </head>

W naszej aplikacji spowoduje to wyświetlenie następującej strony głównej:

Image

Mamy tu minimalną stronę bez elementów graficznych. Mogło być gorzej. Niektóre wersje przeglądarek rozpoznają arkusze stylów, ale interpretują je nieprawidłowo. W rezultacie strona może być zniekształcona i bezużyteczna. Powstaje zatem kwestia typu przeglądarki klienckiej. Istnieją techniki, które pomagają określić typ przeglądarki klienckiej. Nie są one jednak w pełni niezawodne. Można wówczas napisać różne arkusze stylów dla różnych przeglądarek, a nawet stworzyć wersję bez arkusza stylów dla przeglądarek, które je ignorują. Oczywiście utrudnia to pracę programisty. Ten istotny problem został tutaj pominięty.

Dzięki arkuszom stylów możemy zapewnić użytkownikom naszej aplikacji spersonalizowane środowisko. Moglibyśmy wyświetlić im stronę z kilkoma możliwymi stylami prezentacji. Użytkownicy mogliby wybrać ten, który najbardziej im odpowiada. Wybór ten można by zapisać w bazie danych. Gdy użytkownik zaloguje się ponownie, aplikacja mogłaby uruchomić się z arkuszem stylów, który został przez niego wybrany.

7.5.5. Moduł wprowadzania danych w aplikacji

Klienci będą znali jedynie moduł wejściowy aplikacji: apparticles.php. Ogólny zarys jego działania jest następujący:

  • Żądanie klienta jest odbierane i analizowane. Może ono być skonfigurowane lub nie. Jeśli żądanie zawiera parametry, oczekiwane parametry są następujące: action=[action]&phase=[phase]&PHPSESSID=[PHPSESSID]
  • Jeśli żądanie nie zawiera parametrów lub jeśli pobrane parametry nie są zgodne z oczekiwanymi, serwer wysyła w odpowiedzi stronę uwierzytelniającą (login, hasło). Gdy tylko użytkownik poprawnie się zaloguje, tworzona jest sesja. Służy ona do przechowywania informacji podczas całej wymiany danych między klientem a serwerem.
  • Jeśli żądanie zostanie poprawnie rozpoznane, jest ono przetwarzane przez moduł zależny zarówno od akcji, jak i od bieżącej fazy.
  • Wszelki dostęp do bazy danych odbywa się za pośrednictwem klasy biznesowej articles.php.
  • Przetwarzanie żądania zawsze kończy się wysłaniem do klienta strony main.php, w której w pliku $main['contenu'] określonoURL strony, która ma zostać umieszczona w strefie 4 strony szablonowej.

Szkielet skryptu apparticles.php mógłby wyglądać następująco:

<?php
     // zarządzanie tabelą artykułów
  include "config.php";
  include "articles.php";  

  // działania, które należy podjąć
  $sAction=$_POST["action"] ? $_POST["action"] : $_GET["action"] ? $_GET["action"] : "authentifier";
  $sAction=strtolower($sAction);
   // ewentualna faza
  $sPhase=$_POST["phase"] ? $_POST["phase"] : $_GET["phase"] ? $_GET["phase"] : "0";

  // sesja
  session_start();
  $dSession=$_SESSION["session"];

     // czy trwa sesja?
  if(! isset($dSession)){
      // uwierzytelnianie użytkownika
    if($sAction=="authentifier" && $sPhase=="0") authentifier_0($dConfig);
    if($sAction=="authentifier" && $sPhase=="1") authentifier_1($dConfig);
    if($sAction=="authentifier" && $sPhase=="2") authentifier_2($dConfig);
     // nieprawidłowe żądanie
    authentifier_0($dConfig);        
  }//if – brak sesji

     // odzyskiwanie sesji
  $dSession=unserialize($dSession);

     // przetwarzanie żądania
     // ----- uwierzytelnianie
  if($sAction=="authentifier" && $sPhase=="0") authentifier_0($dConfig);
  if($sAction=="authentifier" && $sPhase=="1") authentifier_1($dConfig);
  if($sAction=="authentifier" && $sPhase=="2") authentifier_2($dConfig);  
     // ----- dodanie artykułu
  if($sAction=="addarticle" && $sPhase=="0") addArticle_0($dConfig,$dSession);
  if($sAction=="addarticle" && $sPhase=="1") addArticle_1($dConfig,$dSession);
  if($sAction=="addarticle" && $sPhase=="2") addArticle_2($dConfig,$dSession);
     // ----- aktualizacja produktu
  if($sAction=="updatearticle" && $sPhase=="0") updateArticle_0($dConfig,$dSession);
  if($sAction=="updatearticle" && $sPhase=="1") updateArticle_1($dConfig,$dSession);
  if($sAction=="updatearticle" && $sPhase=="2") updateArticle_2($dConfig,$dSession);
  if($sAction=="updatearticle" && $sPhase=="3") updateArticle_3($dConfig,$dSession);
     // ----- usunięcie artykułu
  if($sAction=="deletearticle" && $sPhase=="0") deleteArticle_0($dConfig,$dSession);
  if($sAction=="deletearticle" && $sPhase=="1") deleteArticle_1($dConfig,$dSession);
  if($sAction=="deletearticle" && $sPhase=="2") deleteArticle_2($dConfig,$dSession);
    // ----- przeglądanie artykułów
  if($sAction=="selectarticle" && $sPhase=="0") selectArticle_0($dConfig,$dSession);
  if($sAction=="selectarticle" && $sPhase=="1") selectArticle_1($dConfig,$dSession);
  if($sAction=="selectarticle" && $sPhase=="2") selectArticle_2($dConfig,$dSession);
     // ----- wysłanie zapytania SQL
  if($sAction=="sql" && $sPhase=="0") sql_0($dConfig,$dSession);
  if($sAction=="sql" && $sPhase=="1") sql_1($dConfig,$dSession);
  if($sAction=="sql" && $sPhase=="2") sql_2($dConfig,$dSession);


    // błędna operacja – wyświetlana jest strona uwierzytelniania
  session_destroy();
  authentifier_0($dConfig,"0");
...
?>

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

  • funkcje obsługujące konkretne żądanie klienta kończą się wygenerowaniem strony odpowiedzi oraz instrukcją exit, która kończy wykonywanie skryptu apparticles.php. Innymi słowy, z tych funkcji nie ma „powrotu”.
  • Funkcje przyjmują jeden lub dwa parametry:
    • $dConfig to słownik zawierający informacje pochodzące z pliku konfiguracyjnego config.php. Wszystkie funkcje korzystają z niego.
    • $dSession to słownik zawierający informacje o sesji. Istnieje on tylko wtedy, gdy sesja została utworzona, czyli po pomyślnym uwierzytelnieniu użytkownika. Dlatego funkcje uwierzytelniające nie posiadają tego parametru.

7.5.6. Strona błędów

Każda aplikacja musi umieć prawidłowo obsługiwać błędy, które mogą wystąpić. Aplikacje internetowe nie są tu wyjątkiem. W przypadku wystąpienia błędu umieścimy następującą stronę erreurs.php w strefie 4 strony szablonowej:

Les erreurs suivantes se sont produites :
<ul>
    <?php
        for($i=0;$i<count($main["erreurs"]);$i++){
            echo "<li>".$main["erreurs"][$i]."</li>\n";
        }//dla
    ?>
</ul>
<a href="<?php echo $main["href"] ?>"><?php echo $main["lien"] ?></a>

Zawiera listę błędów zdefiniowaną w $main['erreurs']. Ponadto może zawierać link powrotny, zazwyczaj do strony poprzedzającej stronę z błędami. Link ten zostanie zdefiniowany za pomocą etykiety $main['lien'] oraz URL $main['href']. Aby uniknąć wyświetlania tego linku, wystarczy wpisać pusty ciąg znaków w polu $main['lien']. Oto przykład strony błędów w przypadku nieprawidłowego logowania użytkownika:

Image

7.5.7. Strona informacyjna

Czasami chcemy przekazać użytkownikowi prostą informację, na przykład o pomyślnym zalogowaniu. W tym celu użyjemy następującej strony: infos.php:

<?php echo $main["infos"] ?>

Aby wyświetlić informację w odpowiedzi na żądanie klienta,

  • umieścić informację w $main['infos']
  • przeniesiemy URL z infos.php do $main['contenu']

Oto przykładowe dane zwracane po pomyślnym zalogowaniu się użytkownika:

Image

7.6. Działanie aplikacji

Mamy już teraz dobre wyobrażenie o ogólnej strukturze aplikacji, którą mamy napisać. Pozostaje nam jeszcze przedstawić ścieżki poruszania się użytkownika w aplikacji, czynności, które może on wykonać, oraz odpowiedzi, jakie otrzymuje od serwera. Gdy to zrobimy, będziemy mogli napisać funkcje obsługujące różne żądania klienta. W dalszej części przedstawimy działanie aplikacji na przykładzie stron wyświetlanych użytkownikowi w odpowiedzi na niektóre z tych działań. Za każdym razem podamy następujące informacje:

action utilisateur
początkowa akcja użytkownika, która doprowadziła do wyświetlenia odpowiedzi
paramètres envoyés
parametry wysłane przez przeglądarkę klienta do serwera w odpowiedzi na ręczną akcję użytkownika
page réponse
skrypt generujący sekcję 4 strony szablonowej

7.6.1. Uwierzytelnianie

Zanim użytkownik będzie mógł korzystać z aplikacji, będzie musiał się zalogować za pomocą następującej strony:

Image

action utilisateur
1 – początkowe żądanie dotyczące URL apparticles.php
2 – skorzystanie z opcji „Uwierzytelnianie” w menu
3 – bezpośrednie wywołanie funkcji URL articles.php z błędnymi parametrami
paramètres envoyés
1 – brak parametrów
2 – action=authentifier?phase=0
3 – lista błędnych parametrów
page réponse
login.php

Na stronie głównej link [Ajouter un article] ma następującą postać: action=addarticle?phase=0. Pozostałe linki mają tę samą postać, z action=(authentifier, updatearticle, deletearticle, selectarticle, sql). Użytkownik wypełnia formularz i korzysta z przycisku [Connexion]:

Image

Odpowiedź wygląda następująco:

Image

action utilisateur
bouton [Connexion]
paramètres envoyés
action=authentifier?phase=1
page réponse
infos.php

Tytuł strony został zmieniony, aby wskazywał login użytkownika oraz jego uprawnienia administratora/użytkownika. Ponadto wszystkie linki w strefie 2 zostały zmodyfikowane, aby odzwierciedlały fakt rozpoczęcia sesji. Dodano do nich parametr PHPSESSID=[PHPSESSID].

Jeśli serwer nie zdołał zidentyfikować klienta, otrzyma on inną odpowiedź:

Image

action utilisateur
bouton [Connexion]
paramètres envoyés
action=authentifier?phase=1
page réponse
erreurs.php

Link [Retour à la page de login] jest linkiem do strony URL apparticles.php?action=authentifier&phase=2&txtLogin=x. Link ten przekierowuje klienta z powrotem na stronę logowania, gdzie pole logowania jest wypełnione wartością parametru txtLogin:

Image

action utilisateur
lien [Retour à la page de login]
paramètres envoyés
action=authentifier?phase=2&txtLogin=x
page réponse
login.php

7.6.2. Dodawanie artykułu

Link z menu [Ajouter un article] przekierowuje do następującej strony w strefie 4 szablonu:

Image

action utilisateur
lien [Ajouter un article]
paramètres envoyés
action=addArticle?phase=0&PHPSESSID=[PHPSESSID]
page réponse
addarticle.php

Użytkownik wypełnia pola i wysyła dane do serwera za pomocą przycisku [Ajouter], który jest typu submit. Po stronie klienta nie odbywa się żadna weryfikacja. Weryfikację przeprowadza serwer. W odpowiedzi może on wysłać stronę z komunikatami o błędach, tak jak na poniższym przykładzie:

Żądanie
Odpowiedź
action utilisateur
bouton [Ajouter]
paramètres envoyés
action=addArticle?phase=1&PHPSESSID=[PHPSESSID]
page réponse
erreurs.php

Link [Retour à la page d'ajout d'article] pozwala powrócić do strony wprowadzania danych:

Wniosek
Odpowiedź
action utilisateur
lien [Retour à la page d'ajout d'article]
paramètres envoyés
action=addArticle?phase=2&PHPSESSID=[PHPSESSID]
page réponse
article.php

Jeśli dodanie przebiegnie bezbłędnie, użytkownik otrzyma komunikat potwierdzający:

Żądanie
Odpowiedź
action utilisateur
bouton [Ajouter]
paramètres envoyés
action=addArticle?phase=1&PHPSESSID=[PHPSESSID]
page réponse
infos.php

7.6.3. Przeglądanie artykułów

Link z menu [Lister des articles] przekierowuje do następującej strony w strefie 4 szablonu:

Image

action utilisateur
link w menu [Lister des articles]
paramètres envoyés
action=selectArticle?phase=0&PHPSESSID=[PHPSESSID]
page réponse
select1.php

Zapytanie select [colonnes] from articles where [where] order by [orderby] zostanie wysłane do tabeli artykułów, gdzie [colonnes], [where] oraz [orderby] stanowią zawartość powyższych pól. Na przykład:

Zapytanie
Odpowiedź
action utilisateur
bouton [Afficher]
paramètres envoyés
action=selectArticle?phase=1&PHPSESSID=[PHPSESSID]
page réponse
select2.php

Żądanie może być nieprawidłowe, w którym to przypadku klient otrzymuje stronę z komunikatem o błędzie:

Żądanie
Odpowiedź

W obu przypadkach (niezależnie od tego, czy wystąpiły błędy, czy nie) link [Retour à la page de sélection d'articles] umożliwia powrót do strony select1.php:

Żądanie
Odpowiedź
action utilisateur
lien [Retour à la page de sélection d'articles]
paramètres envoyés
action=selectArticle?phase=2&PHPSESSID=[PHPSESSID]
page réponse
select1.php

7.6.4. Modyfikacja artykułów

Link w menu [Modifier un article] powoduje wyświetlenie następującej strony w obszarze 4 szablonu strony:

Image

action utilisateur
link w menu [Modifier un article]
paramètres envoyés
action=updateArticle?phase=0&PHPSESSID=[PHPSESSID]
page réponse
updatearticle1.php

Z listy rozwijanej wybieramy kod artykułu, który chcemy edytować, a następnie wpisujemy [OK], aby edytować artykuł o tym kodzie:

Zapytanie
Odpowiedź
action utilisateur
bouton [OK]
paramètres envoyés
action=updateArticle?phase=1&PHPSESSID=[PHPSESSID]
page réponse
updatearticle2.php

Po wyświetleniu karty artykułu, który ma zostać zmodyfikowany, użytkownik może wprowadzić zmiany:

Żądanie
Odpowiedź
action utilisateur
bouton [Modifier]
paramètres envoyés
action=updateArticle?phase=2&PHPSESSID=[PHPSESSID]
page réponse
infos.php

Użytkownik może popełnić błędy podczas edycji:

Zapytanie
Odpowiedź

Link [Retour à la page de modification d'article] pozwala powrócić do strony wprowadzania danych:

Image

action utilisateur
lien [Retour à la page de modification d'article]
paramètres envoyés
action=updateArticle?phase=3&PHPSESSID=[PHPSESSID]
page réponse
updatearticle2.php

7.6.5. Usunięcie artykułu

Link w menu [Supprimer un article] przekierowuje do następującej strony w strefie 4 szablonu:

Image

action utilisateur
link w menu [Supprimer un article]
paramètres envoyés
action=deleteArticle?phase=0&PHPSESSID=[PHPSESSID]
page réponse
deletearticle1.php

Użytkownik wybiera kod artykułu do usunięcia z listy rozwijanej:

Żądanie
Odpowiedź
action utilisateur
bouton [OK]
paramètres envoyés
action=deleteArticle?phase=1&PHPSESSID=[PHPSESSID]
page réponse
deletearticle2.php

Użytkownik potwierdza usunięcie artykułu za pomocą przycisku [Supprimer]:

Żądanie
Odpowiedź
action utilisateur
bouton [Supprimer]
paramètres envoyés
action=deleteArticle?phase=2&PHPSESSID=[PHPSESSID]
page réponse
infos.php

7.6.6. Wysyłanie zapytań administratora

Link w menu [Requête SQL] przekierowuje do następującej strony w strefie 4 strony szablonowej:

Image

action utilisateur
link w menu [Requête SQL]
paramètres envoyés
action=sql?phase=0&PHPSESSID=[PHPSESSID]
page réponse
sql1.php

Wpisujemy tekst zapytania SQL w polu wprowadzania danych i używamy przycisku [Exécuter], aby je wykonać. Tylko administrator może wysyłać takie zapytania, jak pokazuje poniższy przykład:

Zapytanie
Odpowiedź
action utilisateur
bouton [Exécuter]
paramètres envoyés
action=sql?phase=1&PHPSESSID=[PHPSESSID]
page réponse
erreurs.php

Link [Retour à la page d'émission de requêtes SQL] pozwala powrócić do strony wprowadzania danych:

Image

action utilisateur
lien [Retour à la page d'émission de requêtes SQL]
paramètres envoyés
action=sql?phase=2&PHPSESSID=[PHPSESSID]
page réponse
sql1.php

Jeśli użytkownik jest administratorem, a zapytanie jest poprawne pod względem składniowym:

Żądanie

otrzymujemy wynik zapytania:

Odpowiedź
action utilisateur
bouton [Exécuter]
paramètres envoyés
action=sql?phase=1&PHPSESSID=[PHPSESSID]
page réponse
sql2.php

Można wysyłać zapytania dotyczące aktualizacji tabel:

Żądanie
Odpowiedź
action utilisateur
bouton [Exécuter]
paramètres envoyés
action=sql?phase=1&PHPSESSID=[PHPSESSID]
page réponse
infos.php

7.6.7. Zadania do wykonania

Napisać skrypty i funkcje niezbędne dla aplikacji:

identyfikator
typ
rola
apparticles.php
skrypt
punkt wejścia do przetwarzania wniosków klientów
authentifier_0
funkcja
przetwarza zgłoszenie o parametrach action=authentifier&phase=0
authentifier_1
funkcja
przetwarza żądanie z parametrami action=authentifier&phase=1
authentifier_2
funkcja
przetwarza żądanie z parametrami action=authentifier&phase=2
addarticle_0
funkcja
przetwarza żądanie z parametrami action=addArticle&phase=0
addarticle_1
funkcja
przetwarza żądanie o parametrach action=addArticle&phase=1
addarticle_2
funkcja
przetwarza żądanie o parametrach action=addArticle&phase=2
updatearticle_0
funkcja
przetwarza żądanie o parametrach action=updatearticle&phase=0
updatearticle_1
funkcja
przetwarza żądanie z parametrami action=updatearticle&phase=1
updatearticle_2
funkcja
przetwarza żądanie z parametrami action=updatearticle&phase=2
updatearticle_3
funkcja
przetwarza żądanie z parametrami action=updatearticle&phase=3
deletearticle_0
funkcja
przetwarza żądanie z parametrami action=deletearticle&phase=0
deletearticle_1
funkcja
przetwarza żądanie z parametrami action=deletearticle&phase=1
deletearticle_2
funkcja
przetwarza żądanie z parametrami action=deletearticle&phase=2
selectarticle_0
funkcja
przetwarza żądanie z parametrami action=selectarticle&phase=0
selectarticle_1
funkcja
przetwarza żądanie z parametrami action=selectarticle&phase=1
selectarticle_2
funkcja
przetwarza żądanie z parametrami action=selectarticle&phase=2
sql_0
funkcja
przetwarza żądanie z parametrami action=sql&phase=0
sql_1
funkcja
przetwarza żądanie z parametrami action=sql&phase=1
sql_2
funkcja
przetwarza żądanie z parametrami action=sql&phase=2
main.php
skrypt
generuje stronę typu
login.php
skrypt
generuje stronę logowania
erreurs.php
skrypt
generuje stronę błędów
infos.php
skrypt
generuje stronę z informacjami
addarticle.php
skrypt
generuje stronę dodawania artykułu
updatearticle1.php
skrypt
generuje stronę 1 edycji artykułu
updatearticle2.php
skrypt
generuje stronę 2 edycji artykułu
deletearticle1.php
skrypt
generuje stronę 1 usunięcia artykułu
deletearticle2.php
skrypt
generuje stronę 2 procesu usuwania artykułu
select1.php
skrypt
generuje stronę 1 wyboru artykułów
select2.php
skrypt
generuje stronę 2 z listy artykułów
sql1.php
skrypt
generuje stronę 1 wysyłania zapytań
sql2.php
skrypt
generuje stronę 2 wysyłania zapytań

7.7. Rozwój aplikacji

W tym momencie mamy aplikację, która działa zgodnie z przeznaczeniem i charakteryzuje się akceptowalną ergonomią. Zamierzamy ją ulepszyć w kilku aspektach:

  • SGBD
  • jej bezpieczeństwo
  • wygląd
  • jej wydajność

7.7.1. Zmiana typu bazy danych

W naszym badaniu przyjęliśmy, że używany plik SGBD to MySQL. Zmień na SGBD i pokaż, że jedyna zmiana, jaką należy wprowadzić, dotyczy definicji zmiennej $dDSN w pliku konfiguracyjnym config.php.

7.7.2. Zwiększanie bezpieczeństwa

Podczas tworzenia aplikacji internetowej nigdy nie należy zakładać, że klientem jest przeglądarka, a żądanie, które nam wysyła, jest kontrolowane przez formularz, który wysłaliśmy mu przed tym żądaniem. Każdy program może być klientem aplikacji internetowej i w związku z tym wysyłać do niej dowolne żądania, z parametrami lub bez. Aplikacja musi zatem wszystko sprawdzić.

Jeśli przyjrzymy się kodowi skryptu apparticles.php, zauważymy,

  • że bez sesji nie może nastąpić żadna czynność poza uwierzytelnieniem. Sesja ta istnieje tylko wtedy, gdy użytkownik pomyślnie się uwierzytelnił. Przypomnijmy, że sesja jest identyfikowana przez dość długi ciąg znaków zwany tokenem sesji, który ma następującą postać: 176a43609572907333118333edf6d1fb. Ten token można przesłać do aplikacji na różne sposoby, na przykład za pomocą skonfigurowanego parametru URL:

apparticles.php?PHPSESSID=176a43609572907333118333edf6d1fb. 

Program, który wielokrotnie wysyłałby poprzedni kod URL, losowo zmieniając token w nadziei na znalezienie właściwego, najprawdopodobniej potrzebowałby wielu dni na wygenerowanie prawidłowej kombinacji, ponieważ liczba możliwych kombinacji jest ogromna. Do tego czasu sesja, mając ograniczony czas trwania, najprawdopodobniej zostanie zakończona. Innym zagrożeniem jest to, że token, przesyłany w postaci niezaszyfrowanej przez sieć, może zostać przechwycony. Ryzyko to jest realne. Można zatem zastosować szyfrowane połączenie między serwerem a klientem.

  • tak, że po rozpoczęciu sesji dozwolone są tylko określone działania. Token URL skonfigurowany jako action=tricher&phase=0&PHPSESSID=[PHPSESSID] zostałby odrzucony, ponieważ akcja „tricher” nie jest dozwolona. Gdy parametry (action, phase) nie są rozpoznawane, nasza aplikacja wyświetla stronę uwierzytelniającą.

Aplikacja nie sprawdza jednak, czy dozwolone akcje następują po sobie w prawidłowej kolejności. Na przykład dwie poniższe akcje:

  1. action=addArticle&phase=0&PHPSESSID=[PHPSESSID]
  2. action=updateArticle&phase=1&PHPSESSID=[PHPSESSID]

są dwoma dozwolonymi akcjami. Jednak akcja 2 nie może następować po akcji 1.

Jak śledzić sekwencję żądań URL wysyłanych przez przeglądarkę klienta?

Można skorzystać z dwóch zmiennych PHP: $_SERVER['REQUEST_URI] oraz $_SERVER['HTTP_REFERER], które są dwiema informacjami wysyłanymi przez przeglądarki klienckie w ich nagłówkach HTTP.

$_SERVER['REQUEST_URI]: Jest to URI żądane przez klienta. Na przykład

/apparticles.php?action=addArticle&phase=0&PHPSESSID=[PHPSESSID]

$_SERVER['HTTP_REFERER]: Jest to plik URL, który był wyświetlany w przeglądarce przed nowym plikiem URL, o który właśnie prosi przeglądarka (poprzedni plik URI). Na przykład, jeśli przeglądarka, która wyświetliła wspomniany wcześniej plik URI, wysyła nowe żądanie do serwera, zmienna $_SERVER['HTTP_REFERER'] tego serwera będzie miała wartość

http://maszyna:port//apparticles.php?action=addArticle&phase=0&PHPSESSID=[PHPSESSID]

Aby sprawdzić, czy dwie akcje naszej aplikacji następują po sobie w odpowiedniej kolejności, można postępować w następujący sposób:

W ramach działania 1:

  • odnotowuje się żądany kod URI (URI1) i zapisuje go w sesji

W ramach czynności 2:

  • pobieramy kod HTTP-REFERER z czynności 2. Na tej podstawie wywodzi się kod URI (URI2) na podstawie kodu URL, który był wcześniej wyświetlany w przeglądarce wysyłającej żądanie.
  • Pobieramy URI i URI1, które były zapisane w sesji i które odpowiadają URI dla akcji wcześniej żądanej od serwera
  • Jeśli akcja 2 następuje po akcji 1, wówczas musi zachodzić równość URI2 = URI1. Gdyby tak nie było, odmówilibyśmy wykonania żądanej akcji i wyświetlilibyśmy stronę uwierzytelniającą.
  • W sesji zapisujemy identyfikatory URI i URI2 bieżącej akcji w celu weryfikacji następnej akcji. I tak dalej.

Oto przykład. Po uwierzytelnieniu wybieramy link [Ajouter un article]:

Image

Kod URL tej strony to:

http://localhost:81/st/php/articles/gestion/articles8/apparticles.php?action=addArticle&phase=0&PHPSESSID=006a63e6027f16c70b63cdae93405eeb

Bezpośrednio w polu [Adresse] przeglądarki zmieniamy URL w następujący sposób:

http://localhost:81/st/php/articles/gestion/articles8/apparticles.php?action=deleteArticle&phase=1&PHPSESSID=006a63e6027f16c70b63cdae93405eeb

W ten sposób wyświetla się strona uwierzytelniania:

Image

Warto to wyjaśnić. Gdy żądamy URL, wpisując bezpośrednio identyfikator w polu adresu przeglądarki, przeglądarka nie wysyła nagłówka HTTP_REFERER. Nasza aplikacja nie odnajduje zatem identyfikatora URI z poprzedniej akcji, URI, który zapamiętała w sesji. W odpowiedzi zwraca więc stronę uwierzytelniającą.

Mechanizm ten jest skuteczny w przypadku przeglądarek, ale zupełnie nie sprawdza się w przypadku klienta programowego. Klient ten może wysłać dowolny nagłówek HTTP_REFERER. Może zatem „oszukiwać”, twierdząc, że przeszedł przez dany etap, podczas gdy w rzeczywistości tego nie zrobił. Należy zatem upewnić się, że kolejność etapów jest zachowana. Jeśli więc żądana akcja to action=addArticle&phase=1 (wprowadzanie danych), to poprzednią akcją musi być koniecznie action=deleteArticle&phase=0 (pierwsze wywołanie strony wprowadzania danych) lub action=addArticle&phase=2 (powrót do wprowadzania danych po błędnym dodaniu). Podobnie, jeśli żądana akcja to action=addArticle&phase=2 (dodawanie), to poprzednia akcja musi być action=addArticle&phase=1 (wprowadzanie danych). Można wymusić na użytkowniku przestrzeganie tych sekwencji.

Podczas gdy pierwszy mechanizm ma charakter ogólny i może być stosowany w każdej aplikacji, drugi wymaga specyficznego kodowania dla każdej aplikacji i jest bardziej pracochłonny: konieczne jest przeanalizowanie wszystkich możliwych działań użytkownika i ich sekwencji. Sekwencje te można zapisać w słowniku, jak pokazuje poniższy kod:

  // uwierzytelnianie
  $dPrec['authentifier']['0']=array();
  $dPrec['authentifier']['1']=array(
         array('action'=>'authentifier','phase'=>'0'),
    array('action'=>'authentifier','phase'=>'2')
  );
  $dPrec['authentifier']['2']=array(
         array('action'=>'authentifier','phase'=>'1'),
  );

   // dodanie artykułu
  $dPrec['addarticle']['0']=array();  
  $dPrec['addarticle']['1']=array(
         array('action'=>'addarticle','phase'=>'0'),
    array('action'=>'addarticle','phase'=>'2')
  );
  $dPrec['addarticle']['2']=array(
         array('action'=>'addarticle','phase'=>'1'),
  );

   // edycja artykułu
  $dPrec['updatearticle']['0']=array();  
  $dPrec['updatearticle']['1']=array(
         array('action'=>'updatearticle','phase'=>'0'),
  );
  $dPrec['updatearticle']['2']=array(
         array('action'=>'updatearticle','phase'=>'1'),
    array('action'=>'updatearticle','phase'=>'3')
  );
  $dPrec['updatearticle']['3']=array(
         array('action'=>'updatearticle','phase'=>'2'),
  );

   // usunięcie artykułu
  $dPrec['deletearticle']['0']=array();  
  $dPrec['deletearticle']['1']=array(
         array('action'=>'deletearticle','phase'=>'0'),
  );
  $dPrec['deletearticle']['2']=array(
         array('action'=>'deletearticle','phase'=>'1'),
  );

      // wybór artykułów
  $dPrec['selectarticle']['0']=array();  
  $dPrec['selectarticle']['1']=array(
         array('action'=>'selectarticle','phase'=>'0'),
    array('action'=>'selectarticle','phase'=>'2')
  );
  $dPrec['selectarticle']['2']=array(
         array('action'=>'selectarticle','phase'=>'1'),
  );

      // zapytanie administratora
  $dPrec['sql']['0']=array();  
  $dPrec['sql']['1']=array(
         array('action'=>'sql','phase'=>'0'),
    array('action'=>'sql','phase'=>'2')
  );
  $dPrec['sql']['2']=array(
         array('action'=>'sql','phase'=>'1'),
  );

$dPrec['action']['phase'] to tablica zawierająca działania, które mogą poprzedzać dane działanie, oraz fazy służące jako indeksy w słowniku. Te poprzedzające działania są również reprezentowane przez słownik z dwoma kluczami: „działanie” i „faza”. Jeśli danemu działaniu może poprzedzać dowolne działanie, wówczas $dPrec['action']['phase'] będzie pustą tablicą. Brak akcji w słowniku oznacza, że nie jest ona dozwolona. Rozważmy powyższą akcję „uwierzytelnij”:

  // uwierzytelnianie
  $dPrec['authentifier']['0']=array();
  $dPrec['authentifier']['1']=array(
         array('action'=>'authentifier','phase'=>'0'),
    array('action'=>'authentifier','phase'=>'2')
  );
  $dPrec['authentifier']['2']=array(
         array('action'=>'authentifier','phase'=>'1'),
  );

Powyższy kod oznacza, że przed akcją action=authentifier&phase=0 może występować dowolna akcja, że akcji action=authentifier&phase=1 może poprzedzać akcja action=authentifier&phase=0 lub action=authentifier&phase=2, a akcji action=authentifier&phase=2 może poprzedzać akcja action=authentifier&phase=1.

Napisz następującą funkcję:

  // ---------------------------------------------------------------
  function enchainementOK(&$dConfig, &$dSession, $sAction, $sPhase){
       // sprawdza, czy bieżąca akcja ($sAction, $sPhase) może nastąpić po poprzedniej akcji
         // zapisana w $dSession['précédent']
         // słownik dozwolonych sekwencji znajduje się w $dConfig['précédents']
         // zwraca TRUE, jeśli sekwencja jest możliwa, w przeciwnym razie zwraca FALSE
....

Ta funkcja pozwala aplikacji głównej sprawdzić, czy sekwencja działań jest poprawna:

<?php
     // zarządzanie tabelą artykułów
  include "config.php";
  include "articles.php";  

  // sesja
  session_start();
  $dSession=$_SESSION["session"];

   // działanie, które należy podjąć
  $sAction=$_POST["action"] ? $_POST["action"] : $_GET["action"] ? $_GET["action"] : "authentifier";
  $sAction=strtolower($sAction);
   // ewentualna faza działania
  $sPhase=$_POST["phase"] ? $_POST["phase"] : $_GET["phase"] ? $_GET["phase"] : "0";

     // czy trwa sesja?  
  if(! isset($dSession)){
      // uwierzytelnienie użytkownika
    if($sAction=="authentifier" && $sPhase=="0") authentifier_0($dConfig);
    if($sAction=="authentifier" && $sPhase=="1") authentifier_1($dConfig);   
    if($sAction=="authentifier" && $sPhase=="2") authentifier_2($dConfig);    
     // działanie nietypowe
    authentifier_0($dConfig);        
  }//if – brak sesji

     // odzyskiwanie sesji
  $dSession=unserialize($dSession);

     // czy sekwencja działań jest prawidłowa?
  if( ! enchainementOK($dConfig,$dSession,$sAction,$sPhase)){
     // nienormalna sekwencja
    authentifier_0($dConfig);        
  }//if

     // przetwarzanie akcji
  if($sAction=="authentifier"){
   if($sPhase=="0") authentifier_0($dConfig);  
   if($sPhase=="1") authentifier_1($dConfig);  
   if($sPhase=="2") authentifier_2($dConfig);  
  }//if     
  if($sAction=="addarticle"){
...

7.7.3. Rozwój „wyglądu”

Pamiętajmy, że jednym z warunków postawionych podczas projektowania tej aplikacji była jej skalowalność. Załóżmy, że po kilku tygodniach okaże się, że należy poprawić ergonomię aplikacji. Zmodyfikuj aplikację w taki sposób, aby zmienić strukturę i wygląd strony szablonowej. Zmiany zostaną wprowadzone w dwóch miejscach:

  • w skrypcie main.php, który definiuje strukturę strony wzorcowej. Zmodyfikuj tę strukturę.
  • w arkuszu stylów, który określa „wygląd” aplikacji. Należy go zmienić.

7.7.4. Poprawa wydajności

Na razie zdecydowaliśmy się na lekką przeglądarkę kliencką: zajmuje się ona wyłącznie prezentacją. Można sprawić, by wykonywała ona przetwarzanie, dołączając skrypty do wysyłanych do niej stron internetowych. Mogą one być napisane w różnych językach, w szczególności w VBScript i JavaScript. Internet Explorer i Netscape dominują na rynku przeglądarek w proporcji zbliżonej do 60/40. Ponadto IE istnieje wyłącznie w środowisku Windows, a nie np. w systemie Unix, gdzie dominuje Netscape. Netscape nie wykonuje natywnie skryptów VBScript, podczas gdy obie przeglądarki wykonują skrypty JavaScript. Ponieważ Netscape nadal zajmuje znaczący udział w rynku przeglądarek, należy unikać skryptów VBScript. Dlatego w skryptach po stronie klienta zazwyczaj stosuje się JavaScript.

Do skryptów po stronie klienta przekazywane są operacje, w których serwer nie musi brać udziału. W naszej aplikacji dobrze byłoby, gdyby przeglądarka klienta wysyłała żądanie do serwera dopiero po jego sprawdzeniu. Nie ma więc sensu wysyłać do serwera żądania uwierzytelnienia, jeśli użytkownik pozostawił pole [login] puste w formularzu uwierzytelniającym. Lepiej jest poinformować użytkownika, że jego żądanie jest nieprawidłowe:

Image

Należy zauważyć, że nie uniemożliwi to serwerowi sprawdzenia, czy pole „login” nie jest puste, ponieważ klientem niekoniecznie jest przeglądarka, a zatem poprzednia weryfikacja mogła nie zostać przeprowadzona. Zakładanie, że klientem jest przeglądarka, stanowi poważne zagrożenie dla bezpieczeństwa aplikacji.

Przeanalizuj różne momenty, w których przeglądarka wysyła informacje do serwera, oraz sytuacje, w których można je zweryfikować, a następnie napisz jedną lub kilka funkcji JavaScript, które pozwolą przeglądarce sprawdzić poprawność informacji przed ich wysłaniem do serwera.

Wracając do poprzedniego przykładu, skrypt login.php, który generuje stronę uwierzytelniającą, przyjmuje następującą postać:


<script language="javascript">
    function check(){
       // sprawdzamy, czy rzeczywiście nastąpiło zalogowanie
    with(document.frmLogin){
        champs=/^\s*$/.exec(txtLogin.value);
      if(champs!=null){
          // brak loginu
        alert("Vous n'avez pas indiqué de login");
        txtLogin.focus();
        return;
      }//if
       // dane są dostępne – wysyłamy je na serwer
      submit();
    }//z
  }//sprawdź
</script>   
    
<form name="frmLogin" method="post" action="<?php echo $main["post"] ?>">
    <table>
        <tr>
            <td>login</td>
            <td><input type="text" value="<?php echo $main["login"] ?>" name="txtLogin" class="text"></td>
        </tr>
        <tr>
            <td>mot de passe</td>
            <td><input type="password" value="" name="txtMdp" class="text"></td>
      <td><input type="button" onclick="check()" value="Connexion" class="submit"></td>      
        </tr>
    </table>
</form>    

7.8. Więcej informacji

Na zakończenie przedstawiamy kilka pomysłów na pogłębienie tej analizy przypadku:

  • ciekawie byłoby sprawdzić, czy typowa strona tej aplikacji mogłaby stać się przedmiotem klasy. Klasa ta mogłaby być następnie wykorzystywana w innych aplikacjach.
  • Nasza aplikacja jest dobrze dostosowana do klientów typu przeglądarkowego, ale w mniejszym stopniu do klientów typu „aplikacja autonomiczna”. Te ostatnie muszą:
    • nawiązać połączenie TCP z serwerem
    • „komunikować się” z nim za pomocą kodu HTTP
    • analizować jego odpowiedzi HTML w celu odnalezienia pożądanych informacji, ponieważ klient autonomiczny prawdopodobnie nie będzie zainteresowany kodem prezentacyjnym HTML przeznaczonym dla przeglądarek.

Byłoby interesujące, gdyby nasza aplikacja generowała kod XML zamiast HTML. Jej klienci mogliby wówczas być zarówno przeglądarkami (choć dość nowymi), jak i aplikacjami autonomicznymi. Te ostatnie nie miałyby żadnych trudności ze znalezieniem poszukiwanych informacji, ponieważ odpowiedź serwera XML nie zawierałaby żadnych informacji dotyczących prezentacji, a jedynie samą treść.

  • Z pewnością należałoby zwrócić uwagę na kwestię jednoczesnego dostępu do bazy artykułów. Należy wyjaśnić co najmniej dwie kwestie:
  1. czy kod SGBD używany przez aplikację prawidłowo obsługuje jednoczesny dostęp do tego samego artykułu? Na przykład, co się stanie, jeśli dwóch użytkowników edytuje ten sam artykuł w tym samym momencie (naciskają przycisk [Modifier] jednocześnie)? Zależy to prawdopodobnie od bazowego obiektu SGBD.
  2. Obecnie nasza aplikacja nie obsługuje jednoczesnego dostępu. Jednak baza danych powinna pozostać w spójnym stanie, choć można spodziewać się pewnych nieprzewidzianych sytuacji. Rozważmy następującą sekwencję zdarzeń:
      • użytkownik U1 przechodzi do edycji artykułu
      • użytkownik U2 przechodzi do usuwania tego samego artykułu nieco później
      • każda z tych dwóch czynności wymaga wymiany danych między klientem a serwerem. W zależności od stylu pracy każdego z użytkowników, użytkownik U2 może zakończyć swoją pracę przed użytkownikiem U1. Kiedy ten ostatni zakończy wprowadzanie zmian i zatwierdzi je za pomocą [Modifier], otrzyma w odpowiedzi stronę informacyjną, a komunikat SGBD poinformuje go, że [0 ligne(s) ont été modifiées], ponieważ strona, którą chciał edytować, została w międzyczasie usunięta. Użytkownik z pewnością będzie zaskoczony. Z punktu widzenia ergonomii lepszym rozwiązaniem byłoby wyświetlenie strony, która wyraźniej sygnalizowałaby ten błąd. Ponadto można by rozważyć zapewnienie użytkownikowi wyłącznego dostępu do artykułu, gdy tylko rozpocznie on jego aktualizację. Inny użytkownik próbujący aktualizować ten sam artykuł otrzymałby komunikat, że trwa już inna aktualizacja. Stworzy to problem, jeśli pierwszy użytkownik zwleka z zatwierdzeniem swojej aktualizacji: pozostali użytkownicy zostaną zablokowani. Należy znaleźć odpowiednie rozwiązania, które będą w dużej mierze zależały od możliwości używanego modułu SGBD. Na przykład Oracle ma większe możliwości w tym zakresie niż MySQL.