Najważniejsza zasada obowiązująca podczas tworzenia formularzy w serwisach WWW brzmi: nigdy nie ufaj internaucie i zawsze poddawaj jego dane kontroli. I jest to szczera prawda. Nigdy nie jesteś pewien, czy zadania wypełnienia formularza podejmie się zwyczajny internauta, który zastosuje się do polecenia "Wprowadź liczbę z zakresu 1-29384", czy też może zwyczajny debil. Dlatego każdy, kto poważnie podchodzi do projektowania serwisów WWW, powinien znaleźć i używać dobrego sposobu na kontrolę tychże danych. W tym artykule mam zamiar opisać jeden z nich.
Czym jest maper
Maper jest w zasadzie zwyczajną klasą, przez którą przechodzą wszystkie dane wejściowe. Programista wypełnia, jakie pola chciałby pobrać z tablic
$_POST,
$_GET itd., a także ustawia specjalne flagi definiujące, co ma się w nich znajdować. Jeżeli zawartość pola będzie się zgadzać z tym, co podał, zostanie ono dodane do wewnętrznej tablicy już zmapowanych danych, skąd będzie można je bezpiecznie pobrać.
Tak więc do właściwego miejsca, z któego skrypt będzie czerpać dane, trafiać będą tylko te pola, które spełniają nasze wymogi, i żadne inne. Ponadto maper może nawet poddać ich zawartość wstępnej obróbce: dokonać konwersji typów czy uciąć niepotrzebne spacje!
W opisie wszystko wygląda pięknie. Niestety w praktyce kod źródłowy takiego mapera jest dość zagmatwany, dlatego należy dokładnie się przyłożyć do pracy. Za to rezultat końcowy jest wręcz rewelacyjny - sam używam maperów od pewnego już czasu i jestem w pełni zadowolony, iż tak wiele pracy wykonuję praktycznie jednym poleceniem!
Problemy
Najważniejszym problemem przy pisaniu mapera w PHP jest sama natura tego języka. Zapewne wiadomo Ci, że zmienne mogą dowolnie zmieniać swój typ, natomiast sam parser czasem może nie przypisać takiego typu, jaki powinien. Dlatego nie możemy nigdy ufać wewnętrznym funkcjom:
is_string(),
is_float(),
is_integer(). To zmusza nas do skorzystania z pomocy innych narzędzi...
Po drugie, wiele flag określających typ danego pola nie może być mieszanych ze sobą! Tak jest na przykład z flagami typów - maper zgłupiałby, gdyby mu podać flagę np. "INTEGER | STRING" :). Ale jeżeli chcemy zapewnić sobie większą kontrolę, np. sprawdzenie, czy podana wartość mieści się w jakimś zakresie, także musimy zdefiniować do tego zestaw flag, w dodatku pobierający parametry! To już kompletnie wyklucza możliwość współistnienia ze sobą dwóch flag tego typu, jak bowiem odróżnić, które parametry należą do której flagi? Jednym z rozwiązań jest zapisanie wszystkich dodatkowych parametrów do tablicy i pobieranie ich z niej w sposób
$parametry[$i], gdzie
$i to iterator. Każde polecenie, które już przetworzyło swoje parametry, zwiększy go o ich ilość i tym samym problem zostaje rozwiązany. Jego implementację pozostawię już jednak Tobie.
Te wszystkie problemy muszą znaleźć odzwierciedlenie w naszym kodzie, jeżeli ma być on stabilny i maksymalnie "idiotoodporny".
Zaczynamy kodowanie
W artykule tym opiszę wyłącznie sposób budowy mapera dla formularzy, aczkolwiek bez problemu da się go przerobić na inne postacie - wystarczy pozmieniać tablice, z której będą zasysane dane i po sprawie.
Cały kod oparty jest o programowanie obiektowe z PHP 5, które pozwoli nam wykorzystać pewną swą ciekawą właściwość do pobierania już zmapowanych danych. Na początek lista stałych opisujących kolejne flagi:
<?php
define('FORM_INTEGER', 1);
define('FORM_FLOAT', 2);
define('FORM_NUMERIC', 4);
define('FORM_STRING', 8);
define('FORM_TEXT', 16);
define('FORM_PASSWORD', 32);
define('FORM_BOOLEAN', 64);
define('FORM_GREATHER_THAN', 128);
define('FORM_LOWER_THAN', 256);
define('FORM_SCOPE', 512);
define('FORM_COMPARE', 1024);
define('FORM_LENGTH_COMPARE', 2048);
define('FORM_NOTYPE', 4096);
define('FORM_REQUIRED', 8192); |
Kolejnym flagom przypisałem następujące po sobie potęgi dwójki. Dzięki temu każda z nich będzie zajmowała w liczbie osobny bit i tym samym łatwo będzie sprawdzić, czy jest ustawiona. Wśród nich mamy zarówno flagi opisujące typy danych (np.
FORM_STRING i
FORM_NUMERIC, jak i te do sprawdzenia poprawności informacji. Oto opis wszystkiego:
FORM_INTEGER
Liczba całkowita
FORM_FLOAT
Liczba zmiennoprzecinkowa (ułamek)
FORM_NUMERIC
Liczba całkowita albo ułamek
FORM_STRING
Ciąg o długości maks. 256 znaków, wśród którego musi znaleźć się przynajmniej jeden znak inny, niż cyfra.
FORM_TEXT
Wypracowanie albo coś w tym stylu. Przynajmniej jeden znak musi być inny, niż cyfra.
FORM_BOOLEAN
Typ logiczny. Dozwolone wartości: 1 lub 0.
FORM_NOTYPE
Typ nieistotny.
FORM_PASSWORD
Sprawdzanie, czy hasło zostało poprawnie potwierdzone. Jako parametru wymaga nazwy pola, z którego należy wziąć wersję do porównania.
FORM_GREATHER_THAN
Dla liczb: sprawdza, czy w polu wpisano większą wartość od podanej w parametrze.
Dla tekstu: sprawdza, czy tekst ma większą długość, niż podano w parametrze.
FORM_LOWER_THAN
Jak wyżej, ale mniejszą.
FORM_SCOPE
Dla liczb: sprawdza, czy podana liczba mieści się w zakresie określonym przez parametry.
Dla tekstu: sprawdza, czy długość tekstu mieści się w zakresie określonym przez parametry.
FORM_COMPARE
Porównuje liczbę/tekst z tym podanym w parametrze.
FORM_LENGTH_COMPARE
Porównuje długość tekstu z tą podaną w parametrze.
FORM_REQUIRED
Pole musi zostać wypełnione. |
Nasza klasa będzie zaczynać się tradycyjnie od nagłówka z polami. Jest on niewielki:
class mapper{
private $mapped_data;
public $mapping_ok = 1; |
Drugie pole będzie przechowywało całkowity stan mapowania (1 - gdy wszystkie aktualne dane zostały zmapowane; 0 - gdy coś poszło nie tak). W pierwszym natomiast będziemy tworzyli referencje do zmapowanych prawidłowo zasobów. Pobierać je stamtąd będzie metoda specjalna
__get():
public function __get($name){
if(isset($this->mapped_data[$name])){
return $this->mapped_data[$name];
}
return NULL;
} // end __get(); |
Dzięki temu do poszczególnych danych możemy się odwoływać w ten sposób:
$maper -> nazwa_zasobu zamiast czegoś w stylu
$maper -> mapped_data['nazwa_zasobu']. Jeśli dany element nie będzie istniał, zwracane jest
null.
Wszystkie flagi opisujące mapowanie elementu prześlemy jako liczbę. Trzeba z niej wyciągnąć sugerowany typ elementu (FORM_STRING, FORM_INTEGER itd.). Zajmie się tym ta metoda:
private function extract_type($name, $mapping, &$extracted_type){
if($mapping & FORM_INTEGER){
$extracted_type = FORM_INTEGER;
return ctype_digit($_POST[$name]);
}elseif($mapping & FORM_FLOAT){
$extracted_type = FORM_FLOAT;
return preg_match('/([0-9]*?)[\.,]([0-9]*?)/', $_POST[$name]);
}elseif($mapping & FORM_NUMERIC){
$extracted_type = FORM_NUMERIC;
return preg_match('/([0-9 \-]*?)([\.,][0-9]*?)?/', $_POST[$name]);
}elseif($mapping & FORM_STRING){
if(!ctype_digit($_POST[$name])){
$extracted_type = FORM_STRING;
return (strlen($_POST[$name]) < 256);
}
}elseif($mapping & FORM_TEXT){
if(!ctype_digit($_POST[$name])){
$extracted_type = FORM_TEXT;
return 1;
}
}elseif($mapping & FORM_BOOLEAN){
$extracted_type = FORM_BOOLEAN;
return preg_match('/(0|1)/', $_POST[$name]);
}elseif($mapping & FORM_NOTYPE){
$extracted_type = FORM_NOTYPE;
return 1;
}
return 0;
} // end extract_type(); |
Jak widać, dodatkowo sprawdzamy tutaj, czy dane są rzeczywiście zapisane w podanym przez nas typie. Czasami używam do tego wyrażeń regularnych Perla (
preg_match()), a czasami funkcji modułu
Character type, służącego do sprawdzania, czy w podanej zmiennej są same cyfry itp. Nie musisz się martwić o brak obsługi - od PHP 4.2 moduł ten jest domyślnie włączony, a od PHP 4.3 jest już wbudowany (my korzystamy tu z PHP 5 :)). Dostajemy z nim ciekawą funkcję -
ctype_digit(), która zwróci nam 1, jeśli podany ciąg zawiera wyłącznie cyfry. Pomoże nam ona w obsłudze flag
FORM_INTEGER,
FORM_TEXT oraz
FORM_STRING.
Oto właściwa metoda mapująca dane. Może ona przyjmować od 2 do 4 parametrów. Zaczynamy więc ją kodem do ich obsługi:
public function add_mapping($name, $type){
if(func_num_args() == 3){
$arg = func_get_arg(2);
}elseif(func_num_args() == 4){
$arg1 = func_get_arg(2);
$arg2 = func_get_arg(3);
} |
Nadmiarowe parametry wprowadziłem do zmiennych tymczasowych, gdyż (z tego, co się zdążyłem zorientować) funkcja do ich pobierania
func_get_arg() zaczyna się w przeciwnym przypadku dość dziwnie zachowywać :). Prawdopodobnie chodzi tu o ich specyficzne działanie - pobieranie parametrów, co wyklucza pewne możliwości użycia. Jeśli ktoś wie, o co z tym dokładnie chodzi, prosiłbym o kontakt.
Jako pierwszą obsłużymy flagę
FORM_REQUIRED, gdyż jeśli jakiś zasób nie będzie istnieć, choć musi, po co go będziemy próbowali bezskutecznie mapować?
if(!isset($_POST[$name]) && $type & FORM_REQUIRED){
$this -> mapping_ok = 0;
return 0;
} |
Kolejny etap to "rozpakowanie" typu zasobu przy pomocy napisanej wcześniej metody
extract_type():
if(isset($_POST[$name])){
if(!$this -> extract_type($name, $type, $extype)){
$this -> mapping_ok = 0;
return 0;
} |
Następnie mapujemy jedną z trzech flag -
FORM_SCOPE,
FORM_GREATHER_THAN oraz
FORM_LOWER_THAN. Zauważ, iż dokonujemy tu sprawdzenia typu i wyboru decyzji, czy porównywać wartość (dla liczb), czy jej długość (dla tekstów):
if($type & FORM_SCOPE){
if($extype == FORM_STRING || $extype == FORM_TEXT){
if(!($arg1 < strlen($_POST[$name]) && strlen($_POST[$name]) < $arg2)){
$this -> mapping_ok = 0;
return 0;
}
}else{
if(!($arg1 < $_POST[$name] && $_POST[$name] < $arg2)){
$this -> mapping_ok = 0;
return 0;
}
}
}elseif($type & FORM_GREATHER_THAN){
if($extype == FORM_STRING || $extype == FORM_TEXT){
if(!($arg < strlen($_POST[$name]))){
$this -> mapping_ok = 0;
return 0;
}
}else{
if(!($arg < $_POST[$name])){
$this -> mapping_ok = 0;
return 0;
}
}
}elseif($type & FORM_LOWER_THAN){
if($extype == FORM_STRING || $extype == FORM_TEXT){
if(!($arg > strlen($_POST[$name]))){
$this -> mapping_ok = 0;
return 0;
}
}else{
if(!($arg > $_POST[$name])){
$this -> mapping_ok = 0;
return 0;
}
}
} |
Kolejny krok to obsługa
FORM_PASSWORD. Z parametru
$arg pobieramy nazwę pola, z którym będziemy porównywać to aktualnie mapowane. Flaga ta nie działa tylko z polami
FORM_TEXT - wszystkie inne są dozwolone.
Dodam, iż tu właśnie objawia się pewna słabość tego mapera. Jeżeli nadamy tę flagę, nie możemy nadać innej, opisującej np. minimalną długość hasła, ponieważ obie współdzielą między sobą jeden z parametrów. Zadanie naprawienia tego pozostawiam tobie, a jedno z możliwych rozwiązań sugerowałem na początku tego tekstu.
if($type & FORM_PASSWORD && $extype != FORM_TEXT){
if($_POST[$name] != $_POST[$arg]){
$this -> mapping_ok = 0;
return 0;
}
} |
Na koniec zostawiliśmy sobie porównywanie wartości i ich długości:
if($type & FORM_COMPARE && $extype != FORM_TEXT && $extype != FORM_NOTYPE){
if($_POST[$name] != $arg){
$this -> mapping_ok = 0;
return 0;
}
}elseif($type & FORM_LENGTH_COMPARE && ($extype == FORM_TEXT || $extype != FORM_STRING)){
if(strlen($_POST[$name]) != $arg){
$this -> mapping_ok = 0;
return 0;
}
}
} |
Jeśli kod dotarł aż tutaj, to znak, że dany zasób przeszedł przez wszystkie kontrole - jakakolwiek nieprawidłowość bowiem przerywała działanie mapera poprzez wywołanie instrukcji
return 0;. Stąd też możemy bez przeszkód dodać referencję do zasobu do tablicy
$mapped_data oraz potwierdzić stan pola
$mapping_ok.
$this -> mapped_data[$name] = &$_POST[$name];
$this -> mapping_ok = $this->mapping_ok && 1;
return 1;
} // end add_mapping();
}
?> |
Zauważ, jak obsługiwane jest pole
$this->mapping_ok. Dzięki zastosowaniu operatora
&& zrobiłem to, co "normalnie" musiałbym robić przy użyciu instrukcji
if - jeśli pole to ma wartość 0, zostanie 0. Jeśli 1, to 1. W zasadzie ta instrukcja nie jest potrzebna, ale chciałem pokazać, iż czasami niektóre rzeczy można w niezwykły sposób upraszczać :). Zainteresowanych odsyłam do artykułu "Wyrażenia w PHP".
Przykłady
Czas pokazać działanie mapera w praktyce. Pierwszy przykład to prosty formularz proszący o podanie imienia, nazwiska, wieku oraz zainteresowań. Trzy pierwsze pola muszą zostać wypełnione, dwa z nich muszą zawierać ciąg tekstowy, jedno liczbę. Ostatnie z pól powinno być zapełnione wypracowaniem. Nasz maper idealnie nadaje się do takiego celu. Proszę popatrzeć:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Mapper test 1</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-2"/>
</head>
<body>
<table width="60%">
<form method="post" action="przyklad1.php?co=przetworz">
<tr>
<td width="40%">Imię *:</td>
<td width="60%"><input type="text" name="imie" value="<?=$_POST['imie']?>"/></td>
</tr>
<tr>
<td width="40%">Nazwisko *:</td>
<td width="60%"><input type="text" name="nazwisko" value="<?=$_POST['nazwisko']?>"/></td>
</tr>
<tr>
<td width="40%">Wiek *:</td>
<td width="60%"><input type="text" name="wiek" value="<?=$_POST['wiek']?>"/></td>
</tr>
<tr>
<td width="40%">Zainteresowania:</td>
<td width="60%"><textarea name="zainteresowania" rows="6" cols="40"><?=$_POST['zainteresowania']?></textarea></td>
</tr>
<tr>
<td width="100%" colspan="2"><input type="submit" value="Wyślij"/></td>
</tr>
</form>
</table>
* - pola wymagają wypełnienia.<br/>
<?php
if($_GET['co'] == 'przetworz'){
require('./maper.php');
$maper = new mapper;
if(!$maper -> add_mapping('imie', FORM_STRING | FORM_REQUIRED | FORM_GREATHER_THAN, 2)){
echo '<font color="red">Źle wypełnione pole `imię`!</font><br/>';
}
if(!$maper -> add_mapping('nazwisko', FORM_STRING | FORM_REQUIRED | FORM_GREATHER_THAN, 2)){
echo '<font color="red">Źle wypełnione pole `nazwisko`!</font><br/>';
}
if(!$maper -> add_mapping('wiek', FORM_INTEGER | FORM_REQUIRED | FORM_SCOPE, 10, 99)){
echo '<font color="red">Źle wypełnione pole `wiek`!</font><br/>';
}
$maper -> add_mapping('zainteresowania', FORM_TEXT);
if(!$maper -> mapping_ok){
echo '<font color="red">Formularz został niewłaściwie wypełniony.</font><br/>';
}else{
echo 'Twoje imię to '.$maper->imie.'<br/>';
echo 'Twoje nazwisko to '.$maper->nazwisko.'<br/>';
echo 'Twój wiek to '.$maper->wiek.' lat<br/>';
if($maper->zainteresowania != NULL){
echo 'Twoje zainteresowania to: <i>'.$maper->zainteresowania.'</i><br/>';
}
}
}
?>
</body>
</html> |
Najpierw dołączamy klasę mapera i tworzymy jego obiekt. Zauważ, iż każde z mapowanych pól możemy obsłużyć na dwa sposoby. Pierwszy to wrzucenie wywołania metody
add_mapping() do instrukcji IF. Wtedy będziemy wiedzieli, na którym polu coś nawaliło i dokładnie wskażemy to internaucie. Drugi sposób to sprawdzenie stanu pola
mapping_ok. Jeśli będzie ono równać się zeru, coś poszło nie tak, a co za tym idzie, internauta wprowadził złe dane.
Przyjrzyj się wywołaniu
add_mapping(), a w szczególności sposobowi łączenia ze sobą poszczególnych flag (tu również odsyłam do artykułu "Wyrażenia w PHP"). Jeśli flagi tego wymagają, podajemy od jednego do dwóch dodatkowych parametrów.
Pole "zainteresowania" nie jest wymagane, a więc musimy sprawdzić, czy internauta je wypełnił. Posłuży nam do tego to, o czym wspomniałem przy omawianiu metody
__get() obsługującej te właśnie wywołania. Jeśli coś nie jest stworzone, zwracamy wartość
null.
Drugi przykład koncentruje się na fladze
FORM_PASSWORD.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Maper test 2</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-2">
</head>
<body>
<table width="60%">
<form method="post" action="przyklad2.php?co=przetworz">
<tr>
<td width="40%">Nick:</td>
<td width="60%"><input type="text" name="nick" value="<?=$_POST['nick']?>"/></td>
</tr>
<tr>
<td width="40%">Hasło:</td>
<td width="60%"><input type="password" name="haslo"/></td>
</tr>
<tr>
<td width="40%">Powtórz hasło:</td>
<td width="60%"><input type="password" name="haslo2"/></td>
</tr>
<tr>
<td width="100%" colspan="2"><input type="submit" value="Wyślij"/></td>
</tr>
</form>
</table>
<?php
if($_GET['co'] == 'przetworz'){
require('./maper.php');
$maper = new mapper;
if(!$maper -> add_mapping('nick', FORM_STRING | FORM_REQUIRED | FORM_GREATHER_THAN, 5)){
echo '<font color="red">Źle wypełnione pole `nick`!</font><br/>';
}
if(!$maper -> add_mapping('haslo', FORM_STRING | FORM_REQUIRED | FORM_PASSWORD, 'haslo2')){
echo '<font color="red">Podane hasła nie zgadzają się ze sobą lub mają niewłaściwy format!</font><br/>';
}
if(!$maper -> mapping_ok){
echo '<font color="red">Formularz został niewłaściwie wypełniony.</font><br/>';
}else{
echo 'Twój nick to '.$maper->nick.'<br/>';
echo 'Twoje hasło to '.$maper->haslo.'<br/>';
}
}
?>
</body>
</html> |
Pole
nick mapujemy podobnie, jak w przykładzie poprzednim. Jeśli chodzi o hasła, tu wywołujemy
add_mapping() tylko na jednym z pól, a nazwę drugiego podajemy za opcjonalny parametr. W ten sposób dokonujemy całej kontroli.
Pobaw się tymi przykładami i spróbuj je samodzielnie rozszerzyć o kilka innych właściwości. Eksperymentuj z różnymi kombinacjami flag.
Zakończenie
Mapery to bardzo ciekawy sposób kontrolowania tego, co otrzymujemy z formularza. Niestety nie widziałem żadnego "gotowego" skryptu, na którym można by się wzorować. Całą powyższą metodę działania i inne duperele opracowałem sam, w wolnej chwili, podczas opracowywania kolejnego z moich dziwnych programistycznych wynalazków :). Muszę jednak stwierdzić, że sprawdza się znakomicie.
W ramach praktyki polecam Ci przerobić przedstawiony tu kod tak, aby obsługiwał dane z adresu URL. Spróbuj dodać do niego flagę
BASE64, która spowoduje uprzednie zdekodowanie danych przesłanych właśnie w formacie
Base64, bardzo dobrze sprawdzającym się w przypadku URL'i.
Na tym kończę ten artykuł. Miłego pisania!