Autor: Tomasz Jędrzejewski
Data publikacji: 29.01.2005, 09:53 | Ostatnia modyfikacja: 19.11.2006, 17:42
W artykule opisany jest sposób na kontrolÄ™ poprawnoÅ›ci danych wejÅ›ciowych przychodzÄ…cych do skryptu PHP przy pomocy maperów.
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.
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órego 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!
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".
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".
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.
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!
Autor: Tomasz "Zyx" Jędrzejewski, www.zyxist.com
Waszym zdaniem:
shize :: 18.04.2007, 12:26 :: #88
Czy preg_match z FORM_NUMERIC i FORM_FLOAT nie zwróci 1 w przypadku kiedy oprócz poprawnego wyrażenia znajdzie się niepożądany znak?