例如,如果以表格的形式顯示數據,則可以很容易地把每個列標題轉換為可以選擇的連接,以便將該列的數據重新排序。它允許單擊一次就可以以不同的方式查看數據,而又不必鍵入任何查詢。或者可以提供一種用戶可以為數據庫搜索而鍵入的標准格式,然後,顯示含
有搜索結果的頁面。像這種簡單的能力能夠特別地改變為訪問數據庫內容而提供的交互性的水平。除此之外,Web 浏覽器的顯示能力比在終端窗口獲得的能力要明顯地更好一些,所以,輸出也經常看起來更漂亮。
在這部分,我們將創建下面的基於Web 的腳本:
samp_db 數據庫中表的通用浏覽器。這與我們想對這個數據庫完成的任何特定的任務無關,但是它舉例說明了若干Web 程序設計概念,並提供了一種查看這些表所含有的信息的方便方式。
允許我們查看任何給定的測驗或測試分數的分數浏覽器。它作為回顧評分事件結果的快速方式是很方便的,並且當我們需要創建測試的等級曲線時,它是有用的,所以我們可以以字母等級來標記試卷。
尋找分享共同興趣的歷史同盟成員的腳本。通過允許用戶輸入搜索短語來完成它,然後在member 表的interests 域來搜索短語。我們已經編寫了一個行命令腳本來做這些,但是,基於Web 的版本提供了有指導意義的參考觀點,允許對同一任務比較兩種方法。
我們將使用CGI.pm Perl 模塊來編寫這些腳本,這個模塊是將DBI 腳本連接到Web 上最容易的方法(有關獲得CGI.pm 模塊的說明,請參閱附錄A)。之所以稱為C G I . p m,是因為它有助於編寫使用公共網關協議的腳本,這個協議定義了Web 服務器如何與其他程序通信。CGI.pm 處理涉及了許多通用內務處理的任務細節,如收集通過Web 服務器傳遞到腳本的作為輸出的參數值。CGI.pm 也提供了生成HTML 輸出的便利方法,與編寫自己原始的H T M L 標記相比,它減少了編寫難看的Html 的機會。
在本章中,您將學到足夠有關CGI.pm 的知識來編寫自己的Web 應用程序,但是,當然不是它所包括的所有性能。要想學習有關這個模塊的更多知識,請參閱Lincoln Stein (John Wiley 1998 出版) 撰寫的《O fficial Guide to Programming with CGI.pm》,或在以下網址查閱聯機文檔:
http://stein.cshl.org/www/software/CGI/
設置CGI 腳本的apache
除了DBI 和CGI.pm 之外,編寫基於Web 的腳本還需要有一個以上的組件:Web 服務器。這裡的說明適合apache 服務器使用腳本,但是,如果願意,稍微改編一點這些說明,就可以使用不同的服務器。
一般來說,Apache 裝置的各個部分位於/usr/local/apache 目錄。對我們的目的來講,這個目錄中最重要的子目錄為h t d o c s(Html 文檔樹)、CGI-bin (可執行的腳本和We b服務器調用的程序),和c o n f(配置文件)。這些目錄也可能放在系統中的其他地方。如果是這樣,則要對下面的注意事項做適當的調整。
應該驗證CGI-bin 目錄不在apache 文檔樹的內部,以便它內部的這些腳本不能作為無格式文本來請求。這是個安全的防范方法。您也不願意讓懷有惡意的客戶機程序檢查您的腳本,通過提取這些腳本的文本並研究它們來作為安全的突破口。
要想安裝以apache 方式使用的CGI 腳本,則將它放在CGI-bin 目錄下,然後將這個腳本的所有權更改為運行Apache 的用戶,並將它的模式更改為對該用戶為可執行的和只讀的模式。例如,如果apache 以名稱為www 的用戶方式運行,則使用下面的命令:
% chown www script_name
% chmod 500 script_name
可能需要用www 或root 運行這些命令。如果不允許在CGI-bin 目錄下安裝腳本,則可以請求系統管理員代表您來這樣做。安裝這個腳本之後,通過向Web 服務器發送適當的U R L,可以請求浏覽器上的這個腳本。典型的URL 是這樣的:
http://your.host.name/CGI-bin/script_name
從Web 浏覽器請求腳本會導致Web 服務器執行它。返回腳本的輸出,結果作為We b 頁面出現在浏覽器中。
如果為尋求更好的性能而使用具有mod_perl 的CGI 腳本,則可以這樣做:
1) 確保至少有以下版本的必需軟件: Perl 5.004、CGI.pm 2.36和mod_perl 1.07。
2) 確保將mod_perl 編譯為apache 可執行的文件。
3) 建立一個存儲腳本的目錄。我使用了/usr/local/apache/cgi-perl。cgi-bin 不應該位於apache文檔樹的內部,出於同樣的安全原因, CGI-perl目錄也不應該在那裡。
4) 告知apache,與位於CGI-perl 目錄中的腳本mod_perl 相關聯:
如果正在使用Apache 的當前版本,這個版本使用單個的配置文件,則將所有這些指示放在httpd.conf 中。如果apache 的版本使用三個舊文件的方法來配置信息,則將A l i a s指示放入srm.conf 中,將Location 行放入Access.conf 中。對於cgi-perl 目錄,不要啟用m o d _ per l、PerlSendHeader 或PerlSetupEnv 指示。這些由CGI.pm 自動地處理,啟用它們可能導致處理沖突。
mod_perl 腳本的URL 與標准的CGI 腳本的URL 相類似。唯一的不同之處在於指定cgi - perl 而不是CGI - bin。
http://your.host.name/CGI-perl/script_name
有關的詳細信息,請參閱下面地址的apache Web 站點的mod_perl 區域:
http://perl.apache.org/
CGI.pm 的簡要介紹
為了編寫使用CGI.pm 模塊的Perl 腳本,將use 行放在這個腳本的開頭附近,然後創建讓您訪問CGI.pm 方法和變量的CGI對象:
use CGI;
my($cgi)=new CGI;
我們的CGI 腳本使用了CGI.pm 的性能,它通過使用$CGI 變量調用方法來實現。例如,為了生成級別1標題,我們將這樣使用h1( ) 方法:
print $CGI->h1("My Heading");
CGI.pm 也支持允許以函數調用它的方法的使用風格,而不用前導的‘ $ c g i - >’。在這裡,我沒有使用這個語法,是因為‘ $ c g i - >’符號更類似於使用DBI 的方式,還因為它防止C G I . p m函數名與可以定義的任何函數名產生沖突。
1. 檢查輸入參數,並編寫輸出
CGI.pm 所做的事情之一就是照看所有丑陋的細節,這些細節涉及到收集由We b服務器向腳本提供的輸入信息。為了獲得那些信息,所需做的就是調用param( ) 方法。可以如下獲得所有可用的參數名:
my (@param)=$CGI->param();
為了檢索特定參數的值,只命名感興趣的參數:
CGI.pm還提供生成傳送給客戶機浏覽器的輸出方法。考慮下面的Html文檔:
這個代碼使用$CGI來產生等價的文檔:
使用CGI.pm 生成輸出,而不是編寫自己原始的H T M L,這樣做的一些優點是,可以按邏輯單元考慮,而不是按單獨的組成標識來考慮,而且Html 不太可能含有錯誤(我說“不太可能”的原因是CGI.pm 不禁止做古怪的事情,如含有一列內部的標題)。除此之外,對於
編寫的非標記文本,CGI.pm 提供自動的字符轉義,如Html 中指定的‘<’和‘>’。
如果願意,CGI.pm 生成輸出方法的使用並不排斥編寫自己原始的H T M L。可以將這兩種方法混合起來,組合調用具有生成文字標識的顯示語句的CGI.pm 方法。
2. 轉義的Html 和URL 文本
如果經CGI.pm 方法,如start_Html( ) 或h1( ),編寫非標記的文本,則自動地轉義文本中的特定字符。例如,如果使用下面的語句生成標題,則標題文本中的‘ &’字符將由C G I . p m 轉換為‘& a m p ;’:
print $CGI->start_Html (-title=>"A,B&C");
如果不使用CGI.pm 生成輸出的方法編寫非標記的文本,則可能應該先讓它經過escapeHtml( ) ,以便確保可以正確地轉義任何指定的字符。當構造可能含有特定字符的URL 時也是這樣,盡管在那種情況下應該使用escape( ) 方法來代替它。使用適當的編碼方法是很重要的,因為每種方法都將不同的字符集作為特殊的字符來對待,並使用彼此不同的格式來對待特殊的字符編碼。考慮下面簡短的Perl 腳本:
如果運行這個腳本,則它生成下面的輸出,從這裡可以看到Html 文本的編碼不同於URL 的編碼:
3. 編寫多目的頁面
編寫基於Web 的腳本來生成H T M L,而不是編寫靜態的Html 文檔的主要原因之一是,根據調用方式,腳本可以產生不同類型的頁面。我們將要編寫的所有CGI 腳本都有這種特性。每一個都像下面這樣操作:
1) 當從浏覽器第一次請求這個腳本時,它生成一個初始頁面,允許選擇想要的信息類型。
2) 當做了選擇以後,重新調用這個腳本,但是,這次它在第二頁檢索,並顯示請求的特定信息。
這裡的主要問題是想從第一頁的選擇中確定第二頁的內容,但是,通常Web 頁面是彼此獨立的,除非安排某些特定排列的次序。這個竅門是讓腳本生成頁面,這個頁面給參數設置一個值,告訴這個腳本的下一個調用想要的內容。當第一次調用這個腳本時,這個參數沒有
值;告訴這個腳本給出它的初始頁面。當指出想看的信息內容時,這個頁面再一次調用這個腳本,但是,將參數設為指示這個腳本做什麼的一個值。
將說明從頁面傳送回腳本有不同的方式。一種方式是提供一種用戶填寫的表格。當用戶提交這張表格時,將它的內容提交給Web 服務器。服務器將信息傳遞給腳本,這個腳本通過調用param( ) 方法,能夠找出提交的內容。這就是我們對第三個CGI 腳本所做的事情(允許用戶輸入搜索歷史同盟目錄的關鍵字)。
對腳本指定說明的另外一種方法是,當請求腳本時,將信息作為發送到We b服務器的U R L的一部分來傳遞。這就是我們對於samp_db 表浏覽器和分數浏覽器腳本要做的事情。這種工作方式是腳本生成含有超鏈接的頁面。選擇一個連接,再次調用這個腳本,但是,這次
指定參數值,這個參數值指示這個腳本做什麼。實際上,這個腳本以不同的方式調用它本身,來提供不同類型的結果,這取決於用戶所選擇的連接。
腳本可以允許通過向浏覽器向它自己的URL 傳送一個含有超鏈接的頁面來調用它本身。例如,腳本my_script 可以編寫含有如下這樣連接的頁面:
<A HREF="/CGI-bin/my_script">Click Me!</A>
當用戶敲入文本“ Click Me!”時,用戶浏覽器就請求將my_script 發送回Web 服務器。當然,所有這些會導致腳本再次發送出同一個頁面,因為它不支持其他信息。然而,如果將一個參數附加到URL 上,則當用戶選擇這個連接時,將這個參數送回Web 浏覽器。服務器
調用這個腳本,這個腳本可以調用param ( ) 來偵測設置的參數,並根據它的值采取行動。
為了把參數附到URL 的末尾,加一個“?”字符放到名稱/值的前面。為了附上多個參數,用字符“&”分隔。例如:
/CGI-bin/my_script?name=value
/CGI-bin/my_script?name=value&name2=value2
為了構造帶有附加參數的自引用的U R L,C G I腳本應該通過調用script_name ( ) 方法獲得自己的U R L來開始,然後像按照如下方法添加參數:
在構造U R L之後,通過使用CGI.pm 的a( ) 方法,可以生成一個包括它的超鏈接<A> 標記:
print $CGI->a ({.href=>$url},"Click Me!);
通過檢查一個簡短的CGI 腳本來查看如何工作會更容易。第一次調用時,下面的腳本f l i p _ f l o p,給出了一個含有單個超鏈接的稱為頁面A 的頁面。選擇這個連接再次調用這個腳本,但是設置page 參數,告訴它顯示頁面B。頁面B也包括對腳本的連接,但是page 參數沒有值。因此,在頁面B中選擇這個連接導致重新顯示原始頁面。隨後的腳本調用將頁面在腳本A和腳本B之間來回切換:
如果另一個客戶機程序出現並請求f l i p _ f l o p,就給出初始頁面,因為不同客戶機的浏覽器並不互相影響。
實際上,$url 的值被前面的樣例設置成漂亮的風格。在把它們放在URL 之後以免包括特殊字符時,使用escape( ) 方法對參數名和值進行編碼是比較好的。這裡有一個較好的方法來用附加的參數值來構造U R L:
從Web 腳本連接到MySQL服務器
我們在前一節“運行DBI”中開發的命令行腳本,為建立到MySQL服務器的連接共享了一個通用的前文。CGI 腳本也共享了一些代碼,但是有一些不同:
這個前文與命令行腳本使用的前文的不同之處在於以下幾個方面:
第一部分現在含有一條use CGI 語句。不再分析命令行的參數。
代碼仍然在可選文件中尋找連接參數,但是,在用戶執行腳本的主目錄中不使用.my.cnf 文件(也就Web 服務器用戶的主目錄)。Web 服務器可能運行訪問其他數據庫的腳本,沒有理由假設所有腳本會使用同一連接參數。相反,我們尋找不同位置存放的可選文件( / us r / l o c a l / a p a c h e / c o n f / samp_db . c n f)。如果想使用不同的文件,應該修改可選文件的路徑名。
通過Web 服務器調用的腳本作為Web 服務器用戶,而不是作為您來運行。這就提出了一些安全問題,因為在Web 服務器接管之後您就不再控制了。應該把可選文件的所有權交給運行Web 服務器的用戶(可能是www 或者nobody 或者一些類似的用戶),並將模式設置為4 0 0 或6 0 0,以便其他用戶不能讀取。不幸的是,可以安裝這個Web 服務器的腳本來執行的任何人仍然能夠讀取這個文件。他們要做的所有事情就是編寫一個腳本,顯式地打開可選文件,並在We b頁面上顯示它的內容。因為他們的腳本作為We b服務器用戶來運行,所以它將有足夠的權利來讀取這個文件。
由於這個原因,創建一個對samp_db 數據庫具有只讀( S E L E C T)權限的MySQL用戶,然後在samp_db.cnf 文件中列出這個用戶的名稱和口令,而不是您自己的名稱和口令,這種行為是很謹慎的。作為有權修改數據庫的表的用戶,這種方式不會冒險允許腳本連接到數據庫。第11章“常規的MySQL管理”,討論了如何創建具有嚴格權限的MySQL用戶賬戶。
另一種選擇,可以在apache 的suEXEC 機制下安排執行腳本。這就允許作為特殊權限的用戶執行腳本,然後編寫腳本,從只對那個用戶為只讀的可選文件中獲得連接參數。例如,需要編寫訪問數據庫的腳本,就可以這樣做。
還有另外一種方法就是編寫腳本,從客戶機用戶請求用戶姓名和口令,並使用這些值建立到MySQL服務器的連接。這種方法對於為管理目的而創建腳本比對於為一般使用提供腳本更適合。無論如何,應該警惕用戶名和口令請求的一些方法受到一些人的攻擊,這些人可
能在您和服務器之間的網絡上安放竊聽器。
因為可以從前面的段落中搜集,所以Web 腳本的安全性是個棘手的問題。很明顯,應該多讀一些有關安全的主題,因為它是一個大的主題,所以在這裡我不能真正做得很全面。查看apache 手冊中有關安全性的資料是一種好的方法。您也可以查找WWW 安全性的FAQ 說
明,例如可以使用下面的網址:
http://www.w3.org/Security/Faq/
samp_db數據庫浏覽器
對於第一個基於Web 的應用程序,我們將開發一個簡單的腳本— samp_db — 允許查看samp_db 數據庫中存在的表,並從Web 浏覽器中交互式地檢查這些表中的內容。samp_db的工作方式如下:
當首次從浏覽器中請求samp_db 時,它連接到MySQL服務器,在samp_db 數據庫中檢索一列表,並向浏覽器發送一個頁面,在這個頁面中出現的每個表都作為可選擇的連接。當選擇這個頁面中的一個表名時,浏覽器就向Web 服務器發送一個請求,請求samp_browse 顯示那個表的內容。
當調用samp_browse 時,如果它收到從Web 服務器發來的一個表名,則它就檢索這個表的內容,並將信息顯示在Web 浏覽器上。數據每列的標題就是表中列的名稱。標題作為連接出現;如果選擇它們中的一個,則浏覽器就向Web 服務器發送一個請求,顯示同樣的表,但按選擇的列排序。
注意,這裡有個警告: samp_db 表中的這些表相對較小,因此向浏覽器發送表的全部內容並不是大問題。如果編輯samp_db,顯示包含大型表的不同數據庫中的表,則應該考慮向行檢索語句中增加一個L I M I T子句。
在samp_browse 腳本的主體中,我們創建了CGI 對象,並取消了Web 頁面的初始部分。然後檢查是否按我們的假設,根據tbl_name 參數值顯示了一些特定的表:
很容易找出參數的值,因為CGI.pm 做了找出Web 服務器傳遞給這個腳本信息的全部工作。我們只需調用具有我們感興趣的參數名的param( ),在s a m p _ b r o w s e的主體中,這個參數為tbl_name。如果它沒有定義或者為空,則它就是這個腳本的初始調用,我們顯示這個表列。否則,就顯示由tbl_name 參數命名的表的內容,由sort_column 參數命名的列值排序。顯示適當的信息之後,我們調用end_html( ) 消除結束的Html 標志。
display_table_list( ) 函數生成初始頁面。display_table_list( ) 檢索這個表列並寫出在每個單元中都含有一個數據庫表名的單列的H T M L表:
display_table_list( ) 生成的頁面含有如下連接:
當調用samp_browse 時,如果tbl_name 參數有值,則這個腳本將這個值傳遞給display_table( ),連同按名稱排序後的列名。如果沒有命名的列,則我們按第一列排序(我們可以通過位置引用列,因而很容易地使用ORDER BY 1子句來完成):
表顯示了與重新顯示該表的連接相關的列標題的頁面;這些連接包括sort _ c o l um n參數,它顯式地指定排序的列。例如,對於顯示event 表內容的頁面,列標題連接看起來如下所示:
display_table_list( ) 和display_table( ) 都使用了display_cell( ),H T M L表中作為單元顯示值的實用程序函數。這個函數使用了一個小竅門,就是將空值轉換為不可分的空格(‘& n b s p ;’),因為在帶有邊框的表中,空單元不會正確地顯示邊框。將不可分的空格放入這個單元中解決了這個問題。display_cell( ) 還具有控制是否將單元值編碼的第三個參數。這是必需的,因為調用display_cell( ),顯示了一些已經編碼的單元值,如含有URL 信息的列標題:
如果想編寫更通用的腳本,可以將samp_browse 更改為浏覽多個數據庫。例如,在腳本開始時,可以顯示服務器上的一列數據庫,而不是一個特定數據庫中的一列表。然後,選出一個數據庫,獲得它的一列表,再從那裡繼續。
學分保存方案分數浏覽器
每當我們輸入測試的分數時,都需要生成一個有序的分數列表,以便確定等級曲線,並分配字母等級。請注意,對於這個列表我們將做的所有事情就是顯示它,以便能確定每個字母等級終止的位置。然後在返回給學生之前,在測試卷上標出等級分數。我們不在數據庫中
繼續記錄這個字母等級,因為在等級周期末尾的等級取決於數字分數,而不是字母等級。再請注意,嚴格地說,在創建檢索分數的方法之前,就應該有一個輸入分數的方法。我將輸入分數的腳本一直保存到下一章。在這期間,在數據庫中,我們已經從早期的等級周期部分中
得到了幾組分數。即使沒有方便的分數輸入方法,我們也可以使用具有那些分數的腳本。
我們浏覽分數的腳本score_browse 與samp_browse 有些類似,但是希望查看給定測試或測驗的分數這種更特定的目標。初始頁面給出一列可以從中選擇的可能的等級事件,允許用戶選擇它們中的任何一個,來查看與事件相關的分數。給定事件的分數按照高分在前的順序按分數排序,因此可以顯示出結果,並用它確定等級曲線。
score _ b r o w s e只需要檢查一個參數e v e n t _ i d,查看是否指定了特定事件。如果不是,則score_browse 就顯示event 表中的行,以便用戶可以選擇其中的一個。否則,就顯示與所選事件相關的分數:
使用請求表列標題的列名,函數display_events( ) 從event 表中抽取信息,並以表格形式顯示它。在每行的內部,顯示event_id 值,作為可以選擇的連接,以觸發檢索相應事件分數的查詢。每個事件的URL 都只是到具有附加參數score _ b r o w s e的路徑,這個參數指定事件號碼:
/CGI-bin/score_browse?event_id=number
display_events( ) 函數編寫如下:
當用戶選擇事件時,浏覽器發送一個具有附加事件ID 值的score_browse 請求。score_browse 找到event_id 參數集,並調用display_scores( ) 列出所有特定事件的分數。這個頁面也顯示了文本“ Show Event List”,作為返回初始頁面的連接,以便用戶能很容易地返回事件列表頁面。這個連接的URL 引用了score_browse 腳本,但不指定event_id 參數的任何值。display_scores( )子程序如下所示:
display_scores( ) 運行的查詢與我們以前在第1章的1. 4 . 8節中的“從多個表中檢索信息”小節中開發的說明如何編寫連接的查詢極為類似。在那一章中,我們請求給定日期的分數,因為日期比事件的ID值更有意義。相反,當我們使用score_browse 時,知道了精確的事件ID。那不是因為我們按照事件ID 考慮(我們沒有),而是因為腳本給了我們一列可從中選擇的事件ID。可以看到這種類型的接口減少了了解特定細節的需要。我們不必了解事件的ID;只需要識別出想要的事件。
歷史同盟共同興趣的搜索
samp_browse 和score_browse 腳本通過在初始頁面給出一列選擇而允許用戶做出選擇,那個頁面中的每個選擇都是用特定參數值再次調用這個腳本的一個連接。允許用戶做出選擇的另外一種方法是,將含有可編輯域的表格放在頁面中。當可選范圍沒有約束到一些容易確定的值的集合時,這種方法更加合適。我們的下一個腳本舉例說明了請求用戶輸入的這種方法。
在7 . 3節“運行DBI”中,我們為尋找共享特定興趣的歷史聯盟成員構造了一個命令行腳本。然而,那個腳本並不是聯盟成員已經訪問的腳本;聯盟秘書必須運行這個腳本,然後把結果郵寄給請求這個列表的成員。最好使這個搜尋性能更廣泛,以便成員可以自己使用。編寫Web 腳本就是進行這種事的一種方法。
腳本interests 把少量表格放到用戶能夠輸入關鍵字的地方,然後搜索member 表,尋找有滿足條件的成員並顯示結果。通過將通配符“ %”加到關鍵字的兩端,來執行這個搜索,以便在interests 列值的任何地方都能找到。
在每個頁面都顯示關鍵字表格,以便用戶可以立即輸入新的搜索,甚至在顯示搜索結果的頁面中也可以。除此之外,還在關鍵字表格中顯示前面頁面的搜索字符串,以便如果用戶想要運行類似的搜索,可以編輯這個字符串。這樣就不必重新鍵入許多內容了:
腳本和自己交流信息與samp_browse 或score_browse 略有不同。interest 參數沒有加到URL 的末尾,而表格中的信息由浏覽器編碼,並作為POST 請求的一部分發送出去。然而,CGI.pm 使如何發送信息成為不相關的;參數值仍然通過調用param( ) 來獲得。
實現搜索和顯示結果的函數如下所示,沒有顯示格式化項的函數format_Html_entry( ),因為它與gen_dir 腳本中的相同: