Programowanie serwerów WWW
Niniejszy rozdział poświęcony jest programowaniu aplikacji serwerów WWW opartych na technologiach CGI/WinCGI oraz ISAPI/NSAPI z wykorzystaniem technologii WebBroker i InternetExpress.
Reprezentantami technologii WebBroker na gruncie C++Buildera są: specjalizowany moduł danych, zwany modułem danych WWW lub krótko modułem WWW (ang. WebModule), bogaty zestaw komponentów i dwa kreatory, z których najczęściej używanym jest Web Server Application Wizard. Drugi z kreatorów — Database Web Application Wizard — umożliwia integrację tabeli danych z modułem WWW.
Wspomniane komponenty podzielone są na dwie grupy. Pierwsza z nich to tzw. komponenty-producenci, m.in. TPageProducer, TDataSetPageProducer, TDataSetTableProducer, TQueryTableProducer; C++Builder 5 w wersji Enterprise wprowadza dodatkowo dwa komponenty: TMidasPageProducer i TReconcilePageProducer. Komponenty drugiej grupy to podstawowe zręby aplikacji WWW: TWebModule, TWebDispatcher, TWebRequest i TWebResponse.
Przed uruchomieniem przykładowego projektu ilustrującego zagadnienia poruszane w niniejszym rozdziale konieczne jest dokonanie pewnych ustawień w środowisku C++Buildera, w szczególności instalacja dodatkowych pakietów i komponentów; szczegółowe instrukcje na ten temat znajdują się w pliku README.TXT umieszczonym na załączonym CD-ROMie wraz ze wspomnianym projektem w katalogu WebServer.
Moduły WWW
Określenia „WebBroker” i „Moduł WWW” często używane są zamiennie. W rzeczywistości WebBroker stanowi element modułu WWW (tzw. dyspozytor akcji — action dispatcher) wzbogacający „zwykły” moduł danych (datamodule) o elementy charakterystyczne dla modułu WWW. Innymi słowy, stanowi on centralną część mechanizmów umożliwiających budowę aplikacji opartych na protokołach ISAPI/NSAPI, CGI lub WinCGI bez troszczenia się o różnice pomiędzy tymi protokołami. Dodatkowo Web Bridge umożliwia wykorzystanie tej samej aplikacji zarówno dla protokołu ISAPI (wszystkie wersje) jak i NSAPI (Netscape do wersji 3.6 włącznie).
Aplikacje serwerów WWW są aplikacjami niewizualnymi — interfejs użytkownika zapewniają im aplikacje klienckie. Moduły WWW nie mogą więc zawierać komponentów wizualnych, ich konstruowanie odbywa się poza tym w sposób identyczny z konstruowaniem formularzy.
Web Serwer Application Wizard
Standardowy kreator wspomagający tworzenie aplikacji serwerów WWW — Web Server Application Wizard — dostępny jest na karcie New okna New Items (otwieranego za pomocą opcji File|New menu głównego IDE). Bezpośrednio po jego uruchomieniu wyświetlane jest okno umożliwiające wybór protokołu tworzonej aplikacji (rys. 13.1):
Tu proszę wkleić rysunek z pliku ORIG-13-1.BMP
Rysunek 13.1 Wybór protokołu aplikacji serwera WWW
CGI
Aplikacja typu CGI (Common Gateway Interface) jest aplikacją konsolową, ładowaną przez serwer na żądanie użytkownika i rozładowywaną po zrealizowaniu tego żądania. Żądanie klienta przekazywane jest aplikacji poprzez standardowe wejście (StdIn), natomiast odpowiedź — zazwyczaj w formacie HTML — wypisywana jest do standardowego wyjścia (StdOut).
WinCGI
WinCGI stanowi specyficzną dla Windows implementację protokołu CGI. W przeciwieństwie do standardowego CGI aplikacja WinCGI jest aplikacją typu GUI, chociaż nie manifestuje swej obecności w postaci wizualnej. Rolę standardowych strumieni StdIn i StOut przejął pojedynczy plik INI, za pośrednictwem którego odbywa się wymiana danych pomiędzy serwerem i użytkownikiem.
ISAPI/NSAPI
Rozszerzenia serwera typu ISAPI (Microsoft IIS) i NSAPI (Netscape) tym różnią się od aplikacji CGI/WinCGI, iż w przeciwieństwie do tych ostatnich są bibliotekami DLL. Realizacja żądań odbywa się tu szybciej, ponieważ biblioteki te, ładowane jednorazowo w efekcie pierwszego żądania, pozostają w pamięci serwera.
Jako że przyszłe wersje Netscape z pewnością obsługiwać będą protokół ISAPI, a w chwili obecnej możliwe jest „mapowanie” odwołań do ISAPI na odwołania do NSAPI, w dalszym ciągu rozdziału używać będziemy dla uproszczenia pojedynczej nazwy ISAPI na określenie obydwu protokołów.
CGI czy ISAPI?
Biblioteki DLL stanowiące rozszerzenia typu ISAPI mają tę niezaprzeczalną przewagę nad aplikacjami CGI, iż są szybsze i efektywniejsze w działaniu — wszystkie żądania dotyczące danej biblioteki obsługiwane są przez jej pojedynczy egzemplarz, oszczędza się więc zarówno czas, jak i pamięć. Owa „integracja” biblioteki z serwerem ma jednak swe ujemne strony: po pierwsze, jej „podmiana” na nową wersję wymaga zatrzymania serwera i jego ponownego uruchomienia; po drugie i ważniejsze — błędna biblioteka może spowodować załamanie całego serwera, tak więc przed „poważnym” zastosowaniem musi ona zostać przetestowana znacznie gruntowniej niż zwykła aplikacja. Należy zdawać sobie sprawę z faktu, iż biblioteka taka pracuje w warunkach powtarzalnego wykorzystywania (ang. serial reusability), co stwarza okazję do popełnienia wielu typowych błędów, jak np. brak inicjowania zmiennych roboczych przy każdym żądaniu — niezależne aplikacje CGI wolne są od tego problemu.
Niezależnie jednak od tego, czy finalnym produktem danego projektu ma być aplikacja CGI, czy biblioteka ISAPI/NSAPI, jego moduł WWW jest dokładnie taki sam. Stwarza to możliwość przetestowania przyszłej aplikacji CGI w tymczasowej postaci biblioteki DLL (śledzenie bibliotek DLL w C++Builderze jest znacznie łatwiejsze niż śledzenie zewnętrznych procesów). Należy w tym celu utworzyć (w ramach jednej grupy) dwa projekty: zasadniczy, tworzący aplikację CGI i pomocniczy, tworzący bibliotekę ISAPI. Następnie, używając Menedżera Projektu, należy usunąć moduł WWW z projektu zasadniczego (CGI) a następnie dodać do tegoż projektu moduł WWW z projektu pomocniczego (ISAPI) — w ten sposób obydwa projekty dzielić będą ten sam moduł WWW.
Podstawowe komponenty WebBrokera
Po wybraniu typu aplikacji z okna dialogowego pokazanego na rysunku 13.1 nastąpi utworzenie pustego modułu danych i związanego z nim modułu źródłowego. Używając opcji File|Save All z menu głównego IDE należy zapisać ów projekt w oddzielnym katalogu (np. WebServer) nadając modułowi nazwę WebMod, zaś projektowi nazwę WebShow (tych właśnie nazw używać będziemy w dalszej części rozdziału).
Przed przystąpieniem do budowy aplikacji należy wpierw zapewnić, by nie była on zależna od żadnych zewnętrznych pakietów. W tym celu należy przejść na kartę Linker opcji projektu (Shift+Ctrl+F11) i wyłączyć opcję Use dynamic RTL, a następnie na kartę Packages i wyłączyć opcję Build with runtime packages.
Moduł WWW jest już gotowy do tego, by umieszczać na nim stosowne komponenty. Komponenty te znajdują się w palecie na stronie Internet (patrz rysunek 13.2) oraz na stronie InternetExpress, którą zajmiemy się nieco później.
Tu proszę wkleić rysunek z pliku ORIG-13-2.BMP
Rysunek 13.2 Strona Internet palety Komponentów
Spoglądając na rysunek 13.2 widzimy (kolejno od lewej) ikony komponentów TClientSocket i TServerSocket (nie mają one związku z WebBrokerem) a następnie komponenty TWebDispatcher, TPageProducer, TQueryTableProducer, TDataSetTableProducer, TDataSetPageProducer i TCppWebBrowser — na bazie tego ostatniego zbudowano aplikację umożliwiającą śledzenie aplikacji WWW z poziomu IDE, którą to aplikację wykorzystywać będziemy w dalszej części rozdziału.
Niezależnie od komponentów znajdujących się w Palecie opiszemy komponenty kluczowe dla modułów WWW: TWebModule, TWebRequest i TWebResponse.
TWebDispatcher
Komponent TWebDispatcher jest tym elementem modułu WWW, który odróżnia ów moduł od „zwykłego” modułu danych (tak więc TDataModule + TWebDispatcher = TWebModule). Jego zadaniem jest przekazywanie żądań użytkowników do odpowiednich komponentów TWebActionItem zgrupowanych w kolekcję reprezentowaną przez właściwość Actions modułu WWW.
W danej aplikacji może wystąpić tylko jeden komponent TWebDispatcher — co oznacza, iż aplikacja ta może posiadać tylko jeden moduł WWW. C++Builder nie zezwoli na umieszczenie dodatkowego komponentu TWebDispatcher w module WWW utworzonym przez kreator — zagadkowy więc wydaje się fakt, iż nie zabrania on tego w stosunku do zwykłego modułu TDataModule.
TWebModule
Moduł WWW, reprezentowany przez komponent TWebModule, może być uważany za „wrażliwą” na Internet odmianę modułu danych (TDataModule). Najważniejszą jego właściwością jest właściwość Actions wskazując kolekcję grupującą komponenty TWebActionItem. Każdy z tych komponentów reprezentuje pewną akcję stanowiącą odpowiednik żądania użytkownika — ich właściwość PathInfo jest mianowicie tożsama z łańcuchem występującym z treści żądania bezpośrednio po URL; za odnalezienie komponentu TWebActionItem odpowiadającego danemu żądaniu odpowiedzialny jest TWebDispatcher.
Dodawanie i usuwanie komponentów TWebActionItem realizowane jest przez tzw. edytor akcji, stanowiący specjalizowany edytor właściwości Actions. Można go uruchomić na kilka sposobów — na przykład klikając w wielokropek w linii właściwości Actions lub wybierając opcję Action Editor z menu kontekstowego modułu WWW. W wersji 5 C++Buildera można także posłużyć się projektantem modułu danych (Visual Data Module Designer) w którego drzewie (widocznym w lewej części modułu) przedmiotowe komponenty ukazane są jako potomne w stosunku do właściwości Actions modułu, a ich zestaw zmieniać można za pomocą menu kontekstowego.
Rysunek 13.3 przedstawia uruchomiony edytor akcji, w oknie którego widoczne są pozycje reprezentujące osiem akcji odpowiadających ośmiu różnych żądaniom użytkownika — /hello, /page, /dataset, /table, /query, /login, /browse i /final. Zwróć uwagę, iż pierwszy z komponentów akcji — WebActionItem1 — jest komponentem domyślnym — jego właściwość Default ma wartość true. Jeżeli żądanie użytkownika nie odpowiada właściwości PathInfo w żadnym z komponentów akcji, wybierany jest właśnie komponent domyślny.
Tu proszę wkleić rysunek z pliku ORIG-13-3.BMP
Rysunek 13.3 Edytor akcji modułu WWW
W kontekście wybranego przez TWebDispatcher komponentu akcji generowane jest następnie zdarzenie OnAction, w ramach obsługi którego dokonać ma się realizacja żądania użytkownika. Szkielet funkcji obsługującej to zdarzenie wygląda następująco:
void __fastcall TWebModule1::WebModule1WebActionItem1Action(
TObject *Sender, TWebRequest *Request, TWebResponse *Response,
bool &Handled)
{
}
Zanim jednak przystąpimy do oprogramowania poszczególnych akcji, konieczne jest przyjrzenie się pewnym szczegółom związanym z parametrami Request i Response.
TWebResponse
Odpowiedź na żądanie użytkownika, generowana w ramach zdarzenia OnAction, reprezentowana jest przez parametr Response funkcji zdarzeniowej. Parametr ten posiada kilka właściwości, z których najważniejszą jest Content zawierająca treść odpowiedzi w postaci łańcucha HTML — wykonanie poniższej funkcji spowoduje wygenerowanie odpowiedzi „Hello, world!”:
void __fastcall TWebModule1::WebModule1WebActionItem1Action(
TObject *Sender, TWebRequest *Request, TWebResponse *Response,
bool &Handled)
{
Response->Content = "<H1>Hello, world!</H1>"
}
Format informacji przypisywanej właściwości Content określony jest przez właściwość ContentType. Musi mieć ona postać zgodną z jednym z formatów MIME (w postaci „typ/podtyp”) — wartością domyślną jest text/html. Odpowiedzi w formie binarnej (np. obrazki) nie mogą być jednak przekazywane wprost przez właściwość Content — do ich przekazania służy właściwość ContentStream.
Pozostałe właściwości klasy TWebResponse odpowiedzialne są za kodowanie odpowiedzi (ContendEncoding), zapisywanie cookies w komputerze klienta (Cookies), bezpieczeństwo (StatusCode, ReasonString, WWWAuthenticate, Realm), czas życia dynamicznie generowanego dokumentu (Date, Expires) i informacje diagnostyczne (LogMessage).
TWebRequest
Parametr Request funkcji zdarzeniowej określa różnorodne aspekty żądania użytkownika. Zależnie od metody wysłania żądania (GET lub POST — patrz niżej) określonej przez właściwość Method jego treść dostępna jest albo pod właściwościami Query i QueryFields, albo pod właściwościami Content i ContentFields. Oto prosty przykład wykorzystania tej informacji w generowanej odpowiedzi:
void __fastcall TWebModule1::WebModule1WebActionItem1Action(
TObject *Sender, TWebRequest *Request, TWebResponse *Response,
bool &Handled)
{
Response->Content = "<H1>Hello, world!</H1>";
if (Request->Method == "GET")
Response->Content = Response->Content + "<B>GET</B>" +
"<BR>Query: " + Request->Query;
else
if (Request->Method == "POST")
Response->Content = Response->Content + "<B>POST</B>" +
"<BR>Content: " + Request->Content;
}
Do innych użytecznych właściwości klasy TWebRequest należą m.in. CookieField (reprezentująca treść cookie odczytanego z komputera klienta), Authorization (odpowiedzialna za bezpieczeństwo) oraz Referrer i UserAgent (związane bezpośrednio z metodą wysyłania żądania).
Wysłanie żądania wg metody GET polega na umieszczeniu jego treści wprost w URL. Jest to metoda względnie szybka, lecz rozmiar przesyłanych danych jest ograniczony do co najwyżej kilku kilobajtów.
W przypadku metody POST żądanie przesyłane jest za pomocą standardowych mechanizmów wejścia-wyjścia (lub przez plik INI w przypadku WinCGI). Przesyłanie jest wolniejsze, lecz rozmiar żądania ograniczony jest tylko wielkością dostępnej przestrzeni dyskowej. Ponadto nie jest widoczna treść przesyłanych danych, nie są więc one narażone na (przypadkowe) uszkodzenie przez użytkownika.
Serwery WWW
Pora przystąpić do przetestowania pierwszej aplikacji serwera WWW. Do swego działania wymaga ona funkcjonującego serwera WWW, którym może być Personal Web Server (dla Windows 95/98) albo Microsoft Internet Information Server (IIS) dla Windows NT/2000. Firmowy podręcznik użytkownika (Borland C++Builder 5 Developer's Guide) , dostarczany wraz z kopią C++Buildera 5 zawiera w rozdziale 30. szczegółowe wskazówki co do skonfigurowania Personal Web Servera tak, by można było na nim testować i śledzić aplikacje CGI i biblioteki ISAPI.
Należy przypomnieć, iż biblioteki ISAPI raz załadowane przez serwer nie są rozładowywane aż do zakończenia jego pracy. Przed utworzeniem nowej wersji biblioteki należy więc zatrzymać serwer (oraz usługę administracyjną IIS), a po zakończeniu kompilacji uruchomić go ponownie (co automatycznie uruchomi również usługę administracyjną IIS). Błędnie skonstruowana biblioteka DLL może ponadto spowodować kompletne załamanie serwera, wymagające jego zatrzymania i ponownego uruchomienia. Problem ten złagodzony został w najnowszej wersji IIS, oferującej opcję Run in Separate Memory Space, powodującą odizolowanie wątpliwego procesu od pozostałych procesów serwera. Możliwe jest także rozładowywanie biblioteki ISAPI po zrealizowaniu każdego żądania i ponowne jej ładowanie po nadejściu kolejnego, co umożliwia jej aktualizację bez zatrzymywania serwera (należy w tym celu wyłączyć opcję Cache ISAPI Application dla danej biblioteki w oknie dialogowym Menedżera IIS); uczynienie tego w „rzeczywistym” serwerze spowoduje jednak utratę korzyści wynikających z szybkości protokołu ISAPI.
W charakterze narzędzia wspomagającego śledzenie aplikacji serwera WWW można użyć freeware'owego programu IntraBob stworzonego przez jednego z autorów amerykańskiego wydania niniejszej książki. Program ten znajduje się na załączonym CD-ROMie w katalogu Software\IntraBob, można go także pobrać ze strony http://www.drbob42.com. Zastępuje on serwer WWW i umożliwia śledzenie bibliotek ISAPI z poziomu IDE C++Buildera lub Delphi. Należy przekopiować jego pliki do katalogu zawierającego przedmiotową bibliotekę ISAPI i użyć pliku IntraBob.exe w charakterze aplikacji nadrzędnej (Host Application), jak pokazuje to rysunek 13.4:
Tu proszę wkleić rysunek z pliku ORIG-13-4.BMP.
Rysunek 13.4 IntraBob jako aplikacja nadrzędna dla śledzonej biblioteki ISAPI
Śledzenie biblioteki DLL będzie jednakże możliwe tylko wtedy, gdy w jej kodzie źródłowym ustanowiony zostanie punkt przerwania (breakpoint). Ustawmy go więc w jednej z początkowych linii funkcji zdarzeniowej komponentu WebActionItem1 (rys. 13.5):
Tu proszę wkleić rysunek z pliku AG-13-A.BMP
Rysunek 13.5 Ustawienie punktu przerwania w kodzie źródłowym biblioteki DLL
Aby „uruchomić” śledzoną bibliotekę ISAPI należy wpierw przygotować formularz HTML zawierający odpowiednie żądanie skierowane do tej biblioteki, na przykład:
<HTML>
<BODY>
<H1>WebBroker HTML Form</H1>
<HR>
<FORM ACTION="http://localhost/scripts/WebShow.dll" METHOD=POST>
Name: <INPUT TYPE=EDIT NAME=Name>
<P>
<INPUT TYPE=SUBMIT VALUE=Submit>
</FORM>
</BODY>
</HTML>
Po wyświetleniu tej strony w przeglądarce WWW, wpisaniu czegokolwiek w pole nazwy i kliknięciu w przycisk Submit spowodujemy załadowanie biblioteki DLL i uruchomienie aplikacji nadzorującej IntraBob.exe. Przegląda ona formularz HTML i automatycznie wypełnia swe opcje treścią żądania, nazwą modułu wykonywalnego (.EXE lub .DLL), pozostawiając możliwość wpisania odpowiedniej informacji w pole PathInfo w celu spowodowania żądanej akcji modułu WWW. Kartę opcji programu IntraBob przedstawia rysunek 13.6.
SCAN
Tu proszę zeskanować rysunek 13.5 ze strony 576 oryginału
Rysunek 13.6 Opcje programu IntraBob
Jeżeli teraz wyślemy do serwera żądanie z pustym polem PathInfo, uruchomi się domyślna akcja, reprezentowana przez komponent WebActionItem1 i wykonanie kodu biblioteki ISAPI zatrzyma się na ustawionym wcześniej punkcie przerwania (rys. 13.7):
SCAN
Tu proszę zeskanować rysunek 13.6 ze strony 577 oryginału
Rysunek 13.7 Zatrzymanie wykonywania biblioteki DLL na punkcie przerwania
Początek Ostrzeżenia
Zdarza się, iż w czasie wykonywania programu IntraBob sygnalizowany jest następujący wyjątek:
Project Intrabob.exe raised exception class Exception with message
"Only one data module per application". Process Stopped.
Use Step or Run to continue.
Przyczyną tego błędu jest sposób ładowania i rozładowywania przez C++Builder bibliotek ISAPI. Funkcja DllEntryPoint() w głównym pliku *.cpp dokonuje mianowicie bezwarunkowego kreowania egzemplarza modułu danych, niezależnie od przyczyny wywołania określonej przez parametr reason:
int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*)
{
try
{
Application->Initialize();
Application->CreateForm(__classid(TWebModule1), &WebModule1);
Application->Run();
}
catch (Exception &exception)
{
}
return 1;
}
Próba tworzenia modułu danych podejmowana jest więc nie tylko przy załadowaniu biblioteki (reason == DLL_PROCESS_ATTACH) lecz także przy jej rozładowaniu (reason == DLL_PROCESS_DETACH), jak również podczas przyłączania (reason == DLL_THREAD_ATTACH) i odłączania (reason == DLL_THREAD_DETACH) jej od wątku. Należy więc wyeliminować trzy ostatnie okoliczności, wzbogacając treść funkcji o jedną instrukcję warunkową:
int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*)
{
try
{
Application->Initialize();
if (reason == DLL_PROCESS_ATTACH)
Application->CreateForm(__classid(TWebModule1), &WebModule1);
Application->Run();
}
catch (Exception &exception)
{
}
return 1;
}
Koniec ostrzeżenia
Po ponownym uruchomieniu (F9) zatrzymanej w punkcie przerwania biblioteki otrzymamy stronę stanowiącą odpowiedź na wysłane żądanie (rys. 13.8); zwróć uwagę, iż spacje zamienione zostały na znak „+”, w podobny sposób rozmaite „niedrukowalne” znaki reprezentowane są w postaci kodu szesnastkowego poprzedzonego znakiem „%”.
SCAN
Tu proszę zeskanować rysunek 13.7 ze strony 578 oryginału
Rysunek 13.8 Wynikowa strona HTML w oknie programu IntraBob
Komponenty-producenci WebBrokera
Zadaniem komponentów-producentów jest tworzenie wynikowych stron HTML według specyficznych kryteriów. Komponenty te znajdują się na stronie Internet Palety Komponentów; w kolejnych sekcjach zaprezentujemy teraz na praktycznych przykładach zastosowanie niektórych z nich.
TPageProducer
Pod parametr Response funkcji zdarzeniowej podstawić można cokolwiek — nawet kompletną stronę WWW — często zdarza się jednak, iż strona stanowiąca rezultat odpowiedzi powinna być oparta na predefiniowanym szablonie, zaś realizacja żądania użytkownika powinna wówczas polegać na wypełnieniu konkretnymi wartościami pewnych pól tego szablonu. Zadanie to spełnia komponent TPageProducer.
Wspomniany szablon może być dostarczony komponentowi dwojako: albo w postaci pliku dyskowego — nazwa tego pliku jest wówczas reprezentowana przez właściwość HTMLFile — albo w postaci listy łańcuchów TStrings — lista ta wskazywana jest wówczas przez właściwość HTMLDoc. Właściwości HTMLFile i HTMLDoc wykluczają się nawzajem — zmiana jednej z nich powoduje automatyczne „wyczyszczenie” drugiej.
Szablon strony może zawierać dowolny tekst w formacie HTML, jak również specjalne znaczniki rozpoczynające się od znaku „#” (tzw. invalid # tags); znaczniki te są właśnie wspomnianymi polami szablonu i są ignorowane przez przeglądarki WWW, natomiast napotkanie takiego znacznika przez komponent TPageProducer powoduje wygenerowanie zdarzenia OnHTMLTag, którego obsługa powinna zastąpić ów znacznik konkretną zawartością.
W charakterze przykładu rozpatrzmy następujący szablon:
<H1>TPageProducer</H1>
<HR>
<#Greeting> <#Name>
<P>
It's now <#Time> and we're working with a PageProducer
Poniższa funkcja zdarzeniowa zastępuje pole <#Name> imieniem Bob, w pole <#Time> wpisuje aktualną datę i czas, zaś pole <#Greetings> zastępowane jest powitaniem stosownym do pory dnia:
Wydruk 13.1 Przykład zastępowania pól szablonu przez TPageProducer
void __fastcall TWebModule1::PageProducer1HTMLTag(TObject *Sender,
TTag Tag, const AnsiString TagString, TStrings *TagParams,
AnsiString &ReplaceText)
{
if (TagString == "Name")
ReplaceText = "Bob";
else
if (TagString == "Time")
ReplaceText = DateTimeToStr(Now());
else
if ((double)Time() < 0.5) // przed południem
ReplaceText = "Good Morning";
else
if ((double)Time() > 0.7) // po 16:48 (0.7 dnia)
ReplaceText = "Good Evening";
else
ReplaceText = "Good Afternoon";
}
Zastępowanie znacznika ustaloną a priori zawartością jest jednak mało ciekawe, wszak formularze HTML przeważnie pytają użytkownika o imię i nazwisko. Moduł WWW posiada właściwość Request, której wartość identyczna jest z wartością właściwości Request bieżącego komponentu akcji, możemy więc w prosty sposób uzyskać dostęp do poszczególnych pól żądania (za pośrednictwem podwłaściwości QueryFields lub ContentFields) — to samo tyczy się właściwości Response.
Oto więc bardziej rozbudowana obsługa zdarzenie OnHTMLTag:
Wydruk 13.2 Inny przykład zastępowania pól szablonu przez TPageProducer
void __fastcall TWebModule1::PageProducer1HTMLTag(TObject *Sender,
TTag Tag, const AnsiString TagString, TStrings *TagParams,
AnsiString &ReplaceText)
{
if (TagString == "Name")
{
if (Request->Method == "POST")
ReplaceText = Request->ContentFields->Values["Name"];
else
ReplaceText = Request->QueryFields->Values["Name"];
}
else
if (TagString == "Time")
ReplaceText = DateTimeToStr(Now());
else
if ((double)Time() < 0.5)
ReplaceText = "Good Morning";
else
if ((double)Time() > 0.7)
ReplaceText = "Good Evening";
else
ReplaceText = "Good Afternoon";
}
Skojarzenie komponentu-producenta z odpowiednim komponentem akcji dokonuje się za pomocą właściwości Producer tego ostatniego. Jest to nowość wersji 5 C++Buildera, w poprzednich wersjach kojarzenie to musiało być dokonywane jawnie w kodzie programu i polegało na przepisaniu zawartości komponentu-producenta do parametru Response funkcji zdarzeniowej komponentu akcji. W przypadku komponentu wykonującego obsługę żądania /hello naszego projektu wyglądałoby to następująco:
void __fastcall TWebModule1::WebModule1WebActionItem2Action(
TObject *Sender, TWebRequest *Request, TWebResponse *Response,
bool &Handled)
{
Response->Content = PageProducer1->Content();
}
Nowa możliwość wersji 5 eliminuje co prawda konieczność pisania dodatkowego kodu, lecz tym samym uniemożliwia przechwycenie (np. przez ustawienie punktu przerwania) transferu danych od komponentu-producenta do komponentu akcji. W razie potrzeby zawsze można posługiwać się rozwiązaniem tradycyjnym.
Wpisując nazwisko „Bob Swart” do formularza uruchamiającego bibliotekę ISAPI ujrzymy w odpowiedzi stronę prezentowaną na rysunku 13.9; należy przy tym pamiętać o właściwym ustawieniu wartości PathInfo (na /hello) bądź to w oknie programu IntraBob, bądź wprost w treści formularza:
<FORM ACTION="http://localhost/scripts/WebShow.dll/hello" METHOD=POST>
SCAN
Tu proszę zeskanować rysunek 13.8 ze strony 581 oryginału
Rysunek 13.9 Strona HTML wyprodukowana przez TPageProducer
Niektóre edytory stron HTML (szczególnie FrontPage) umożliwiają ujmowanie parametrów znaczników w podwójne cudzysłowy, nie wchodzące oczywiście w skład parametru (na przykład <#Date "Format">). Cudzysłowy te są konsekwentnie ignorowane przez TPageProducer, o ile jego właściwość StripParamQuotes ustawiona jest na true (jest to ustawienie domyślne).
TDataSetPageProducer
Komponent TDataSetPageProducer jest komponentem pochodnym do TPageProducer. Zastępuje on pole szablonu nie wskazaną bezpośrednio wartością, lecz zawartością pola o wskazanej nazwie w bieżącym rekordzie wskazanego zbioru danych (TDataSet).
Aby zobaczyć to w praktyce, umieść w module WWW komponent TDataSetPageProducer i komponent TTable. Ten ostatni nazwij TableBiolife, pod właściwości DatabaseName i TableName podstaw (odpowiednio) BCDEMOS i biolife.db. Otwórz tabelę ustawiając na true jej właściwość Active i przypisz tę tabelę do właściwości DataSet komponentu DataSetPageProducer1. Następnie uruchom edytor właściwości HTMLDoc tego ostatniego i utwórz następującą listę łańcuchów:
<H1>BIOLIFE Info</H1>
<HR>
<BR><B>Category:</B> <#Category>
<BR><B>Common_Name:</B> <#Common_Name>
<BR><B>Species Name:</B> <#Species Name>
<BR><B>Notes: </B><#Notes>
Przejdź teraz do czwartego komponentu akcji i przypisz do jego właściwości Producer komponent DataSetPageProducer1. Następnie w formularzu „startowym” zmień rodzaj żądania na /dataset:
<FORM ACTION="http://localhost/scripts/WebShow.dll/dataset" METHOD=POST>
Wynikową stronę HTML przedstawia rysunek 13.10.
SCAN
Tu proszę zeskanować rysunek 13.9 ze strony 583 oryginału
Rysunek 13.10 Strona HTML wyprodukowana przez TDataSetPageProducer
Uważny Czytelnik spostrzeże natychmiast, iż na rysunku 13.10 dwie rzeczy są nie w porządku.
Po pierwsze, zamiast zawartości pola Notes widzimy napis (MEMO). Aby zamiast tego napisu ujrzeć zawartość pola wykorzystamy fakt, iż TDataSetPageProducer, jako pochodny do TPageProducer, generuje zdarzenie OnHTMLTag; w ramach obsługi tego zdarzenia pobierzemy zawartość pola memo i zastąpimy nim wspomniany napis:
void __fastcall TWebModule1::DataSetPageProducer1HTMLTag(TObject *Sender,
TTag Tag, const AnsiString TagString, TStrings *TagParams,
AnsiString &ReplaceText)
{
if (ReplaceText == "(MEMO)")
ReplaceText = TableBiolife->FieldByName(TagString)->AsString;
}
Puste pola memo uwidaczniane są przez TDataSetPageProducer w postaci napisu (Memo), który nie zostanie zastąpiony przez powyższą funkcję pustym łańcuchem — i bardzo dobrze.
Druga anomalią na rysunku 13.10 jest to, iż nie została pobrana zawartość pola Species Name. Przyczyna tego jest prosta — nazwa pola zawiera spację, która przez TPageProducer traktowana jest jako ogranicznik nazwy znacznika, dlatego też TDataSetPageProducer poszukuje pola o nazwie Species, a nie znalazłszy go produkuje pusty łańcuch.
Problem ten można rozwiązać przez jawne wskazanie, iż znacznik o nazwie Species odnosi się do pola Species Name:
void __fastcall TWebModule1::DataSetPageProducer1HTMLTag(TObject *Sender,
TTag Tag, const AnsiString TagString, TStrings *TagParams,
AnsiString &ReplaceText)
{
if (TagString == "Species") // Species Name
ReplaceText = TableBiolife->FieldByName("Species Name")->AsString;
else
if (ReplaceText == "(MEMO)")
ReplaceText = TableBiolife->FieldByName(TagString)->AsString;
}
Innym rozwiązaniem jest dodanie do struktury tabeli nowego pola obliczanego (wirtualnego) tożsamego z polem Species Name, lecz nie zawierającego spacji w swej nazwie (np. SpeciesName).
Niewątpliwie są to jednak tylko półśrodki, a niniejszy przykład stanowi wyraźny argument na rzecz unikania spacji w nazwach pól bazy danych.
Przykładową — tym razem poprawną — zawartość jednego z rekordów na wynikowej stronie HTML przedstawia rysunek 13.11.
SCAN
Tu proszę zeskanować rysunek 13.10 ze strony 584 oryginału
Rysunek 13.11 Poprawiona zawartość strony z rysunku 13.10
Gdybyśmy chcieli zobaczyć zawartość innych rekordów, moglibyśmy przemieszczać się po tabeli bazy danych albo wyświetlać kilka rekordów jednocześnie. Wędrówka po rekordach tabeli wymaga utrzymywania informacji o położeniu bieżącego rekordu, a więc informacji o charakterze pamięci stanu; zajmiemy się tym zagadnieniem w dalszej części rozdziału. Oglądanie kilku rekordów jednocześnie możliwe jest natomiast dzięki kolejnemu komponentowi-producentowi — TDataSetTableProducer.
TDataSetTableProducer
Podobnie jak TDataSetPageProducer, tak i TDataSetTableProducer czerpie wyświetlaną zawartość ze zbioru danych reprezentowanego przez właściwość DataSet. Wyświetlana zawartość obejmuje jednak cały zestaw rekordów i ma postać tabelaryczną (ang. grid).
Umieść w module WWW drugi komponent TTable i nazwij go TableCustomer, a następnie skojarz z bazą danych BCDEMOS i tabelą customer.db, po czym otwórz ustawiając właściwość Active na true.
Umieść następnie w module WWW komponent TDataSetTableProducer i skojarz go (poprzez właściwość DataSet) z komponentem TableCustomer.
Komponent TDataSetTableProducer posiada szereg właściwości określający wygląd strony wynikowej — między innymi Header i Footer określający nagłówek i „stopkę” strony oraz RowAttributes i TableAttributes definiujące układ (wyrównanie, kolor itp.) wyświetlanych wierszy i tabeli jako całości.
Niewątpliwie jednak lepszy wgląd w wizualną postać tabeli daje właściwość Columns i związany z nią specjalizowany edytor; jego okno z przykładową zawartością przedstawia rysunek 13.12.
SCAN
Tu proszę zeskanować rysunek 13.11 ze strony 586 oryginału
Rysunek 13.12 Edytor kolumn komponentu TDataSetTableProducer
Przy pierwszym uruchomieniu edytora ujrzymy wszystkie pola tabeli customer.db. To standardowe zachowanie wszystkim komponentów pozostający w związku z TDataSet — jeżeli mianowicie nie wybrałeś żadnego pola, przyjmuje się, że chcesz je widzieć wszystkie. Mniej zrozumiały jest natomiast fakt, iż z zestawu tego nie da się usunąć żadnego pola ani też zmienić kolejności pól.
Otóż aby manipulować zestawem i kolejnością wyświetlanych pól, trzeba wpierw utworzyć ich listę — należy w tym celu kliknąć prawym przyciskiem w wyświetlany zestaw pól i z menu kontekstowego wybrać opcję Add All Fields. Na pierwszy rzut oka nic się nie zmieni, jednak poszczególne pola dadzą się teraz usuwać i przemieszczać. Co więcej, właściwości poszczególnych pól dadzą się niezależnie ustawiać za pomocą Inspektora Obiektów i to w sposób automatycznie odzwierciedlany w edytorze kolumn.
Podobnie jak w przypadku poprzednich komponentów-producentów, skojarzymy teraz komponent TDataSetTableProducer z odpowiednim — tym razem piątym — komponentem akcji, oczywiście za pomocą właściwości Producer.
Godnym uznania jest fakt, iż TDataSetTableProducer umożliwia formatowanie wyświetlanego widoku na poziomie poszczególnych komórek — przed wyświetleniem każdej komórki generowane jest mianowicie zdarzenie OnFormatCell dające okazję zmiany jej standardowego wyglądu. Zilustrujemy tę możliwość, wyświetlając na czerwonym tle (w kolumnie Country) pozycje związane ze Stanami Zjednoczonymi i nadając pustym komórkom kolor srebrny:
void __fastcall TWebModule1::DataSetTableProducer1FormatCell(
TObject *Sender, int CellRow, int CellColumn, THTMLBgColor &BgColor,
THTMLAlign &Align, THTMLVAlign &VAlign, AnsiString &CustomAttrs,
AnsiString &CellData)
{
if (CellData == "") BgColor = "Silver";
else
if ((CellColumn == 6) && (CellData.Pos("US") > 0))
BgColor = "Red";
}
Uzyskany wygląd wyświetlanej tabeli przedstawia rysunek 13.13
SCAN
Tu proszę zeskanować rysunek 13.12 ze strony 587 oryginału
Rysunek 13.13 Tabela bazy danych wyświetlana za pomocą komponentu TDataSetTableProducer
TQueryTableProducer
Nazwa komponentu TQueryTableProducer bierze się bynajmniej nie stąd, iż można z nim skojarzyć komponent TQuery — można go bowiem skojarzyć z dowolnym producentem posiadającym właściwość DataSet — lecz stąd, iż posiada on dodatkowe elementy obsługi parametryzowanych zapytań SQL.
Umieść w module WWW komponenty TQueryTableProducer i TQuery; drugi z nich nazwij QueryDemos, skojarz z bazą BCDEMOS i przypisz następujący tekst właściwości SQL:
SELECT * FROM ORDERS.DB AS O
WHERE (O.CustNo = :CustNo)
Zapytanie to, jak widać, posiada jeden parametr — CustNo. Konieczne jest określenie jego typu — w tym celu należy w Inspektorze Obiektów uruchomić edytor właściwości Params, podświetlić jedyny parametr w wyświetlonej liście, i ustawić jego typ (DataType) jako ftInteger, charakter (ParamType) jako ptInput i domyślną wartość (Value) jako 0.
W końcu należy „otworzyć” komponent QueryDemos i skojarzyć go z producentem TQueryTableProducer za pośrednictwem właściwości Query tego ostatniego.
Możliwości komponentu TQueryTableProducer w zakresie wyświetlania (i jego konfiguracji) są takie same jak w przypadku TDataSetTableProducer — obydwa wszak wywodzą się z klasy TDSTableProducer, przy czym komponent TQueryTableProducer wprowadza dodatkową właściwość Query.
Komponent TQueryTableProducer poszukuje nazwy parametru (w tym przypadku CustNo) wśród nazw pól składających się na właściwość ContentFields (lub QueryFields, zależnie od metody wysłania — GET albo PUT) i w przypadku znalezienia pola o żądanej nazwie podstawia jego wartość pod parametr w treści zapytania SQL. Zdefiniujmy w charakterze przykładu następujący formularz:
<HTML>
<BODY>
<H1>WebBroker HTML Form</H1>
<HR>
<FORM ACTION="http://localhost/scripts/WebShow.dll/query" METHOD=POST>
CustNo: <INPUT TYPE=EDIT NAME=Name>
<P>
<INPUT TYPE=SUBMIT VALUE=Submit>
</FORM>
</BODY>
</HTML>
Zwróć uwagę, iż nazwa pola pobieranego z formularza tożsama jest z nazwą parametru zapytania SQL. Wpisując w to pole np. 1221 otrzymamy listę wszystkich zamówień związanych z kontrahentem o numerze 1221 — w granicach określonych przez właściwość MaxRows zawierającą ograniczenie na liczbę wyświetlanych wierszy. Nadając tej właściwości dużą wartość zapewniamy sobie co prawda wyświetlenie wszystkich interesujących rekordów, lecz ryzykujemy długie oczekiwanie na moment, gdy cokolwiek pojawi się w oknie przeglądarki — wyświetlanie tabeli nie zostanie bowiem rozpoczęte przed odczytaniem znacznika </TABLE>. Uwaga ta odnosi się także do komponentu TDataSetTableProducer.
Rysunek 13.14 przedstawia wyświetlenie wyników zapytania w sytuacji, gdy w pole Custno na formularzu wpisano 1221.
SCAN
Tu proszę zeskanować rysunek 13.13 ze strony 589 oryginału
Rysunek 13.14 Wynikowy zbiór parametryzowanego zapytania SQL wyświetlany za pomocą komponentu TQueryTableProducer
Uzyskany wynik stwarza doskonałą okazję, by połączyć go z tym pokazanym na rysunku 13.13 — tak by dla wskazanego kontrahenta wyświetlone zostały (w osobnym oknie) związane z nim zamówienia. Wymaga to modyfikacji funkcji zdarzeniowej formatującej komórki — tak, by komórki w kolumnie CustNo zawierały hiperłącza, których klikanie powodować będzie generowanie nowych żądań /query. Można także przekształcić każdą ze wspomnianych komórek w formularz z ukrytym polem o nazwie Custno i predefiniowaną zawartością równą numerowi kontrahenta. Te dwie możliwości — hiperłącza i formularze — rozróżniane są w treści funkcji zdarzeniowej za pomocą symbolu kompilacji warunkowej LINK.
Oto zapowiadana funkcja w swej ostatecznej postaci:
void __fastcall TWebModule1::DataSetTableProducer1FormatCell(
TObject *Sender, int CellRow, int CellColumn, THTMLBgColor &BgColor,
THTMLAlign &Align, THTMLVAlign &VAlign, AnsiString &CustomAttrs,
AnsiString &CellData)
{
if ((CellColumn == 0) && (CellRow > 0)) // 1.kolumna - CustNo
CellData =
#ifdef LINK
// hiperłącze
"<A HREF=\"http://localhost/scripts/WebShow.dll/query?CustNo=" +
CellData + "\">" + CellData + "</A>";
#else
// formularz
(AnsiString)"<FORM ACTION=\"WebShow.dll/query\" METHOD=POST>" +
"<INPUT TYPE=HIDDEN NAME=CustNo VALUE=" + CellData + ">" +
"<INPUT TYPE=SUBMIT VALUE=" + CellData + ">" +
"</FORM>";
#endif
else
if (CellData == "") BgColor = "Silver";
else
if ((CellColumn == 6) && (CellData.Pos("US") > 0))
BgColor = "Red";
}
Na rysunku 13.15 widzimy znajomą kartotekę kontrahentów ze zmodyfikowaną kolumną CustNo, rysunek 13.16 przedstawia natomiast listę zamówień wybranego kontrahenta.
SCAN
Tu proszę zeskanować rysunek 13.14 ze strony 591 oryginału
Rysunek 13.15 Wyświetlenie kartoteki kontrahentów za pomocą komponentu TDataSetTableProducer, z hiperłączami w pierwszej kolumnie
SCAN
Tu proszę zeskanować rysunek 13.15 ze strony 591 oryginału
Rysunek 13.16 Wyświetlenie zamówień związanych z określonym kontrahentem, za pomocą komponentu TQueryTableProducer
Jako że IntraBob potrafi śledzić jedynie skutki żądań generowanych przez formularze HTML, nie będzie on w stanie śledzić skutków kliknięć w hiperłącza kontrahentów — za to bez problemów poradzi sobie z komórkami-formularzami.
Zarządzanie stanem sesji
Pamiętasz wynik produkowany przez TDataSetTableProducer? Wspominaliśmy wówczas o możliwości utrzymywania informacji o numerze bieżącego rekordu — jako przykładowej informacji o stanie dialogu użytkownika z serwerem. Sprawa ta jest o tyle interesująca, iż HTTP jako protokół bezstanowy nie oferuje żadnych standardowych środków w tym zakresie, muszą więc one być zapewnione za pomocą odrębnych technik.
„Gruby URL”
Najprostszym sposobem przechowania informacji o stanie jest uczynienie jej częścią URL — uczyniliśmy to już w przypadku hiperłączy w wyświetlanej kartotece kontrahentów
"<A HREF=\"http://localhost/scripts/WebShow.dll/query?CustNo=" +
CellData + "\">" + CellData + "</A>";
gdzie uczyniliśmy numer kontrahenta częścią URL. Na tej samej zasadzie, jeżeli chcielibyśmy przeglądnąć pierwszy rekord tabeli, można by sformułować żądanie jako
<FORM ACTION="http://localhost/scripts/WebShow.dll/dataset?RecNo=1"
METHOD=POST>
Mimo iż generalna metoda przesyłania danych określona jest jako POST, dane przesyłane są zgodnie z konwencją GET — w sposób widoczny dla użytkownika, a więc podatny na przypadkowe zmiany. Z tego względu metoda ta nie wydaje się godną polecenia.
Cookies
Cookies stanowią porcje danych zapisywane przez serwer na komputerze klienta — i jakkolwiek cała inicjatywa wychodzi od serwera, użytkownik może odrzucać cookies i ogólnie blokować ich przyjmowanie.
Cookie wysyłane jest jako część odpowiedzi Response, za pomocą metody SetCookieField(). Stanowi ono listę łańcuchów (TStringList) o postaci „nazwa=wartość” każdy, tak więc scenariusz wysłania przykładowego cookie mógłby wyglądać na przykład tak:
TStringList* Cookies = new TStringList();
Cookies->Add("RecNo="+IntToStr(Master->RecNo));
Response->SetCookieField(Cookies, NULL, NULL, Now()+1, false);
Cookies->Free();
Drugi i trzeci parametr metody SetCookieField() określa docelową domenę i ścieżkę dla cookie — parametry te mogą zostać „puste”. Czwarty parametr określa datę ważności cookie (tu: do następnego dnia), zaś ostatni parametr decyduje o tym, czy przesłanie cookies ma się odbywać z użyciem bezpiecznego połączenia.
Zapisanie cookie w komputerze użytkownika to jednak dopiero połowa roboty; należy je jeszcze odczytać, gdy okaże się potrzebne, i zrobić użytek z zawartej w nim informacji. Poszczególne łańcuchy cookie reprezentowane są przez poszczególne pola właściwości Request->CookieFields, tak więc odczytanie numeru ostatnio przeglądanego rekordu może być wykonane w następujący sposób:
int RecNo = StrToInt(Request->CookieFields->Values["RecNo"]);
Ukryte pola formularzy HTML
Aby zrealizować nawigowanie po rekordach bazy danych, należy utworzyć nowy formularz z czterema przyciskami oznaczającymi (odpowiednio) pierwszy, poprzedni, następny i ostatni rekord. Jednocześnie w formularzu tym należy umieścić dodatkowy znacznik <#RecNo>:
<FORM ACTION="http://localhost/scripts/WebShow.dll/dataset" METHOD=POST>
<H1>BIOLIFE Info</H1><HR>
<INPUT TYPE=SUBMIT NAME=SUBMIT VALUE="First">
<INPUT TYPE=SUBMIT NAME=SUBMIT VALUE="Prior">
<INPUT TYPE=SUBMIT NAME=SUBMIT VALUE="Next">
<INPUT TYPE=SUBMIT NAME=SUBMIT VALUE="Last">
<#RecNo>
<BR><B>Category:</B> <#Category>
<BR><B>Common_Name:</B> <#Common_Name>
<BR><B>Species Name:</B> <#Species Name>
<BR><B>Notes: </B><#Notes>
Aby zastąpić znacznik <#RecNo> numerem bieżącego rekordu, należy użyć następującej składni:
<INPUT TYPE=HIDDEN NAME=RecNo VALUE=1>
Powyższe polecenie nadaje polu wartość 1. Ukryte pola formularzy nie są widoczne dla użytkownika, ich zawartość przesyłana jest jednak z powrotem do serwera i do modułu WWW aplikacji, gdy użytkownik naciśnie jeden z przycisków SUBMIT.
Aby zrealizować całą tę historię, należy ponownie zmodyfikować obsługę zdarzenia OnHTMLTag, implementując dodatkowo wyświetlanie numeru bieżącego rekordu:
Wydruk 13.4 Wyświetlanie numeru bieżącego rekordu przez TDataSetPageProducer
void __fastcall TWebModule1::DataSetPageProducer1HTMLTag(TObject *Sender,
TTag Tag, const AnsiString TagString, TStrings *TagParams,
AnsiString &ReplaceText)
{
if (TagString == "RecNo")
ReplaceText =
"<INPUT TYPE=HIDDEN NAME=RecNo VALUE=" +
IntToStr(TableBiolife->RecNo) + // numer bieżącego rekordu
"> " + IntToStr(TableBiolife->RecNo) +"/"+
IntToStr(TableBiolife->RecordCount) + "<P>";
else
if (TagString == "Species") // Species Name
ReplaceText = TableBiolife->FieldByName("Species Name")->AsString;
else
if (ReplaceText == "(MEMO)")
ReplaceText = TableBiolife->FieldByName(TagString)->AsString;
}
Należy teraz „oprogramować” każdy z czterech przycisków (First, Prior, Next i Last) :
Wydruk 13.5 Zmiana bieżącego rekordu
void __fastcall TWebModule1::WebModule1WebActionItem1Action(
TObject *Sender, TWebRequest *Request, TWebResponse *Response,
bool &Handled)
{
DataSetPageProducer1->DataSet->Open();
// na wszelki wypadek, gdyby nie był jeszcze otwarty
int RecNr = 0;
// pobranie numeru aktualnego rekordu
AnsiString Str = Request->ContentFields->Values["RecNo"];
if (Str != "") RecNr = StrToInt(Str);
// rozpoznanie naciśniętego przycisku
Str = Request->ContentFields->Values["SUBMIT"];
if (Str == "First") RecNr = 1; // pierwszy rekord
else
if (Str == "Prior") RecNr--; // poprzedni rekord
else
if (Str == "Last") RecNr = // ostatni rekord
DataSetPageProducer1->DataSet->RecordCount;
else // nastepny rekord
RecNr++;
// zabezpieczenie przed wyjściem poza koniec zbioru
if (RecNr > DataSetPageProducer1->DataSet->RecordCount)
RecNr = DataSetPageProducer1->DataSet->RecordCount;
// zabezpieczenie przed wyjściem przed początek zbioru
if (RecNr < 1) RecNr = 1;
// czy faktycznie zmieniono numer rekordu?
if (RecNr != DataSetPageProducer1->DataSet->RecNo)
DataSetPageProducer1->DataSet->MoveBy(
RecNr - DataSetPageProducer1->DataSet->RecNo);
Response->Content = DataSetPageProducer1->Content();
}
„Unowocześniony” wygląd przeglądarki przedstawia rysunek 13.17 — różni się on od tego z rysunku 13.11 zestawem przycisków i informacją o liczbie rekordów oraz numerze rekordu bieżącego.
SCAN
Tu proszę zeskanować rysunek 13.16 ze strony 596 oryginału
Rysunek 13.17 Strona HTML utworzona przez TDataSetPageProducer, z możliwością nawigowania po tabeli rekordów.
W naszym przykładzie rekordy tabeli identyfikowane były dla uproszczenia numerami bezwzględnymi; przedstawioną koncepcję można uogólnić na identyfikowanie rekordów za pomocą unikalnych kluczy, co okazałoby się znacznie praktyczniejsze w przypadku dużych tabel podlegających intensywnym zmianom.
Bezpieczeństwo aplikacji sieciowych
Problem bezpieczeństwa aplikacji WWW nabiera coraz większego znaczenia, wobec gwałtownego rozwoju komercyjnych zastosowań Internetu z jednej strony, a coraz wymyślniejszymi atakami hakerskimi z drugiej. Nawet więc w przypadku najprostszej aplikacji WWW warto całkiem serio pomyśleć o bezpieczeństwie przetwarzanych przez nią danych.
Na szczęście opracowano kilka skutecznych technik, mających na celu ochronę przetwarzania rozproszonego. Przede wszystkim należy dążyć do rzeczywistego rozproszenia danych — nie należy więc na przykład umieszczać bazy danych na serwerze WWW, lecz np. w odrębnym serwerze plików, oddzielonym od serwera WWW firewallem; wskazane jest również umieszczenie drugiego firewalla pomiędzy serwerem WWW a Internetem. Jeżeli nawet w tych warunkach niepowołanej osobie uda się dostać do serwera WWW (co już wymaga pokonania przynajmniej jednego firewalla) nie oznacza to jeszcze dostępu do użytecznych danych. Serwer WWW staje się tym samym czymś w rodzaju „strefy zdemilitaryzowanej” oddzielonej szeregiem firewalli i proxy zarówno od świata zewnętrznego, jak i od przetwarzanych danych.
C++Builder 5 wraz z nowym komponentem TWebConnection udostępnia obsługę serwerów proxy. Komponent ten używany jest po stronie klienta w wielowarstwowych aplikacjach MIDAS (Multitier Distributed Application Services) i umożliwia aplikacji-klientowi połączenie się z odległą bazą danych (w wyższych warstwach modelu) za pośrednictwem proxy HTTP. Po stronie serwera WWW odpowiedzialność za połączenie z bazą danych spoczywa wówczas na bibliotece ISAPI o nazwie httpsvr.dll, która musi być uprzednio zainstalowana. Za pomocą właściwości Proxy komponentu TWebConnection możliwe jest podanie adresu IP serwera proxy, możliwe jest także określenie nazwy użytkownika (właściwość UserName) i jego hasła (właściwość Password); sam komponent TWebConnection nie zajmuje się przy tym weryfikacją nazwy i hasła użytkownika, lecz pełni rolę swoistego „routera” przekazującego te informacje do serwera proxy. Możliwa jest rezygnacja z serwera proxy, należy wówczas pozostawić niewypełnione właściwości Proxy, UserName i Password, możliwe jest także wykorzystanie połączeń „wysokopoziomowych”, jak TDCOMConnection czy TCORBAConnection zamiast „zwykłych” połączeń HTTP.
SSL — warstwa bezpiecznych gniazd
Niezależnie od różnych technik zabezpieczania dostępu do danych możliwe jest także zabezpieczenia samych danych na wypadek przechwycenia ich przez niepowołanego odbiorcę za pomocą np. podsłuchu — co nabiera szczególnego znaczenia w przypadku danych o wysokim stopniu tajności, jak np. numer karty kredytowej. Powszechnie wykorzystywanym zabezpieczeniem tego rodzaju jest szyfrowanie danych w oparciu o tzw. protokół bezpiecznych gniazd (ang. SSL — Secure Socket Layer) i przekazywanie ich w tej postaci w ramach (bezpiecznego) połączenia pomiędzy klientem a serwerem. Przeglądarki WWW sygnalizują fakt bezpiecznego połączenia za pomocą różnych odmian zamkniętej kłódki lub zamka, zaś prefiks adresu ma postać https:// (ang. S-HTTP — Secure HTTP) zamiast standardowego http://.
Funkcjonowanie protokołu SSL opiera się na wykorzystaniu dwóch powiązanych ze sobą kluczy — publicznego i prywatnego, uzyskiwanych przez właściciela serwera z zaufanego źródła, np. VeriSign. Bezpieczne połączenie rozpoczyna się od przesłania klientowi przez serwer klucza publicznego. Klient następnie szyfruje podlegające przesłaniu dane za pomocą tego klucza i w takiej postaci wysyła je do serwera. Ewentualne przechwycenie zarówno klucza publicznego, jak i zaszyfrowanych danych nie stanowi żądnego zagrożenia, dane te mogą być bowiem rozszyfrowane jedynie przy użyciu klucza prywatnego — ten zaś znany jest tylko serwerowi, a algorytm szyfrowania wyklucza uzyskanie go na podstawie klucza publicznego w rozsądnym czasie. Szyfrowanie i deszyfracja danych jest immanentną cechą samego połączenia i jako takie jest ono „przezroczyste” dla aplikacji serwera WWW.
Autoryzacja
Oprócz szyfrowania przesyłanych danych celowe wydaje się także reglamentowanie (w zróżnicowanym stopniu) dostępu do wybranych stron WWW. Wymaga to wykorzystania pewnych technik autoryzacji dostępu — zazwyczaj prostej weryfikacji nazwy i hasła użytkownika, która, nie mając bynajmniej krytycznego znaczenia, okazuje się całkowicie wystarczająca, przynajmniej na użytek przykładów prezentowanych w niniejszym rozdziale.
Jakkolwiek serwer IIS umożliwia przypisanie uprawnień do poszczególnych katalogów, znacznie bardziej atrakcyjną — i czasochłonną dla programistów — jest dynamiczna weryfikacja danych użytkownika.
Nagłówki HTTP
Informacja wynikowa produkowana prze aplikację CGI lub bibliotekę ISAPI rozpoczyna się nagłówkiem HTTP, po którym następuje pusta linia i „właściwa” zawartość. Integralną częścią nagłówka jest typ przesyłanej informacji — na przykład text/html czy image/gif — określający, jak następująca dalej informacja ma być interpretowana przez aplikację-klienta HTTP. Serwer WWW z własnej inicjatywy dodaje także — jako pierwszą linię — specjalną informację o statusie tworzonej dynamicznie strony WWW, zazwyczaj w postaci
HTTP/1.0 200 OK
oznaczającej, iż w dalszym ciągu należy spodziewać się poprawnych informacji.
Załóżmy teraz, iż informacja ta podmieniona zostanie na inną, oznaczającą np. nieautoryzowany dostęp i wymagającą logowania do dziedziny /DrBob:
HTTP/1.0 401 Unauthorized
content-type: text/html
WWW-Authenticate: Basic realm="/DrBob"
Parametr Request funkcji obsługującej zdarzenie OnAction komponentu akcji posiada właściwość Authorization zawierającą zakodowaną informację o autoryzacji logującego się użytkownika. Weryfikując tę informację mamy możliwość odpowiedniego ustawienia kodu błędu i werbalnego opisu błędnej sytuacji, za pomocą właściwości (odpowiednio) StatusCode i ReasonString parametru Response:
Wydruk 13.6 Autoryzacja żądania aplikacji-klienta WWW
void __fastcall TWebModule1::WebModule1WebActionItem7Action(
TObject *Sender, TWebRequest *Request, TWebResponse *Response,
bool &Handled)
{
AnsiString Auth = Request->Authorization;
if (Auth.Pos("Basic ") == 1) Auth.Delete(1,6);
Auth = UnBase64(Auth);
if (Auth.Pos("bswart") == 0)
{
Response->StatusCode = 401;
Response->ReasonString = "Unauthorized";
Response->WWWAuthenticate = "Basic";
Response->Realm = "/DrBob";
Response->SendResponse();
}
else
{
Response->Content = "Welcome: ["+Request->Authorization+"]=["+Auth+"])";
}
}
Bezpośrednio po wyprodukowaniu odpowiedzi Response jest ona bezzwłocznie wysyłana do przeglądarki klienta za pomocą metody SendResponse() — nie jest to konieczne, lecz pozwala uniknąć pewnej zwłoki czasowej.
Gdybyśmy powyższy przykład zastosowali w konsolowej aplikacji CGI, ujrzelibyśmy w efekcie pustą stronę — serwer WWW w dalszym ciągu produkowałby bowiem standardową informację HTTP/1.0 200 OK. w pierwszej linii, zaś kolejne informacje na temat statusu strony są po prostu ignorowane. Aby zmusić serwer WWW do rezygnacji z dołączania tej linii, należy zmienić nazwę pliku wykonywalnego CGI lub biblioteki ISAPI w taki sposób, by nazwa ta rozpoczynała się ciągiem znaków „NPH-” (Non-Protocol Header) — na przykład na NPH-WebShow.dll.
Problem z biblioteką VCL
Kod przedstawiony na wydruku 13.6 funkcjonuje bez zarzutu w przeglądarce Netscape Navigator, stwarza jednak pewien problem w przypadku Internet Explorera w wersji 5 i wyższych, a to z powodu zapamiętywania raz wprowadzonego hasła — rzecz zupełnie niedopuszczalna w sytuacji, gdy z jednego komputera korzysta więcej osób o być może sprzecznych interesach.
Kolejny kłopot polega na tym, iż ani Netscape Navigator, ani Internet Explorer nie umożliwiają wyświetlenia nazwy dziedziny (realm) w dialogu logowania, nazwa ta musi więc być jawnie przypisana właściwości Realm parametru Response — niestety, właściwość ta jest zupełnie ignorowana przez metodę SendResponse().
W treści biblioteki VCL — konkretnie w plikach IsapiAPP.pas i CgiApp.pas — znajduje się bowiem błąd, związany z implementacją metody SendResponse() klas TCGIResponse i TISAPIResponse: otóż instrukcja
AddHeaderItem(WWWAuthenticate, 'WWW-Authenticate %s'#13#10);
powinna zostać zastąpiona następującą sekwencją:
Tmp := Format('WWW-Authenticate: %s realm="%s"'#13#10, ['%s', Realm]);
AddHeaderItem(WWWAuthenticate, Tmp);
(należy oczywiście zadeklarować lokalną zmienną Tmp typu String). Wskazane jest przekopiowanie wspomnianych plików do katalogu zawierającego projekt, dołączenie ich do projektu (za pomocą opcji Project|Add to Project IDE C++Buildera), dokonanie wspomnianych zmian i przekompilowanie projektu w trybie zupełnym (Build).
Zabezpieczanie aplikacji WWW
Łatwość tworzenia aplikacji WWW za pomocą narzędzi typu RAD oraz elastyczność, z jaką aplikacje te zdolne są operować danymi, jest naturalną konsekwencją gwałtownie rosnących zastosowań Internetu w różnorodnych komercyjnych dziedzinach ludzkiej działalności — czego przykładem są sklepy internetowe, aukcje online, internetowy dostęp do kont bankowych itp. Wraz z rozwojem komercyjnych usług internetowych zwiększa się także rozmiar przesyłanych i przechowywanych danych.
C++Builder nie jest bynajmniej jedynym, ani też najbardziej popularnym, narzędziem tworzenia aplikacji WWW; z najbardziej znanych konkurentów wymienić można chociażby ASP czy Perl'a. generalnie każde narzędzie ma swoje dobre i złe strony, jednak to nie wybór konkretnego narzędzia jest najważniejszą decyzją projektową, lecz metoda bezpiecznego przechowywania danych generowanych przez tworzone aplikacje WWW.
Wspomniany wcześniej protokół SSL ogranicza się jedynie do bezpiecznego przesyłania danych pomiędzy klientem a serwerem; zabezpieczenie dostępu do przetworzonych danych jest już sprawą samej aplikacji. Niestety, ten aspekt zagadnienia bywa często niedoceniany lub kompletnie ignorowany, czego konsekwencją bywa utrata znacznej nieraz ilości danych czy po prostu kradzież informacji poufnych — przykład kradzieży ponad 300 tysięcy numerów kart kredytowych z sieci CD Universe w 1999 roku świadczy o tym, z jak wielkiej wagi zagadnieniem mamy tu do czynienia.
Solidność zabezpieczeń
Zagadnienie bezpieczeństwa danych przetwarzanych przez aplikacje sieciowe zrodziło się w konsekwencji pogodzenia dwóch sprzecznych wymagań: z jednej strony dostęp do poufnych danych powinien być jak najbardziej ograniczony, z drugiej strony udostępnianie czegokolwiek za pośrednictwem Internetu z definicji oznacza wystawienie tego czegoś na dostęp nieograniczony. Niezależnie od coraz to lepszych technik zabezpieczania danych powstają jednocześnie coraz bardziej wymyślne metody łamania tychże zabezpieczeń. Jedyne, czego tak naprawdę można być pewnym to to, iż haker jest cierpliwy i wykorzysta każdą słabą stronę zabezpieczeń, by w końcu dostać się (bezprawnie) do upragnionych danych. Rysunek 13.18 stanowi jeden z dowodów na to, jak złudne jest poczucie bezpieczeństwa oparte o hasła przechowywane w programie czy danych w jawnej postaci — przedstawia on aplikację typu ISAPI będącą de facto binarnym edytorem danych, umożliwiającą wgląd w znakową i szesnastkową reprezentację badanego obszaru. Także algorytm dynamicznego tworzenia haseł lub konkatenacji różnych łańcuchów może być często łatwo rozszyfrowany za pomocą różnej maści dekompilatorów i disasemblerów.
SCAN
Tu proszę zeskanować rysunek 13.17 ze strony 601 oryginału
Rysunek 13.18 Hackman hexadecimal editor, copyright 1996-1999 by TechnoLogismiki
Kryptografia to jest to
Nie będziemy się tutaj zagłębiać w teoretyczne podstawy kryptografii, skoncentrujemy się raczej na jej zastosowaniu do aplikacji WWW. Co prawda fundamenty matematyczne leżące u podstaw technik kryptograficznych bywają ogromnie skomplikowane, lecz użytkownik stosujący kryptografię do zabezpieczania danych niekoniecznie musi być ich świadom.
Ogólnie pojmowana „kryptografia” ma różnorodne oblicza i często rozmaite działania będące istotnie szyfrowaniem danych okazują się w efekcie nieskuteczne, a niekiedy wręcz nieprawidłowe. To bardzo niedobry znak, bowiem łańcuch rozmaitych zabezpieczeń jest tak silny, jak jego najsłabsze ogniwo. Projektanci stosujący rozmaite, ich zdaniem bardzo silne, techniki zabezpieczania danych często zapominają o tym, iż uczestniczące w tym szyfrowaniu wzorce danych przechowywane są wraz z tymi danymi, a algorytmy szyfrujące mogą zostać rozpoznane przez hakera dysponującego egzemplarzem aplikacji.
Należy zdawać sobie sprawę z faktu, iż tak naprawdę nie istnieją zabezpieczenia idealne i żadna metoda szyfrowania nie uczyni danych absolutnie bezpiecznymi. Postęp w wynajdywaniu nowych technik łamania zabezpieczeń, luki w samych zabezpieczeniach i determinacja hakerów powodują, iż każda poufna informacja prędzej czy później może stać się ich łupem.
Protokoły, algorytmy i funkcje mieszające
Protokół składa się z szeregu procedur lub etapów, z których niektóre stosować mogą szyfrowanie danych. Niedopracowanie którejś z tych procedur, czy jakakolwiek inna luka w implementacji protokołu mogą sprawić, iż najbardziej wymyślny algorytm szyfrowania okaże się bezskuteczny.
Algorytm szyfrowania jest w swej istocie funkcją matematyczną przekształcającą dane do- lub z postaci zaszyfrowanej. Z punktu widzenia stosowanych do szyfrowania kluczy algorytmy podzielić można na symetryczne — czyli stosujące ten sam klucz do szyfrowania i deszyfrowania — oraz asymetryczne, posługujące się w tym celu dwoma różnymi kluczami: publicznym do szyfrowania danych i prywatnym do ich odszyfrowywania. Przykładem algorytmów symetrycznych są DES (ang. Data Encryption Standard), Blowfish, Twofish i RC2. Do algorytmów asymetrycznych należą między innymi: RSA (od nazwisk wynalazców: Rivesta, Shamira i Adlemana), algorytm Rabina i algorytm plecakowy (Knapsack algorithm). Biorąc pod uwagę stopień komplikacji wymienionych algorytmów łatwo zrozumieć, iż próby ich złamania muszą być pracochłonne, jakkolwiek każde niedopatrzenie projektantów może tę pracochłonność drastycznie obniżyć.
Funkcje mieszające (hashes) otrzymują łańcuchy zmiennej długości i produkują na ich podstawie łańcuchy stałej długości (ang. digests). Dla wielu takich funkcji łatwo jest odgadnąć funkcję odwrotną, czyli odtworzyć łańcuch oryginalny na podstawie jego zaszyfrowanej postaci, wyjątkiem w tym względzie są jednak tzw. jednokierunkowe funkcje mieszające (ang. one-way hashes), gdzie zmiana jednego bitu w łańcuchu oryginalnym skutkuje drastyczną zmianą w jego zaszyfrowanej postaci. Funkcje takie bywają wykorzystywane do generowania unikalnych kluczy sesji dla algorytmów symetrycznych. Przykładami algorytmów szyfrujących wykorzystujących jednokierunkowe funkcje mieszające są SHA, MD4 i MD5.
Przykład — bezpieczny sklep internetowy
W charakterze przykładu rozpatrzymy prostą aplikację realizującą funkcje sklepu internetowego i przyjrzymy się problemowi zabezpieczania przetwarzanych przez nią danych. By uczynić zadanie bardziej interesującym założymy nieograniczony dostęp do bazy danych aplikacji, jej kodu źródłowego i haseł. Mimo wielu poruszanych tu zagadnień przykładowi temu daleko jest jeszcze do kompletności pod względem repertuaru faktycznie stosowanych w praktyce zabezpieczeń.
Narzędzia
Wykorzystamy hybrydową strategię ochrony, łączącą w sobie zalety algorytmów symetrycznych (szybkie szyfrowanie i deszyfracja) i asymetrycznych (większa trudność w złamaniu) — użyjemy mianowicie algorytmów Blowfish, RSA i dodatkowo SHA opartego o jednokierunkowe funkcje mieszające.
Niektóre z algorytmów szyfrujących zostały opatentowane i ich legalne wykorzystywanie uwarunkowane jest otrzymaniem licencji. Niektóre algorytmy mogą też być objęte embargiem eksportowym (importowym).
Krok 1 — Generowanie klucza prywatnego i publicznego
Algorytmy asymetryczne szyfrują dane przy użyciu klucza publicznego, zaś do ich rozszyfrowania używają klucza prywatnego. Para powiązanych kluczy „publiczny-prywatny” generowana jest z użyciem liczb pseudolosowych, przy czym parametr startowy tej generacji („ziarno” — ang. randseed) określany jest przez użytkownika (jako liczba całkowita z zadanego przedziału). Wygenerowany klucz publiczny może być przechowywany gdziekolwiek, natomiast klucz prywatny, jako stanowiący ścisłą tajemnicę serwera, powinien być przechowywany w jemu tylko znanym sekretnym miejscu. Dla zwiększenia bezpieczeństwa zestaw używanych kluczy powinien być zmieniany co pewien czas.
Krok 2 — Składanie zamówienia
Dla sprostania wymogom szybkości przy transakcji składania zamówienia wykorzystamy symetryczny algorytm Blowfish do zaszyfrowania danych użytkownika zawartych w treści zamówienia. W celu uzyskania klucza szyfrująco-deszyfrującego posłużymy się funkcjami mieszającymi wykorzystywanymi przez algorytm SHA. Bezpieczeństwo „mieszania” uwarunkowane jest w dużej mierze stopniem losowości szyfrowanych danych — jeżeli przykładowo czynnikiem „losowym” będzie jedynie czas dnia, haker może przeprowadzić analizę danych z poprzednich dni czy miesięcy; jeżeli „losowość” ta zostanie wzbogacona np. o aktualną datę, zawartość zegara procesora, aktualny stan zajętości zasobów systemu itp., zyska ona charakter wręcz niepowtarzalny i jako taka stanie się nieprzydatna dla wszelkich analiz porównawczych.
Krok 3 — Szyfrowanie danych zamówienia
Po zaszyfrowaniu danych zamówienia należy zastanowić się nad sposobem ich przechowywania. Niektóre aplikacje rozdzielają poszczególne kategorie informacji, przechowując w oddzielnych polach zaszyfrowaną nazwę użytkownika, zaszyfrowany numer karty kredytowej, zaszyfrowany czas transakcji itp. Takie systemy działają na hakerów jak przysłowiowa płachta na byka — o ile bowiem nazwiska czy adresy cechują się raczej dużym stopniem nieregularności, to nie da się tego powiedzieć o numerach kart kredytowych (często rozpoczynających się od znanych hakerom sekwencji cyfr) i o terminach upływu ich ważności. Znacznie bezpieczniejsze jest łączenie poszczególnych pól w jeden blok i przechowywanie go w całości w postaci zaszyfrowanej (ang. CBC —Cipher Block Chaining); wydzielenie poszczególnych pól jest wówczas prawie niemożliwe.
Jeżeli separatorami poszczególnych pól przy łączeniu ich w pojedynczy blok były przecinki, rozbiór odszyfrowanego bloku na poszczególne pola łatwo można przeprowadzić posługując się właściwością CommaText klasy TStringList.
Krok 4 — Szyfrowanie użytego klucza
Unikalny klucz sesji niezbędny jest do odszyfrowania danych zapisanych w serwerze, nie może on być jednak przechowywany w postaci jawnej. Do jego zaszyfrowania użyjemy asymetrycznego algorytmu RSA. W sposób opisany w „Kroku 1” uzyskujemy klucz publiczny (którym szyfrujemy unikalny klucz sesji) i klucz prywatny (który posłuży później do odszyfrowania klucza sesji, a na razie powinien być przechowywany w miejscu znanym tylko serwerowi, na innym komputerze.
Krok 5 — Odczytywanie danych zamówienia
Aby odszyfrować dane związane z zamówieniem, aplikacja WWW odczytuje (z sobie tylko znanej lokalizacji) klucz prywatny i za jego pomocą odszyfrowuje zaszyfrowany unikalny klucz sesji. ten ostatni służy następnie do odszyfrowania przedmiotowych danych.
Krok 6 — Atak hakera
Jak już pisaliśmy wcześniej, nie ma zabezpieczeń absolutnie bezpiecznych, tak więc w przypadku konkretnej metody zabezpieczenia podstawowym pytaniem jest nie to, czy może być ona złamana, lecz to, jak wiele czasu i zasobów wymagałaby uwieńczona sukcesem próba jej złamania. W przypadku dobrego algorytmu szyfrującego próby rozszyfrowania zamówienia mogą trwać tak długo, aż dane zawarte w tym zamówieniu stracą jakąkolwiek użyteczność.
Powracając do analogii z najsłabszym ogniwem łańcucha, przeanalizujmy pod tym kątem przedstawiony schemat zabezpieczeń. Pierwszym z „ogniw” są klucze używane przez algorytm RSA — sam algorytm należy do bardzo bezpiecznych, natomiast bezpieczeństwo to może zostać zniweczone przez…nierozważny wybór miejsca, w którym przechowywany będzie klucz prywatny. Drugim ogniwem jest generowanie unikalnego klucza sesji — przy dostatecznym stopniu losowości algorytmu SHA trudno będzie hakerowi odgadnąć postać tego klucza.
Algorytm Blowfish wykorzystywany do szyfrowania danych zamówienia jest trzecim ogniwem omawianego łańcucha. Algorytm ten jest obecnie postrzegany jako jeden z najlepszych i bardzo trudny — o ile nie wręcz niemożliwy — do złamania.
Czwartym i ostatnim ogniwem jest monolityczne (CBC) szyfrowanie bloku stanowiącego konkatenację (lub inny sposób kombinacji) poszczególnych pól — przechowywanie informacji w postaci oddzielnych pól czyniłoby niniejsze ogniwo niezmiernie słabym.
HTML i XML
Jedną z najczęściej wymienianych nowości C++Buildera 5 jest obsługa języka XML (ang. Extensible Markup Language) skwapliwie wykorzystywanego przez komponenty grupy InternetExpress. W niniejszej sekcji przedstawimy przykład wykorzystania komponentów tej grupy i wyjaśnimy, dlaczego XML jest dla nich idealnym językiem komunikacji.
XML
XML jest podzbiorem języka SGML (ang. Standard Generalized Markup Language), zaś HTML jest aplikacją (zastosowaniem) SGML. Tym co odróżnia XML od HTML jest zestaw reguł syntaktycznych tego ostatniego. Dokument XML również posługuje się sformalizowanym zbiorem reguł, lecz reguły te ustalane są przez autora dokumentu i specyfikowane w tzw. definicji typu dokumentu (ang. DTD — Document Type Definition); teoretycznie więc możliwe jest stworzenie DTD opisującego reguły syntaktyczne HTML. Tak więc dokument XML w połączeniu ze swą specyfikacją DTD stanowi samoopisującą całość, co jest niewątpliwie bardzo użyteczne; sam dokument XML bez towarzyszącej mu specyfikacji DTD jest na ogół niezrozumiały.
Tym co stanowi główną zaletę XML jest jego prosty, czytelny format. W połączeniu z możliwością arbitralnego ustalania reguł syntaktycznych w postaci DTD możliwe jest więc opisanie w postaci XML niemal wszystkiego — od strony WWW i książki, poprzez bazy danych aż po kompletne systemy operacyjne i systemy plików. W szczególności więc język ten mógłby posłużyć także do zapisu informacji produkowanych przez oprogramowanie, jak również do komunikacji pomiędzy częściami (warstwami) aplikacji rozproszonej.
Na razie jednak język XML nie wydaje się być czynnikiem zdolnym przełamać komputerową wieżę Babel; rok-dwa lata temu wiele narzędzi i aplikacji zdawało się wykazywać tendencję do eksportowania i importowania danych w formacie XML, podobnie jak dzisiaj większość aplikacji zwraca się (jednak) ku językowi HTML. Nie oznacza to bynajmniej, iż HTML ma szansę stać się komputerowym Esperanto — tak jak nie sprawdziły się przewidywania sprzed pięciu lat, iż Java zdolna jest wyprzeć inne języki programowania (a propos, czy przypominasz sobie pakiet biurowy Corel's Java?).
Elementy obsługi XML spotykamy już w technologii MIDAS (Multitier Distributed Application Services) gdzie serwer aplikacji (DataSetProvider) zdolny jest komunikować się z klientem będącym serwerem WWW (XMLProvider) który z kolei współpracuje z komponentem—producentem MidasPageProducer. W poprzednich wersjach MIDASa komponenty te wymieniały pomiędzy sobą informację w specyficznym dla siebie formacie, w obecnej wersji jest to format XML, pozbawiony jednak specyfikacji DTD, w praktyce więc zrozumiały jedynie dla aplikacji stworzonych za pomocą Delphi 5 lub C++Buildera 5.
Innymi słowy, mimo iż serwery i klienty MIDASa stworzone za pomocą C++Buildera 5 mogą wymieniać ze sobą informację w formacie XML, to jednak w dalszym ciągu nie jest możliwe przyłączenie do DataSetProvider'a klienta stworzonego w PowerBuilderze, ani też zmuszenie serwera Oracle8i do produkowania dokumentów XML zrozumiałych przez XMLBroker. Nawet w przypadku Delphi 5 dokument XML produkowany przez DataSetProvider wydaje się być użyteczny jedynie dla komponentu XMLBroker, który współpracuje jedynie z producentem MidasPageProducer. Dane pomiędzy DataSetProvider'em a „regularnym” klientem (ClientDataSet) przesyłane są w dalszym ciągu w specyficznym, nieudokumentowanym formacie.
W tym kontekście komponenty grupy InternetExpress wydają się być przełomem, bowiem komunikacja pomiędzy warstwami aplikacji odbywa się w formacie XML, jakkolwiek wymieniane dokumenty zrozumiałe są jedynie w ramach technologii MIDAS. Cóż, to dopiero początek…
InternetExpress
Komponenty InternetExpress łączą w sobie elementy technologii MIDAS i WebBroker, stanowią więc udany mariaż technologii aplikacji wielowarstwowych i mechanizmów internetowych. Stworzone z ich udziałem aplikacje (w środowisku Delphi, C++Buildera i JBuildera) pełnią rolę klientów współpracujących z serwerami MIDASa.
Przykład — kartoteka zamówień
W naszym przykładowym projekcie aplikacje klienckie stworzone za pomocą komponentów InternetExpress pobierają dane z serwera MIDASa. Serwerowi temu poświecony jest rozdział 19, tam też znajduje się kompletny projekt, którego celem jest zarejestrowanie i uruchomienie tego serwera na lokalnym komputerze. W niniejszym rozdziale ograniczymy się tylko do „klienckiej” strony aplikacji.
TMidasPageProducer
Zakładając, iż projekt serwera z rozdziału 19 został zarejestrowany, powróćmy do naszej aplikacji ISAPI Webshow.dll; zawiera ona jeszcze dwa komponenty akcji, których dotąd nie wykorzystywaliśmy: /browse i /final. Poświęcimy im pozostałą część niniejszego rozdziału.
Niewątpliwie musimy rozpocząć od połączenia z serwerem MIDASa. Jako że jest to połączenie ze zdalnym modułem danych (po stronie serwera), użyjemy komponentu TDCOMConnection ze strony MIDAS Palety Komponentów. Komponent ten należy umieścić w module WWW naszej aplikacji i za pośrednictwem jego właściwości ServerName należy wybrać pozycję „MidasServer.CustomerOrders” — pierwszy człon nazwy jest nazwą serwera, drugi natomiast nazwą ko-klasy zdalnego modułu danych na tymże serwerze (stąd prosty wniosek, iż pojedynczy serwer MIDASa może posiadać kilka modułów danych). Aby ostatecznie nawiązać połączenie z serwerem, należy ustawić na true właściwość Connected komponentu TDCOMConnection — zarejestrowany serwer MIDASa zostanie uruchomiony, manifestując ten fakt stosowną ikoną na Pulpicie. Możemy więc już pobierać dane z jego modułu danych — potrzebne nam będą do tego dwa komponenty ze strony InternetExpress Palety Komponentów: TXMLBroker i TMidasPageProducer, umieśćmy więc po jednym egzemplarzu każdego z nich w naszym module WWW i przypiszmy komponent TDCOMConnection do właściwości RemoteServer komponentu TXMLBroker. Musimy jeszcze przypisać do właściwości ProviderName odpowiedni komponent TDataSetProvider zlokalizowany na zdalnym serwerze — w naszym przypadku rozwijalna lista zawiera tylko jedną pozycję (DataSetProvider1) choć sam fakt wyboru świadczy o tym, iż serwer MIDASa może posiadać kilka komponentów tego typu.
Połączenie z serwerem zostało już w pełni nawiązane, przystąpmy więc do projektowania wynikowej strony WWW produkowanej przez TMidasPageProducer.
Web Page Editor
Na pierwszej pozycji menu kontekstowego komponentu TMidasPageProducer znajduje się opcja Web Page Editor — jej wybranie spowoduje uruchomienie edytora o takiej właśnie nazwie; za jego pomocą możemy zaprojektować wygląd wynikowej strony WWW. Edytor umożliwia podgląd zawartości strony zarówno w formacie źródłowym HTML, jak i w postaci typowej dla przeglądarki WWW (patrz rys. 13.19). W górnej części okna edytora znajdują się dwa panele: lewy ukazuje komponenty użyte na potrzeby TMidasPageProducer'a, zaś prawy — komponenty potomne (pierwszego poziomu) komponentu podświetlonego aktualnie w lewym panelu.
Mimo iż Web Page Editor umożliwia tworzenie komponentów, nie posiada on niczego na kształt Palety Komponentów. Tworzenie nowych komponentów odbywa się tu bowiem za pomocą dialogu New Component uruchamianego z menu kontekstowego górnych paneli. Dialog ten oferuje do wyboru komponenty, które mogą stać się komponentami potomnymi w stosunku do pozycji podświetlonej aktualnie w lewym panelu. Początkowo panel ten zawiera jedyną pozycję reprezentującą TMidasPageProducer, możemy wówczas utworzyć jeden z trzech komponentów potomnych: DataForm, QueryForm lub LayoutGroup. Gdy wybierzemy DataForm i uruchomimy dialog ponownie, zaoferuje on nam do wyboru komponenty DataGrid, DataNavigator, FieldGroup i LayoutGroup. Wybierzmy (trzymając klawisz Ctrl) jednocześnie dwa: DataNavigator i FieldGroup.
Edytor ostrzeże nas w tym momencie (w oknie podglądu), iż wskaźniki DataNavigator1.XMLComponent i FieldGroup2.XMLBroker są wskaźnikami pustymi (nil). Ustawmy więc pierwszy z wymienionych wskaźników na komponent FieldGroup, wyniku czego zniknie pierwsze z ostrzeżeń. Drugiemu wskaźnikowi przypiszmy komponent XMLBroker1 (z modułu WWW) w wyniku czego pozbędziemy się również drugiego ostrzeżenia. Jednocześnie w oknie podglądu ukaże się lista wszystkich pól kartoteki kontrahentów, wraz ze specjalnym polem statusu (jako ostatnim). Pole to zawiera jednoznakowy znacznik informujący, czy dany rekord został wstawiony, zmodyfikowany albo usunięty, i jako takie jest może interesujące dla projektanta, lecz jego użyteczność dla użytkownika końcowego jest raczej wątpliwa. Domyślnie widok zawiera wszystkie pola kartoteki, możemy jedna łatwo usunąć niepotrzebne z nich i zmienić kolejność pozostałych, wybierając w lewym panelu komponent FieldGroup i następnie kolejno uruchamiając menu kontekstowe poszczególnych pozycji na prawym panelu, co pokazuje rysunek 13.19.
SCAN
Tu proszę zeskanować rysunek 13.18 ze strony 609 oryginału
Rysunek 13.19 Web Page Editor na etapie projektowania
Uruchomienie aplikacji
Przed uruchomieniem aplikacji stworzonej w technologii InternetExpress musimy wpierw upewnić się, czy biblioteka ISAPI (WebShow.dll) znajduje się w katalogu skryptów serwera WWW pracującego w lokalnym komputerze. Aby zapewnić, iż zawsze będziemy mieli do czynienia z najnowszą wersją biblioteki, najlepiej ustawić ten katalog jako wynikowy dla produkowanych przez projekt binariów. Nie uda się to jednak w przypadku serwera IIS, który nie pozwoli na nadpisanie używanej biblioteki ISAPI.
Następnie musimy przypisać komponent TMidasPageProducer do właściwości Producer tego komponentu akcji, który odpowiada żądaniu /browse.
W końcu musimy zapewnić aplikacji dostęp do specjalnych plików JavaScriptu, niezbędnych do analizy generowanych pakietów XML. Pliki te znajdują się na dysku dystrybucyjnym C++Buildera 5 w katalogu CBuilder5\Source\WebMidas, zajmując łącznie około 60KB, najlepiej więc przekopiować je do katalogu skryptów serwera WWW, jednocześnie przypisując ten katalog właściwości IncludePathURL komponentu TMidasPageProducer (w przypadku posługiwania się programem IntraBob należy pliki te przekopiować do katalogu, w którym program rezyduje wraz z biblioteką ISAPI i ustawić wspomnianą właściwość na „./”).
Po wykonaniu powyższych czynności należy ponownie skompilować projekt WebShow w trybie Build i uruchomić bibliotekę WebShow.dll używając Internet Explorera w wersji 4 lub wyższej, Netscape Communicatora w wersji 4 lub wyższej albo programu IntraBob w wersji 5 — uzyskany widok przedstawia rysunek 13.20.
SCAN
Tu proszę zeskanować rysunek 13.19 ze strony 610 oryginału
Rysunek 13.20 Strona WWW wyprodukowana przez InternetExpress w odpowiedzi na żądanie /browse
Zagnieżdżone tabele — układ „master-details”
W ostatnim przykładzie niniejszego rozdziału zaprezentujemy wyświetlenie zamówień przyporządkowanych poszczególnym klientom w relacji „master-details”, podobnie jak na rysunku 13.16 — z tą jednak różnicą, iż wyświetlenie to odbędzie się w całości w pojedynczym oknie, z możliwością pełnego nawigowania. W tym celu uruchom ponownie Web Page Editor (za pomocą menu kontekstowego komponentu TMidasPageProducer) i dodaj do formularza DataForm komponent DataGrid i kolejny komponent DataNavigator. Następnie przypisz do właściwości XMLBroker komponentu DataGrid (jedyny) komponent TXMLBroker. Spowoduje to zniknięcie ostrzeżenia, lecz wynik będzie inny od oczekiwanego: zamiast pól tabeli zamówień (Orders) ukażą się pola tabeli kontrahentów (Customers). Musisz więc przejść do komponentu FieldGroup1 (w lewym panelu) i przypisać do jego właściwości XMLDataSetField tabelę TableOrders jako tabelę zagnieżdżoną. Ponieważ standardowo ukażą się wszystkie pola zamówień, pozostaw tylko te najważniejsze (usuwając pozostałe w sposób wcześniej opisany), w tym również pole StatusColumn1, które tym razem zawierać będzie użyteczną dla nas informację.
Szerokość produkowanego widoku zależna jest oczywiście od szerokości wyświetlanych pól, co widać na rysunku 13.12; niektóre z nich przydałoby się nieco zwęzić — na przykład szerokość pól zawierających datę można by zmniejszyć do 12 (modyfikując właściwość DisplayWidth), można by też skrócić tytuły niektórych pól, zmieniając np. „PaynmentMethod” na „Paynment”.
SCAN
Tu proszę zeskanować rysunek 13.20 ze strony 611 oryginału
Rysunek 13.21 Web Page Editor w czasie projektowania widoku „master-details”
Mimo, iż nasz widok ma na razie wygląd nieco surowy — bez szczególnej kolorystyki, czcionek, grafiki itp. — rozpoczniemy testowanie naszej aplikacji. Jej atrakcyjna szata graficzna jest co prawda dość istotna, jednak najważniejsze jest oczywiście bezbłędne działanie.
Widok wyświetlony w odpowiedzi na żądanie /final przedstawiony jest na rysunku 13.22.
SCAN
Tu proszę zeskanować rysunek 13.21 ze strony 612 oryginału
Rysunek 13.22 Ostateczny widok w układzie „master-details”
Wyświetlana strona zawiera wszystkie żądane elementy: HTML z tabelą i definicjami wprowadzanych danych odpowiada za wygląd, XML zawiera tabelę i definicje pól zgodnie z aktualnymi danymi, wreszcie JavaScript dokonuje analizy danych XML i wbudowuje je w informację wejściową dla strony WWW. To wszystko!
Zauważyłeś zapewne, iż podczas przeglądania danych wyświetlanie strony WWW wolne jest od migotania — „stała” część strony pozostaje bowiem niezmienna, zmieniają się tylko zawartości kontrolek edycyjnych. To efekt współpracy XML i JavaScriptu.
Nasz projekt ma niestety jedną słabą stronę — dla danego kontrahenta ściągany jest kompletny zbiór jego zamówień, co w przypadku dużych baz może niesamowicie spowolnić wyświetlanie. Lekarstwem na tę przypadłość jest ustawienie właściwości MaxRecords komponentu TXMLBroker na taką liczbę rekordów, jaką chcemy faktycznie zobaczyć.
Spoglądając na rysunek 13.12 zauważymy, iż pole „CustNo” ma wygląd nieco inny niż pozostałe; otóż pole to, jako sprzęgające tabele „master” i „details”, jest polem tylko do odczytu. Pozostałe pola można swobodnie modyfikować, co dla każdego zmodyfikowanego rekordu spowoduje ukazanie się literki M w polu statusu.
Należy jeszcze zwrócić uwagę na ważny fakt, iż wszelkie zmiany dokonywane na wyświetlanym widoku mają na razie charakter prowizoryczny — baza na zdalnym serwerze pozostaje niezmieniona. Aby spowodować utrwalenie poczynionych zmian, należy kliknąć w przycisk „Apply Updates”; spowoduje to wysłanie do serwera pakietu XML z żądaniem aktualizacji, w wyniku czego ze strony serwera nadejść mogą komunikaty o błędach wymagających rozwiązania (ang. reconcilation).
Tak mi szaro…
Mimo poprawnego działania nasza aplikacja prezentuje się nieco ponuro, niczym (nie przymierzając) podręczne narzędzie programistyczne, nie zaś produkt mający dawać zadowolenie użytkownikowi. Jednym ze sposobów nadania jej bardziej atrakcyjnego wyglądu jest edycja właściwości HTMLDoc komponentu TMidasPageProducer; rozwiązaniem bardziej elastycznym jest zapisanie szablonu strony w zewnętrznym pliku o ustalonej nazwie, przypisanej właściwości HTMLFile tegoż komponentu, co umożliwi użytkownikowi modyfikowanie wyglądu strony bez konieczności sięgania do kodu źródłowego projektu.
Można także manipulować właściwościami poszczególnych kolumn w ramach komponentu FieldGroup lub właściwościami poszczególnych komórek w ramach komponentu DataGrid (w edytorze Web Page Editor).
Przykładowy wygląd uatrakcyjnionego widoku przedstawia rysunek 13.23.
SCAN
Tu proszę zeskanować rysunek 13.22 ze strony 613 oryginału
Rysunek 13.23 Widok „master-details” w nowej szacie graficznej
Podsumowanie
Zakończony właśnie rozdział poświęcony został zagadnieniu programowania serwerów WWW w oparciu o technologię WebBroker i związane z nią komponenty C++Buildera. Stworzyliśmy prosty, lecz w pełni funkcjonalny projekt oparty na bibliotece typu ISAPI ilustrujący podstawowe zasady budowania aplikacji-rozszerzeń serwera i prezentujący osiągane dzięki temu rezultaty. Poruszyliśmy także elementarne zagadnienia związane z bezpieczeństwem aplikacji WWW.
Na zakończenie zajęliśmy się komponentami z grupy InternetExpress, ilustrując prostymi przykładami ich współpracę z serwerem MIDASa.
Algorytm RSA opiera swe bezpieczeństwo na różnicy pomiędzy trudnością stwierdzenia, czy duża (powiedzmy 200-cyfrowa) liczba naturalna jest liczbą pierwszą (co jest potrzebne do wygenerowania kluczy) a trudnością rozkładu iloczynu dwóch liczb pierwszych na czynniki (umożliwiałoby to obliczenie klucza prywatnego na podstawie klucza publicznego). Przy obecnym stanie wiedzy algorytmicznej rozłożenie na czynniki 400—cyfrowej liczby będącej iloczynem dwóch liczb pierwszych zajęłoby najszybszym nawet (obecnym) komputerom średnio kilka milionów lat (przyp. tłum.)
2 Część I ♦ Podstawy obsługi systemu WhizBang (Nagłówek strony)
2 E:\HELION\CPP Builder 5\r13\r-13-00.doc