Programy CGI dla baz danych 13 listopada 2007 1 WWW i bazy danych: programy CGI Uwaga: Zakładam minimalną znajomość HTML. Z wyjatkiem formularzy nie jest to dokumentacja HTML jest mnóstwo tutoriali w sieci. 1.1 Wprowadzenie Program CGI jest to program wywoływany przez serwer WWW. Program CGI otrzymuje dane od serwera i produkuje w wyniku stronę, wysyłaną przez serwer do przeglądarki. Interfejs używany przy komunikacji między takimi programami a serwerem WWW nosi nazwę CGI (Common Gateway Interface). Serwer WWW interpretuje komunikaty HTTP pochodzące z przeglądarki. Niektóre w tych komunikatów zawierają żądanie uruchomienia programu (CGI). Część komunikatu określa nazwę programu i jego położenie, a inna część jego argumenty. To, jakie adresy URL są traktowane jako programy CGI, zależy od serwera WWW i jego administratora. Czasem muszą one być zapisane w ustalonym katalogu, np. cgi-bin, a ich adres ma wtedy postać np. http://www.foo.org/cgi-bin/nazwa-programu. Takie programy często są uruchamiane z prawami użytkownika nobody, a więc nie wolno w nich używać ścieżek względnych. Model komunikacji oparty na WWW jest bardzo ograniczony: Ponieważ program CGI musi zakończyć pracę zanim serwer będzie mógł odpowiedzieć na żądanie, dialog o n interakcjach musi po stronie serwera być implementowany n niewielkimi programami. 1
Programy te muszą się jednak jakoś komunikować. Z uwagi na ograniczenia protokołu HTTP nie daje się użyć architektury Model- View-Controller (MVC), stosowanej w klasycznym GUI. Szczególne kłopoty sprawia możliwość klonowania stron w przeglądarce. Programy CGI działają jak typowe filtry w systemie Unix: wczytuja dane ze standardowego wejścia i wypisują wyniki na standardowe wyjscie. 2 Generowanie HTML z programów w Scheme Programy CGI mozna pisać również w Scheme. Najprostszy przykład to skrypt nie pobierający żadnych danych wejściowych. Powinien on wypisać na standardowe wyjście nagłówek HTTP, a następnie zawartość strony zapisaną w HTML. Nagłówek HTTP dla tekstu w HTML wypisuje się funkcją output-http-headers z biblioteki CGI z kolekcji NET. Zawartość strony można generować w różny sposób. Najprościej jest użyć standardowych funkcji wyjścia w Scheme. W pierwszym przykładzie wygenerujemy stronę z nieśmiertelnym komunikatem. (require (lib "cgi.ss" "net")) (output-http-headers) (printf "<html>~%") (printf "<title>pierwszy przykład</title>~%") (printf "<body><h1>witaj świecie!</h1></body>~%") (printf "</html>~%") Do wypisywania poszczególnych wierszy HTML użyliśmy funkcji printf. MzScheme zawiera dwie kolekcje ułatwiające generowanie stron: HTML (rzadko używaną bezpośrednio) i XML. Uwaga: DrScheme traktuje HTML jako XHTML (HTML zgodny z wymogami XML) i w zasadzie wymaga stron poprawnie zbudowanych. Kolekcje te operują X-wyrażeniami nawiasowaną notacją XML: (html (head (title "Pierwszy przykład")) 2
(body ((bgcolor "white")) (center "Pierwszy przykład") (p) (p "Witaj świecie!"))) W naszym drugim przykładzie użyjemy biblioteki XML. Umożliwia ona posługiwanie się X-wyrażeniami i automatyczną zamianę ich na postać XML lub wypisywanie. Korzystanie z niej wymaga umieszczenia w naszym programie wyrażenia (require (lib "xml.ss" "xml")) Do zamiany x-wyrażenia (reprezentującego element XML/HTML listą odpowiedniej postaci) na napis służy funkcja (xexpr->string x-wyrażenia) na przykład > (xexpr->string (html (body "witaj"))) "<html><body>witaj</body></html>" Do wypisania na port (np. na standardowe wyjście) używa się > (display-xml/content (xexpr->xml (html (body "hello")))) <html> <body> hello </body> </html> (chcąc wypisać do pliku należy to przekierować używając with-output-to-file). Zapiszemy teraz jeszcze raz podany przykład, ale używając poznanych konstrukcji: (require (lib "cgi.ss" "net")) (require (lib "xml.ss" "xml")) (output-http-headers) 3
(display-xml/content (xexpr->xml (html (title "Pierwszy przykład") (body (h1 "Witaj świecie!"))))) Na wyjściu powinniśmy otrzymać Content-type: text/html <html><title>pierwszy przykład</title> <body><h1>witaj świecie</h1></body> </html> Otrzymany program należy jeszcze przekształcić na kompletny samodzielny skrypt CGI. Tekst programu należy poprzedzić nagłówkiem wywołującym (używając ścieżki absolutnej) interpreter Scheme na serwerze WWW. Ponadto plik ze skryptem musi być wykonywalny. #! /bin/sh # exec /home/staff/teachers/zbyszek/bin/mzscheme -q -r "$0" "$@" # (require (lib "cgi.ss" "net")) (require (lib "xml.ss" "xml")) (output-http-headers) (display-xml/content (xexpr->xml (html (title "Pierwszy przykład") (body (h1 "Witaj świecie!"))))) 3 Czytanie XML Aby przeczytać element XML z pliku wystarczy użyć: 4
(with-input-from-file xml-file read-xml/element) czasem jednak otrzymuje się błąd: read-xml: parse-error: expected root element - received #<struct:pi> Oznacza to prawdopodobnie, że plik zawiera dokument XML, a nie element XML. Dokument XML zawiera wprawdzie root element, lecz poprzedzony linią w rodzaju <?xml version="1.0"?> W takiej sytuacji należy użyć read-xml (with-input-from-file xml-file read-xml) i funkcją document-element wydobyć root element. 4 Programowe generowanie stron Użycie X-wyrażeń pozwala na prostsze generowanie skomplikowanych stron. Popatrzmy na prostą pętlę zwiększającą licznik i wypisującą każdorazową wartość licznika: (display-xml/content (xexpr->xml (HTML (HEAD (TITLE "Ślad licznika")) (BODY,@(let loop ((n 0)) (if (> n 10) () (cons (P,(format "Bieżąca wartość n wynosi ~a" (number->string n)) (loop (+ n 1)))))))))) 5
Programy nie pobierające danych wejściowych są zwykle bezużyteczne. Z programami CGI sprawa ma się inaczej, ponieważ mogą one wyświetlać informację, która ulega częstym zmianom, na przykład pochodzić z bazy danych czy skrytki pocztowej użytkownika. Napiszemy teraz program podający, jacy użytkownicy są aktualnie zalogowani. Skorzystamy z programu finger Unixa, zamieniając jego wyniki na HTML. Programy Unixa uruchamia się w Scheme używając procedury process: > (process "/usr/bin/finger") (#<stream-input-port>...) Procedura ta tworzy nowy proces i zwraca listę elementów służących do komunikacji z nim. Pierwszym elementem tej listy jest port wejściowy, z którego można odczytać wyniki działania procesu finger. Program obróbki wyników działania programu finger wygląda następująco (define FINGER "/usr/bin/finger") (define (read-all ip) (let ((next (read-line ip))) (if (eof-object? next) () (cons next (read-all ip))))) (output-http-headers) (display-xml/content (xexpr->xml (html (title "Finger Gateway") (body ((bgcolor "white")) (p,(string->html (read-line (car (process "date"))))),@(map (lambda (x) (p,(string->html x))) (read-all (car (process FINGER)))))))) Program ten buduje X-wyrażenie uruchamiając procesy date i finger, po czym zamienia ich wyniki na HTML. Funkcja string->html pochodzi z biblioteki CGI, zamienia ona zwykłe napisy na napisy zgodne z konwencjami HTML. Każde wywołanie tego skryptu wygeneruje inną stronę (chociażby dlatego, że zmieni się data i czas ;-). 6
5 Programy CGI z argumentami Znacznie ciekawsze są programy CGI, których wyniki zależą od otrzymanych danych wejściowych. Najczęściej użytkownik wypełnia formularz WWW i naciskając przycisk potwierdzenia powoduje wysłanie tej informacji (odpowiednio zapisanej). Serwer przekazuje tę informację do programu CGI określonego w formularzu. 5.1 Pobieranie informacji od użytkownika Dane do programów CGI pobiera się używając formularzy HTML. Mogą one zawierać pola tekstowe, rozmaite przyciski, przewijane tablice itp. Pobieranie informacji składa się z dwóch kroków: nalezy przygotować dokument HTML zawierający odpowiednie formularze, a następnie w programie CGI zanalizować wczytane dane i określić akcję do wykonania. 5.1.1 Formularze Formularze umieszcza się w dokumencie HTML używając elementu <form>: <FORM METHOD="POST" ACTION="http://form.url.com/cgi-bin/cgiprogram">... Zawartość formularza... </FORM> Adres URL podany atrybutem ACTION powinien wkazywać na program CGI. Atrybut METHOD podaje sposób przekazania danych z formularza do programu CGI. W powyższym przykładzie użyto zalecanej metody "POST", inna możliwość to metoda "GET", lecz zwykle jej użycie stwarza wiele kłopotów. Wewnątrz formularza można umieścić dowolny element z wyjątkiem innego formularza. Elementy służace do komunikacji z użytkownikiem to INPUT, SELECT i TEXTAREA. Najprostsze są elementy INPUT: <INPUT TYPE="text" NAME="napis" VALUE="domyślna" SIZE=10 MAXLENGTH=20> <INPUT TYPE="checkbox" NAME="thisbox" VALUE="on" CHECKED> <INPUT TYPE="radio" NAME="radio1" VALUE="1"> 7
<INPUT TYPE="radio" NAME="radio1" VALUE="2" CHECKED> <INPUT TYPE="submit" VALUE="gotowe"> Atrybut TYPE określa typ obiektu używanego do komunikacji, dozwolone wartości to "text", "password", "checkbox", "radio", "image", "hidden", "submit" i "reset". Dwa ostatnie pełnią role przycisków wysyłających zawartość formularza do serwera i czyszczących formularz. Każdy z pozostałych typów ma atrybut NAME podający nazwę zmiennej, z którą zostanie związana wartość w polu podczas przekazywania zawartości do programu CGI program. Poszczególne typy mają następującą postać "text" proste pole do wprowadzania tekstu. Atrybut VALUE podaje wartość domyślną (początkową), atrybut SIZE to rozmiar pola (domyślnie 20), zaś atrybut MAXLENGTH podaje maksymalną liczbę znaków do wpisania w pole (domyślnie bez ograniczeń). "password" działa tak samo jak pole tekstowe, ale nie wyświetla wpisywanych znaków. "checkbox" określa przełącznik, który może zostać wciśnięty ( włączony ). Atrybut VALUE określa napis przekazywany do programu CGI gdy przełącznik jest wciśnięty (wartość domyślna to "on"). "radio" określa przełącznik, który może być grupowany z innymi dając listę alternatywnych wyborów. Grupę tworzą przełączniki o tej samej wartości atrybutu NAME. Element SELECT służy do tworzenia menu i przewijalnych list wyboru. <SELECT NAME="menu"> <OPTION>wybór 1 <OPTION>wybór 2 <OPTION>wybór 3 <OPTION SELECTED>wybór 4 <OPTION>wybór 5 <OPTION>wybór 6 <OPTION>wybór 7 </SELECT> <SELECT NAME="scroller" MULTIPLE SIZE=7> <OPTION SELECTED>wybór 1 8
<OPTION SELECTED>wybór 2 <OPTION>wybór 3 <OPTION>wybór 4 <OPTION>wybór 5 <OPTION>wybór 6 <OPTION>wybór 7 </SELECT> Atrybut SIZE określa, czy będzie to spuszczalne menu czy lista wyboru. Jeśli jest pominięty lub ma wartość 1, to domyślnie jest to menu. Jeśli jest większy niż 1, to otrzymamy listę przewijaną z widocznymi SIZE elementami. Obecność opcji MULTIPLE automatycznie wybiera listę przewijaną z wieloma wyborami. Element OPTION podaje nazwy i wartości dla każdego pola i pozwala określić pocją SELECTED, czy jest ono domyślnie wybrane. Element TEXTAREA słuzy do wprowadzania tekstów w wielu wierszach: <TEXTAREA NAME="area" ROWS=5 COLS=30> Mary had a little lamb. A little lamb? A little lamb! Mary had a little lamb. It s fleece was white as snow. </TEXTAREA> Atrybut NAME to nazwa zmiennej, zaś ROWS i COLS określają rozmiar widocznej częsci tekstu. Początkową (domyślną) zawartość podaje się wewnątrz elementu. Spacje są respektowane, podobnie znaki nowej linii (jak w elemencie <PRE>). Spróbujmy napisać usługę, podającą informację tylko o wybranym użytkowniku. Zaczniemy od formularza: <html> <title>specialized Finger Gateway</title> <body> <h3>finger Gateway 2</h3> <form action=".../finger.scm" method="post"> <input type="text" name="who"> <input type="submit" value="finger"> </form> </body> 9
</html> Strona ta oprócz tytułu zawiera jedynie formularz (element form), zawierający dwa elementy input. Pierwszy z nich określa pole tekstowe do wprowadzenia identyfikatora użytkownika (w postaci linii tekstu). Drugi to przycisk potwierdzenia, po jego naciśnięciu zawartość formularza jest przesyłana do programu, którego nazwa jest podana jako atrybut action formularza. Oprócz nazwy programu formularz powinien podawać metodę przekazania wartości zmiennych (w tym przykładzie jedyna zmienną jest who) pobranych z formularza. Dwie główne metody to get (domyślna, informacje są przekazywane przez zmienną środowiskową QUERY_STRING) i post (informacje są przekazywane przez standardowe wejście programu CGI). Do określenia metody służy atrybut method w tym przypadku jego wartością jest post. Informacja o użytej metodzie zostaje przez serwer WWW zapisana w zmiennej środowiskowej REQUEST_METHOD i może być odczytana przez program CGI (wartości to "GET" i "POST"). 5.1.2 Wczytywanie po stronie serwera CGI Zawartość formularza jest przesyłana w postaci zapytania napisu o specjalnej postaci. Dla metody GET napis ten staje się wartością zmiennej środowiska QUERY_STRING. Serwer otrzymuje go bowiem jako fragment URL, na przykład: http://students.mimuw.edu.pl/~ab123456/aa?thisinput=default&thisbox=on&radio1= Zaytanie znajduje się po znaku zapytania (? ). Niezależnie od przyjętej metody, wartości zmiennych formularza sa przekazywane jako ciąg par nazwa/wartość, oddzielanych znakiem &. Nazwy od wartości oddziela się znakiem równości =. Ewentualne spacje w wartościach są zastępowane znakiem +, zaś wszystkie inne znaki nie alfanumeryczne trójznakową sekwencją zaczynająca się znakiem procenta %, po którym zapisany jest dwoma cyframi szesnastkowymi kod znaku, np. napis "20% + 30% = 50%, &c." będzie zapisany jako "20%25+%2b+30%25+%3d+50%25%2c+%26c%2e" 10
W metodzie POST postać zapytania jest taka sama, ale program CGI otrzymuje je przez standardowe wejście. Nie otrzymamy jednak końca pliku. Zamiast tego długość napisu zapytania podawana jest w zmiennej środowiska CONTENT_LENGTH, którą można odczytać funkcją getenv. Pora na program CGI do naszego przykładu. (define FINGER "/usr/bin/finger") (define (read-all ip) (let ((next (read-line ip))) (if (eof-object? next) () (cons next (read-all ip))))) (define *bindings* (get-bindings)) (define *user* (extract-binding/single who *bindings*)) (output-http-headers) (display-xml/content (xexpr->xml (html (title "Finger Gateway") (body ((bgcolor "white")) (p,(string->html (read-line (car (process "date"))))),@(map (lambda (x) (p,(string->html x))) (read-all (car (process (string-append FINGER " " *user*))))))))) Program ten wywołuje finger dla użytkownika podanego w formularzu. Jeśli pole to było puste, wypisana zostanie informacja dla wszystkich użytkowników (tak jak poprzednio). Uwaga: programu zapisanego w powyższej uproszczonej wersji nie należy umieszczać na serwerze nie jest on bezpieczny. Złośliwy użytkownik mógłby wpisać w przeglądarce po nazwie średnik, a następnie dowolne polecenie systemu operacyjnego (np. rm *.*). Poprawna pełna wersja sprawdzałaby najpierw wprowadzoną wartość, np. usuwając niedozwolone znaki. 11
5.2 Zwracanie odpowiedzi dla użytkownika Dokument HTML stanowiący odpowiedź tworzy się dynamicznie na podstawie odpowiedzi na zapytanie użytkownika. Można wykorzystać dowolne techniki formatowania dostępne w HTML. 5.2.1 CGI Output Skonstruowany dokument HTML zawierający odpowiedź na zapytanie nalezy poprzedzić krótkim nagłówkiem HTTP, podającym jako typ MIME HTML i składającym się w najprostszym przypadku z pojedynczego wiersza, po której musi nastąpić pusty wiersz. content-type: text/html <HTML>... plik... </HTML> Oczywiście jest wiele innych typów dokumentów, ale do obsługi baz danych ten w zupełności wystarczy. Programy CGI zwracają skonstruowany dokument HTML przez standardowe wyjście, mozna więc w Scheme po prostu używać funkcji printf. Na przykład aby wypisać nagłówek: (printf "content-type: text/html\r\n\r\n") 6 Wybrane funkcje biblioteczne (output-http-headers) Wypisuje nagłówek HTTP niezbędny dla normalnej odpowiedzi HTML (get-bindings) (wiązanie...) Zwraca wiązania zmiennych przekazane z danego żądania HTTP. Każde wiązanie jest listą dwuelementową postaci (klucz wartość) 12
Klucz może być symbolem lub napisem i podaje nazwę zmiennej, natomiast wartość jest napisem. (extract-bindings klucz lista-wiązań) (wartość...) Zwraca listę wszystkich wartości związanych ze zmienną podaną jako klucz (z kluczem może być związanych kilka wartości gdy użyto elementów checkbox). Jeśli klucz nie wystapił w żadnym wiązaniu, to lista będzie pusta. (extract-bindings/single klucz lista-wiązań) wartość Zwraca (jedyną) wartość związaną z podanym kluczem. Więcej informacji można znaleźć w Help Desku wybierając hasła CGI i XML. 13