MasteringVFP/17/1

出自VFP Wiki

(修訂版本間差異)
跳轉到: 導航, 搜尋
(系列文章)
(系列文章)
第1,036行: 第1,036行:
從程式我們可以看到,作者也是很聰明的直接調用了 Windows API 來達成這件事情。
從程式我們可以看到,作者也是很聰明的直接調用了 Windows API 來達成這件事情。
ok, 下次就來看要怎麼把這個函數套用到之前的 cgilib 裡面去。
ok, 下次就來看要怎麼把這個函數套用到之前的 cgilib 裡面去。
 +
 +
=====Day 12=====
 +
 +
既然已經有了 UrlDecode 這個函數,我們再回頭來看 cgi06。
 +
 +
首先我們修改 Request 類別的 ParseData 方法
 +
<pre>
 +
HIDDEN PROCEDURE ParseData
 +
LPARAMETERS cInput as String , oCollection as Collection
 +
LOCAL i, nStart, nPos, nEqualPos
 +
LOCAL lcStr, lcKey, lcValue
 +
 +
* parse it.
 +
i = 1
 +
nStart = 1
 +
nPos=AT( "&", cInput, i )
 +
IF nPos == 0 THEN
 +
nPos = LEN( cInput )
 +
ENDIF
 +
DO WHILE nPos!=0
 +
lcStr = SUBSTR( cInput, nStart, nPos-nStart )
 +
nEqualPos = AT( "=", lcStr )
 +
IF nEqualPos!=0 THEN
 +
lcKey = SUBSTR( lcStr, 1, nEqualPos-1 )
 +
lcValue = UrlDecode( SUBSTR( lcStr, nEqualPos+1 ) )
 +
oCollection.Add( lcValue, lcKey )
 +
ENDIF
 +
i=i+1
 +
nStart = nPos + 1
 +
nPos=AT( "&", cInput, i )
 +
ENDDO
 +
IF nStart < LEN(cInput) THEN
 +
nPos = LEN( cInput )
 +
lcStr = SUBSTR( cInput, nStart )
 +
nEqualPos = AT( "=", lcStr )
 +
IF nEqualPos!=0 THEN
 +
lcKey = SUBSTR( lcStr, 1, nEqualPos-1 )
 +
lcValue = UrlDecode( SUBSTR( lcStr, nEqualPos+1 ) )
 +
oCollection.Add( lcValue, lcKey )
 +
ENDIF
 +
ENDIF
 +
ENDPROC
 +
</pre>
 +
基本上只修改兩行,用 UrlDecode 來處理 lcValue
 +
<pre>
 +
lcValue = UrlDecode( SUBSTR( lcStr, nEqualPos+1 ) )
 +
</pre>
 +
 +
接著將 cgi06 最上面的 <pre>set procedure to cgilib</pre>
 +
改為 <pre>set procedure to cgilib, httputility</pre>
 +
 +
編譯以後再執行看看,相信就已經可以處理中文了。
 +
但是,我們還沒講到 unicode 與網頁編碼問題的部份...冏...
 +
 +
p.s. 之前有人問 Apache 的部份,我猜想可能是 Apache 設定裡面的 AddDefaultCharset 沒有註解掉的原因~
 +
如果沒有註解掉,Apache 都會假定網頁編碼是 utf-8。
====Day 9====
====Day 9====

在2006年11月22日 (三) 14:03所做的修訂版本

目錄

VFP與CGI

簡介

在網址上有時候會看到類似 http://www.your_site.com/xxx.exe?xxx=yyy 的網址,那並不是一個常見的 .exe, 而是實做了 CGI 介面的 .exe.

甚麼是 CGI ?? CGI 的全名是 Common Gateway Interface,也就是一種介面。只要你的應用程式實做此一介面,Web server 就可以據此與你的應用程式溝通。

原理

CGI是怎麼與Web Server溝通的呢?HTML 裡面有所謂的 Form,你可以指定 Form 的 method 為 POST 或 GET。POST 與 GET 決定了 CGI 該怎麼去取得資料;如果是 POST,那麼 Web server 會把這些資料放到 STDIN (標準輸入資料流)去,你的 CGI 就應該使用標準輸入函數去讀取這些資料;如果是 GET,Web server 會把這些資料放到名為 QUERYSTRING 的環境變數裡,於是你需要使用取環境變數的函數來取得這些資料。至於輸出的部份,統一都是輸入到 STDOUT (標準輸出資料流)。

之後的所有技術都與 CGI 脫不了關係,都是以 CGI 為基礎發展出來的。所以會了 CGI 之後,後面也會了一半。

事實上,VFP在配合Win32 API 之後,是可以實做出CGI介面的,關鍵的API在於 ReadFile / GetENV / WriteFile 這幾個API。ReadFile 可以讀取 STDIN (標準輸入資料流),GetENV 則可以取得環境變數,於是你已經可以取得 POST 或 GET 的資料。WriteFile 可以輸出到 STDOUT (標準輸出資料流)。

在網路上可以搜索到不少資料:

第二個連結裡面有 VFP 的 sample.

實做CGI以後,你需要考量的是當 Request 過多時的問題。當 request 過多的時候,CGI 的效能並不好。因為 web server 在遇到 request CGI 的時候,會為這個 CGI create 一個 process 起來。當同時遇到 1000, 10000, 100000 ....個 request 的時候,web server 就會去 create 1000、10000、100000 ....個 process, 這會拖垮整個 server 的效能!!

這也是後來為甚麼 script 會大為盛行的原因,因為他們所需要的資源較少,處理以及除錯上也較為方便,不過基礎原理還是一樣的。

簡易範例

*
* Main.prg
*

SET PROCEDURE TO cgilib

LOCAL oResponse
oResponse = CREATEOBJECT( "RESPONSE" )
oResponse.Headers.Add( "text/plain", "Content-type" )
oResponse.Headers.Add( "us-ascii", "charset" )
oResponse.Write( "<p>Hello world!</p>")
*
* cgilib.prg
*
DEFINE CLASS RESPONSE as Custom 
	bDirty = .F.
	bHeaderOut = .F.
	ADD OBJECT Headers AS collection
	
	PROCEDURE Init
		CREATE CURSOR outputCache ( outLine varchar(254) )
	ENDPROC
	
	PROCEDURE destroy
		IF this.bDirty == .T.
			this.flush()
		ENDIF 
		USE IN outputCache 
	ENDPROC
	
	PROCEDURE Write
		LPARAMETERS theHtml
		LOCAL lcOutput
		
		INSERT INTO outputCache values( theHtml )
		this.bDirty = .t.
		RETURN 
	ENDPROC
	
	HIDDEN PROCEDURE InternalWrite
		LPARAMETER lcOutput

		DECLARE INTEGER GetStdHandle in Win32API integer nHandleType
		declare integer WriteFile    in Win32API integer hFile, string @ cBuffer,;
				integer nBytes, integer @ nBytes2, integer @ nBytes3

		LOCAL lnOutHandle
		LOCAL lnBytesWritten
		LOCAL lnOverLappedIO 
		
		lnOutHandle=GetStdHandle(-11) 
		lnBytesWritten=0
		lnOverLappedIO=0
		WriteFile(lnOutHandle, @lcOutput, len(lcOutput), @lnBytesWritten, @lnOverLappedIO)
	ENDPROC
	
	PROCEDURE flushHeader
		LOCAL lcDefaultOutput
		LOCAL lcOutput
		LOCAL lcKey
		local i
		
		lcDefaultOutput="HTTP/1.0 200 OK"+chr(13)+chr(10)
		lcOutput = ""

		FOR i = 1 TO this.Headers.Count
			lcKey = this.Headers.GetKey( i )
			IF !EMPTY( lcKey  ) THEN 
				lcOutput = lcOutput + lcKey + ": " + this.Headers.Item(i) + CHR(13) + CHR(10) 
			ENDIF 
		NEXT 
		
		IF this.Headers.getKey( "Content-type" ) == 0 THEN 
			lcOutput = lcOutput + "Content-type: text/html"+chr(13)+chr(10)
		ENDIF

		lcOutput = lcOutput + chr(13)+chr(10)
		this.InternalWrite( lcDefaultOutput + lcOutput )
		this.bHeaderOut = .T.
	ENDPROC
	
	PROCEDURE flush
		IF this.bDirty == .f. then
			RETURN
		ENDIF 

		LOCAL lcAlias
		LOCAL lcOutput
		
		IF this.bHeaderOut == .F. THEN
			this.flushHeader()
		ENDIF 
		
		lcAlias = ALIAS() 
		SELECT outputCache
		GO top
		SCAN
			lcOutput = outputCache.outLine
			? lcOutput
			this.InternalWrite( lcOutput )
		ENDSCAN
		SELECT( lcAlias )
	ENDPROC
ENDDEFINE

參考資料

系列文章

Day 1

CGI,亦即 Common Gateway Interface,應該說是所有 web server side 技術的前身。

基本的原理是利用標準輸出入與環境變數來作為 web server 與應用程式溝通的介面。

VFP 本身並不支援標準輸出入,必須透過 Windows API 才可以。 所以我們利用 GetStdHandle 取得標準輸出檔案的 Handle,再使用 WriteFile 輸出。

讓我們來看看第一個 CGI 應用程式:

*
* cgi01
*
DECLARE INTEGER GetStdHandle in Win32API integer nHandleType
declare integer WriteFile    in Win32API integer hFile, string @ cBuffer,;
		integer nBytes, integer @ nBytes2, integer @ nBytes3

LOCAL lnOutHandle
LOCAL lnBytesWritten
LOCAL lnOverLappedIO 
LOCAL lcOutput

lnOutHandle=GetStdHandle(-11) 
lnBytesWritten=0
lnOverLappedIO=0
lcOutput = "HTTP/1.0 200 OK"+chr(13)+chr(10) + ;
	"Content-type: text/html"+chr(13)+chr(10) + ;
	CHR(13) + CHR(10) + ;
	"<p>Hello world</p>"

WriteFile(lnOutHandle, @lcOutput, len(lcOutput), @lnBytesWritten, @lnOverLappedIO)

步驟:

  1. 請先建立一個新專案,命名為 vfpcgi,再新增一個 cgi01.prg,把上面的程式碼貼進去,再編譯成 Windows executable (.exe) 檔案,所以你得到了 vfpcgi.exe
  2. 在 c:\inetpub\wwwroot 下新增一個目錄,命名為 vfpcgi,將 vfpcgi.exe 複製到這裡
  3. [控制台][系統管理工具]執行Internet Information Services
  4. 在左邊你會看到 vfpcgi,滑鼠右鍵,選取內容
  5. 點選 "建立" 按鈕,左邊原本黯淡的 TextBox 會亮起來,將使用權限改為"指令及執行檔",應用程式保護選擇為"高",按下確定。
  6. 確定你的 IIS 已經啟動,然後打開你的瀏覽器,在網址列輸入 http://localhost/vfpcgi/vfpcgi.exe,這個時候你應該會看到顯示著 Hello world 的頁面~
Day 2

客人:來點肉吧~ 夥計:就來啦~

所以,放點肉吧~ 如果你會一點簡單的 html 語法的話,那麼你大概已經想到,可以怎麼顯示資料了。如果不太懂,那麼也沒關係,這裡會慢慢告訴你。

html 裡面要想畫表格的話,只要會三個 html tag:table、tr、td <table>與</table>代表的是表格,夾在中間的元素就是 tr 與 td 於是

<table>
<tr>
<td>
Hello world
</td>
</tr>
</table>

就會是一個格子。

把昨天的 cgi01.prg 複製成 cgi02.prg,然後將 cgi02.prg 設為主程式。 作一點修改,開啟 VFP 附贈的 Sample 資料庫,並且利用 TRY ... ENDTRY 來幫助我們除錯~

*
* cgi02
*
DECLARE INTEGER GetStdHandle in Win32API integer nHandleType
declare integer WriteFile    in Win32API integer hFile, string @ cBuffer,;
		integer nBytes, integer @ nBytes2, integer @ nBytes3

LOCAL lnOutHandle
LOCAL lnBytesWritten
LOCAL lnOverLappedIO 
LOCAL lcOutput
LOCAL lnCount

lnOutHandle=GetStdHandle(-11) 
lnBytesWritten=0
lnOverLappedIO=0
lcOutput = "HTTP/1.0 200 OK"+chr(13)+chr(10) + ;
	"Content-type: text/html"+chr(13)+chr(10) + ;
	CHR(13) + CHR(10)

SET EXCLUSIVE OFF 
SET TALK OFF 

TRY 
	OPEN DATABASE HOME(1) + "\Samples\Data\testdata"
	USE customer

	GO top
	lnCount = 0
	lcOutput = lcOutput + "<table border='1'>"
	SCAN
		lcOutput = lcOutput + "<tr>"
		lcOutput = lcOutput + "<td>"
		lcOutput = lcOutput + customer.contact
		lcOutput = lcOutput + "</td>"
		lcOutput = lcOutput + "<td>"
		lcOutput = lcOutput + customer.company
		lcOutput = lcOutput + "</td>"
		lcOutput = lcOutput + "</tr>"
		lnCount = lnCount + 1
		IF lnCount > 50 THEN  && 只顯示 50 筆
			EXIT 
		ENDIF
	ENDSCAN 
	lcOutput = lcOutput + "</table>"
CATCH TO oErr
	lcOutput = lcOutput + "<p>"
	lcOutput = lcOutput + "Error: " + oErr.Message
	lcOutput = lcOutput + "</p>"
FINALLY 
	CLOSE DATABASES ALL 
ENDTRY 

WriteFile(lnOutHandle, @lcOutput, len(lcOutput), @lnBytesWritten, @lnOverLappedIO)

編譯之後,還是跟昨天一樣,丟到 c:\inetpub\wwwroot\vfpcgi 下面 記得,確定 IIS 已經啟動之後,打開瀏覽器,在網址列輸入 http://localhost/vfpcgi/vfpcgi.exe

Yes~你已經看到今天的肉了。

Day 3

因為 CGI 是 Web server 共同 follow 的標準,因此當然也適用於 Apache。

我的環境:Windows 2000 + Apache 2.2.3

在安裝 Apache 之後,修改 httpd.conf (開始 > 程式集 > Apache HTTP Server 2.2.3 > Configure Apache Server > Edit the Apache httpd.conf Configuration File

添加以下內容:(對了,路徑記得自行修改,\ 都要改為 /)

#
# VFPCGI
#
Alias /vfpcgi/ "d:/vfpces/vfpcgi/"
<Directory "d:/vfpces/vfpcgi">
    AllowOverride None
    Options ExecCGI
    Order allow,deny
    Allow from all
    AddHandler cgi-script exe
</Directory>

存檔之後,啟動 Apache (用"服務")。

接著我們要修改一下昨天的程式,將

lcOutput = "HTTP/1.0 200 OK"+chr(13)+chr(10) + 
        "Content-type: text/html"+chr(13)+chr(10) + ;
	CHR(13) + CHR(10)

改為

lcOutput = "Content-type: text/html"+chr(13)+chr(10) + ;
	CHR(13) + CHR(10)

同樣的,進行編譯,接著打開瀏覽器,在網址列輸入:http://localhost/vfpcgi/vfpcgi.exe

就會看到昨天的肉了~(放了一天的肉,可能有點酸了~)

Day 4

Apache 本身提供了 ab 這個命令可以讓你對自己的 web server 作測試。 除了可以用在 apache 身上之外,也可以用在 IIS 上。

使用的說明可以參考:Apache 壓力測試

我的測試環境:

  • Windows 2000 Professional
  • AMD 1.7G
  • RAM 512MB
這邊我們統一使用下面指令進行測試,也就是模擬有 5000 個 request 連上 web server。
ab -n 5000 http://localhost/vfpcgi/vfpcgi.exe

這邊我們只觀察兩個數值:

  • Time taken for tests: 總共執行花了多久的時間.(以上 1000 次共多久)
  • Requests per second: 每秒平均可以處理多少個 connection.
  • Time per request: 每個 connection 所花費時間。
Host在IIS上的結果:
Time taken for tests:   600.93750 seconds
Requests per second:    8.33 [#/sec] (mean)
Time per request:       120.019 [ms] (mean)
Time per request:       120.019 [ms] (mean, across all concurrent requests)
Host在Apache上的結果:
Time taken for tests:   754.250000 seconds
Requests per second:    6.63 [#/sec] (mean)
Time per request:       150.850 [ms] (mean)
Time per request:       150.850 [ms] (mean, across all concurrent requests)

Apache 的結果,明顯較 IIS 差,但我這邊使用的是預設的設定,如果你對 Apache 設定有一定瞭解的話,不妨再去作調整,或許可以得到更好的數字。

以上結果,就提供給各位作參考。

Day 5

第五天~

接著,我們就把 WriteFile 包裝一下吧,讓她可以像 ASP 的 Response 一樣這麼用~

*
* CGI03.prg
*
SET PROCEDURE TO cgilib

LOCAL oResponse
oResponse = CREATEOBJECT( "RESPONSE" )

oResponse.Write( "<p>Hello world!</p>")

所以,Response 類別就如下面,這邊利用了 VFP 的 cursor 來當作 cache,所有的 Write,實際上都是先塞到 cursor 裡面去,等到最後被釋放或是呼叫 Flush 的時候,才利用 scan...endscan 一次把所有資料寫出去。

*
* CGILIB.prg
*
DEFINE CLASS RESPONSE as Custom 
	bDirty = .F.
	bHeaderOut = .F.
	ADD OBJECT Headers AS collection
	
	PROCEDURE Init
		CREATE CURSOR outputCache ( outLine varchar(254) )
	ENDPROC
	
	PROCEDURE destroy
		IF this.bDirty == .T.
			this.flush()
		ENDIF 
		USE IN outputCache 
	ENDPROC
	
	PROCEDURE Write
		LPARAMETERS theHtml
		LOCAL lcOutput
		
		INSERT INTO outputCache values( theHtml )
		this.bDirty = .t.
		RETURN 
	ENDPROC
	
	HIDDEN PROCEDURE InternalWrite
		LPARAMETER lcOutput

		DECLARE INTEGER GetStdHandle in Win32API integer nHandleType
		declare integer WriteFile    in Win32API integer hFile, string @ cBuffer,;
				integer nBytes, integer @ nBytes2, integer @ nBytes3

		LOCAL lnOutHandle
		LOCAL lnBytesWritten
		LOCAL lnOverLappedIO 
		
		lnOutHandle=GetStdHandle(-11) 
		lnBytesWritten=0
		lnOverLappedIO=0
		WriteFile(lnOutHandle, @lcOutput, len(lcOutput), @lnBytesWritten, @lnOverLappedIO)
	ENDPROC
	
	PROCEDURE flushHeader
		LOCAL lcDefaultOutput
		LOCAL lcOutput
		LOCAL lcKey
		local i
		
		lcDefaultOutput=""
		lcOutput = ""

		FOR i = 1 TO this.Headers.Count
			lcKey = this.Headers.GetKey( i )
			IF !EMPTY( lcKey  ) THEN 
				lcOutput = lcOutput + lcKey + ": " + this.Headers.Item(i) + CHR(13) + CHR(10) 
			ENDIF 
		NEXT 
		
		IF this.Headers.getKey( "Content-type" ) == 0 THEN 
			lcOutput = lcOutput + "Content-type: text/html"+chr(13)+chr(10)
		ENDIF

		lcOutput = lcOutput + chr(13)+chr(10)
		this.InternalWrite( lcDefaultOutput + lcOutput )
		this.bHeaderOut = .T.
	ENDPROC
	
	PROCEDURE flush
		IF this.bDirty == .f. then
			RETURN
		ENDIF 

		LOCAL lcAlias
		LOCAL lcOutput
		
		IF this.bHeaderOut == .F. THEN
			this.flushHeader()
		ENDIF 
		
		lcAlias = ALIAS() 
		SELECT outputCache
		GO top
		SCAN
			lcOutput = outputCache.outLine
			this.InternalWrite( lcOutput )
		ENDSCAN
		SELECT( lcAlias )
	ENDPROC
ENDDEFINE

這邊我也利用 VFP 提供的 collection 來實做了 Response 的 Header。 VFP 的 collection 跟其他語言的用法不太一樣,前面是所謂的 value,後面才是所謂的 name。 於是,你可以試試看,在 oResponse.Write( "<p>Hello world!</p>") 的前面加上

oResponse.Headers.Add( "text/plain", "Content-type" )
oResponse.Headers.Add( "us-ascii", "charset" )

試試看結果如何~

Day 6

那麼,要怎麼接收參數呢? Web application 如果不能收參數,那就遜掉了~程式也會難寫很多吧~

接收參數有兩種方式,一種是 GET,一種則是 POST 差別在哪裡? 使用者看得到的,最大的差別就在於網址列。 如果網址列有像是 http://localhost/default.aspx?name1=value1&name2=value2 這種的,就表示是以 GET 方式,預設的存取方式其實也是 GET。

對 CGI 應用程式來說,最大的差別在於讀取資料的方式:如果是 GET,那就取得 QUERY_STRING 這個環境變數的內容;如果是 POST,那麼就讀取 STDIN (標準輸入) 的內容。

那麼,CGI 應用程式又要怎麼知道現在是 GET 還是 POST 呢? 同樣地,還是透過環境變數,這個環境變數的名字就叫做 REQUEST_METHOD。

ok,讓我們根據以上的原則來寫寫程式,請新增一個 cgi04.prg,然後將她設置為 main (set as main)

*
* cgi04
*
SET PROCEDURE TO cgilib

LOCAL oResponse
oResponse = CREATEOBJECT( "RESPONSE" )

LOCAL cRequestMethod
cRequestMethod = GETENV( "REQUEST_METHOD" )

oResponse.Write( "<p>REQUEST_METHOD=" + cRequestMethod + "</p>")

DO CASE 
	CASE cRequestMethod == "GET"
		oResponse.Write( "<p>QUERY_STRING=" + GETENV( "QUERY_STRING" ) + "</p>" )
	CASE cRequestMethod == "POST"
		* not implemented.
ENDCASE 

同樣,編譯好,丟到正確的位置之後(如果你還不知道,請參考前面),輸入:

http://localhost/vfpcgi/vfpcgi.exe?name1=value1&name2=value2

就可以看到結果了。 這邊只演示了 GET 的部份,明天再來搞 POST 的部份...

Day 7

上次還沒實做關於 POST 的部份,所以這次延續上次的例子,並且做了一些擴充,這樣可以讓你很清楚怎麼取 GET 與 POST 來的資料。

這裡我添加了一個 form,這是用來放表單資料的,再按下 submit 之後,你會在表單的下方看到讀到的結果。這邊也利用了 text...endtext 來作為一個 template 產生器,你可以看到 text...endtext 可以很方便的同時寫 html,並且在裡面嵌入變數(用<<與>>把變數括起來)。

*
* cgi05
*

SET PROCEDURE TO cgilib
DECLARE INTEGER GetStdHandle in Win32API integer nHandleType
declare integer ReadFile in Win32API integer hFile, string @ cBuffer,;
	integer nBytes, integer @ nBytes2, integer @ nBytes3

LOCAL oResponse
oResponse = CREATEOBJECT( "RESPONSE" )

LOCAL cRequestMethod, cInput
cRequestMethod = UPPER( GETENV( "REQUEST_METHOD" ) )

cInput = "<p>REQUEST_METHOD=" + cRequestMethod + "</p>"

DO CASE 
	CASE cRequestMethod == "GET"
		cInput = cInput + GETENV( "QUERY_STRING" )
	CASE cRequestMethod == "POST"
		&&lenght of input string in STDIN is in this environment variable
		LOCAL lcContentLength, lnContentLength 
		LOCAL lnHandle 
		LOCAL lcInput
		LOCAL lnOverlappedIO
		
		lcContentLength = GETENV("CONTENT_LENGTH")
		IF LEN( lcContentLength ) > 0 THEN 
			lnContentLength=VAL( lcContentLength )
		ELSE
			lnContentLength = 0
		ENDIF 

		IF lnContentLength > 0 THEN 
			&&get the input from STDIN
			lnInHandle=GetStdHandle(-10) 
			lcInPut=REPLICATE(' ', lnContentLength )
			lnOverlappedIO=0
			ReadFile(lnInHandle, @lcInPut, lnContentLength, @lnContentLength, @lnOverlappedIO)
		ELSE
			lcInput=''
		ENDIF
		cInput = cInput + lcInput
	OTHERWISE 
		cInput = ""
ENDCASE 

lcHtml = ""
TEXT TO lcHtml NOSHOW ADDITIVE TEXTMERGE 
<script language="javascript">
function method_change( form )
{
 switch ( form.cboMethod.value ) {
 case "GET": form.method = "GET"; break;
 case "POST": form.method = "POST"; break;
 }
}
</script>
<form method="get" action="vfpcgi.exe">
RequestMethod: <select name="cboMethod" onChange="return method_change(this.form);">
	<option value="GET" <<IIF(cRequestMethod=="GET", "selected", "")>> >GET</option>
	<option value="POST" <<IIF(cRequestMethod=="POST", "selected", "")>> >POST</option>
</select><br/>
<input type="text" name="txt" value=""/>
<select name="cbo">
<option value="0" selected>0</option>
<option value="1">1</option>
<option value="2">2</option>
</select><br/>
<input type="submit" value="Submit"/>
<input type="reset" value="Reset"/>
</form>
<pre>
<<cInput>>
</pre>
ENDTEXT 

oResponse.Write( lcHtml )

同時,也對上次的 Response 類別作一點修正,在上次 Response 類別裡面,用來作為 cache 的 cursor,其資料欄位是 varchar(254),換言之,每次 Write 時,只能塞入 254 個字元,如果超過的話,就完蛋了。這次我把她修正為 M,也就是 Memo 型態。

DEFINE CLASS RESPONSE as Custom 
	bDirty = .F.
	bHeaderOut = .F.
	ADD OBJECT Headers AS collection
	
	PROCEDURE Init
		CREATE CURSOR outputCache ( outLine M )
	ENDPROC
	
	PROCEDURE destroy
		IF this.bDirty == .T.
			this.flush()
		ENDIF 
		USE IN outputCache 
	ENDPROC
	
	PROCEDURE Write
		LPARAMETERS theHtml
		LOCAL lcOutput
		
		INSERT INTO outputCache values( theHtml )
		this.bDirty = .t.
		RETURN 
	ENDPROC
	
	HIDDEN PROCEDURE InternalWrite
		LPARAMETER lcOutput

		DECLARE INTEGER GetStdHandle in Win32API integer nHandleType
		declare integer WriteFile    in Win32API integer hFile, string @ cBuffer,;
				integer nBytes, integer @ nBytes2, integer @ nBytes3

		LOCAL lnOutHandle
		LOCAL lnBytesWritten
		LOCAL lnOverLappedIO 
		
		lnOutHandle=GetStdHandle(-11) 
		lnBytesWritten=0
		lnOverLappedIO=0
		WriteFile(lnOutHandle, @lcOutput, len(lcOutput), @lnBytesWritten, @lnOverLappedIO)
	ENDPROC
	
	PROCEDURE flushHeader
		LOCAL lcDefaultOutput
		LOCAL lcOutput
		LOCAL lcKey
		local i
		
		lcDefaultOutput=""
		lcOutput = ""

		FOR i = 1 TO this.Headers.Count
			lcKey = this.Headers.GetKey( i )
			IF !EMPTY( lcKey  ) THEN 
				lcOutput = lcOutput + lcKey + ": " + this.Headers.Item(i) + CHR(13) + CHR(10) 
			ENDIF 
		NEXT 
		
		IF this.Headers.getKey( "Content-type" ) == 0 THEN 
			lcOutput = lcOutput + "Content-type: text/html"+chr(13)+chr(10)
		ENDIF

		lcOutput = lcOutput + chr(13)+chr(10)
		this.InternalWrite( lcDefaultOutput + lcOutput )
		this.bHeaderOut = .T.
	ENDPROC
	
	PROCEDURE flush
		IF this.bDirty == .f. then
			RETURN
		ENDIF 

		LOCAL lcAlias
		LOCAL lcOutput
		
		IF this.bHeaderOut == .F. THEN
			this.flushHeader()
		ENDIF 
		
		lcAlias = ALIAS() 
		SELECT outputCache
		GO top
		SCAN
			lcOutput = outputCache.outLine
			this.InternalWrite( lcOutput )
		ENDSCAN
		SELECT( lcAlias )
	ENDPROC
ENDDEFINE

同樣的,新增 cgi05 以後,將她設置為主程式,重新編譯之後,再丟到對應路徑即可。

Day 8

當你知道規則之後,很快就能寫出一個類似 Request 的物件了~ 所以我們的 cgilib.prg 又多了一個 Request 類別:

DEFINE CLASS REQUEST as Custom 
	TotalBytes = 0
	RequestMethod = ""
	ADD OBJECT QueryString AS collection
	ADD OBJECT FormField AS collection
	ADD OBJECT Cookies as collection 
	
	PROCEDURE INIT
		DECLARE INTEGER GetStdHandle in Win32API integer nHandleType
		declare integer ReadFile in Win32API integer hFile, string @ cBuffer,;
			integer nBytes, integer @ nBytes2, integer @ nBytes3
		
		LOCAL lcMethod
		LOCAL lcQueryString
		LOCAL lcContentLength, lnContentLength
		LOCAL lcInput
		LOCAL lnOverlappedIO
		LOCAL lnInHandle
		
		lcMethod = UPPER( GETENV( "REQUEST_METHOD" ) )
		this.RequestMethod = lcMethod
		lcQueryString = ""
		
		DO CASE 
			CASE INLIST( lcMethod , "GET" )
				* get environment variable: QUERY_STRING
				lcQueryString = GETENV( "QUERY_STRING" )
				this.ParseData( lcQueryString, this.QueryString )
			CASE INLIST( lcMethod, "POST" )
				&&lenght of input string in STDIN is in this environment variable
				lcContentLength = GETENV("CONTENT_LENGTH")
				IF LEN( lcContentLength ) > 0 THEN 
					lnContentLength=VAL( lcContentLength )
				ELSE
					lnContentLength = 0
				ENDIF 

				IF lnContentLength > 0 THEN 
					&&get the input from STDIN
					lnInHandle=GetStdHandle(-10) 
					lcInPut=REPLICATE(' ', lnContentLength )
					lnOverlappedIO=0
					ReadFile(lnInHandle, @lcInPut, lnContentLength, @lnContentLength, @lnOverlappedIO)
				ELSE
					lcInput=''
				ENDIF 

				IF LEN( lcInput ) > 0 THEN
					this.ParseData( lcInput, this.FormField )
				ENDIF 
			OTHERWISE 
		ENDCASE 
	ENDPROC
	
	HIDDEN PROCEDURE ParseData
		LPARAMETERS cInput as String , oCollection as Collection 
		LOCAL i, nStart, nPos, nEqualPos
		LOCAL lcStr, lcKey, lcValue 
		
		* parse it.
		i = 1
		nStart = 1
		nPos=AT( "&", cInput, i )
		IF nPos == 0 THEN 
			nPos = LEN( cInput )
		ENDIF 
		DO WHILE nPos!=0
			lcStr = SUBSTR( cInput, nStart, nPos-nStart )
			nEqualPos = AT( "=", lcStr )
			IF nEqualPos!=0 THEN 
				lcKey = SUBSTR( lcStr, 1, nEqualPos-1 )
				lcValue = SUBSTR( lcStr, nEqualPos+1 )
				oCollection.Add( lcValue, lcKey )
			ENDIF 
			i=i+1
			nStart = nPos + 1
			nPos=AT( "&", cInput, i )
		ENDDO 
		IF nStart < LEN(cInput) THEN 
			nPos = LEN( cInput )
			lcStr = SUBSTR( cInput, nStart )
			nEqualPos = AT( "=", lcStr )
			IF nEqualPos!=0 THEN 
				lcKey = SUBSTR( lcStr, 1, nEqualPos-1 )
				lcValue = SUBSTR( lcStr, nEqualPos+1 )
				oCollection.Add( lcValue, lcKey )
			ENDIF 
		ENDIF 
	ENDPROC 
ENDDEFINE

Request 這部份是最容易有問題的地方了~因為安全的漏洞往往來自於此。 也正式因為如此,才會有人說,永遠不要相信使用者輸入的資料,無論如何都要作詳細的檢查。 早期多半用 C 語言來寫 CGI 的時候,這邊常會犯的錯誤是,沒有預留足夠的空間來放這些輸入的字串,這也導致了 hacker 可以用緩衝區溢位的方式來進行攻擊。

我們這邊做的比較簡單,都自己寫程式去 parse 字串,然後放到 collection 裡面去。 依照上個範例的輸出結果,我想你應該知道可以利用 aline() 之類的函數來解,這會更方便些~

這邊我們還沒處理 utf-8 與 % 的一些問題....同時也還沒補上 Request 的使用範例...

Day 10

其實 CGI 介面除了前面提過的那幾個環境變數之外,還有提供如下變數:

  • SERVER_SOFTWARE
  • SERVER_NAME
  • GATEWAY_INTERFACE
  • SERVER_PROTOCOL
  • SERVER_PORT
  • REQUEST_METHOD
  • PATH_INFO
  • PATH_TRANSLATED
  • SCRIPT_NAME
  • QUERY_STRING
  • REMOTE_HOST
  • REMOTE_ADDR
  • AUTH_TYPE
  • REMOTE_USER
  • REMOTE_IDENT
  • CONTENT_TYPE
  • CONTENT_LENGTH
  • HTTP_ACCEPT
  • HTTP_USER_AGENT

讓我們都印出來看看~請把以下程式存為 cgi07.prg,並設為主程式進行編譯。

*
* cgi07
*
* Reference:
*   CGI Environment Variables http://hoohoo.ncsa.uiuc.edu/cgi/env.html
*

SET PROCEDURE TO cgilib

LOCAL oResponse
oResponse = CREATEOBJECT( "RESPONSE" )

LOCAL lcHtml
TEXT TO lcHtml TEXTMERGE NOSHOW 
CGI Environment variables:
<ul>
<li>SERVER_SOFTWARE=<<GETENV("SERVER_SOFTWARE")>></li>
<li>SERVER_NAME=<<GETENV("SERVER_NAME")>></li>
<li>GATEWAY_INTERFACE=<<GETENV("GATEWAY_INTERFACE")>></li>
<li>SERVER_PROTOCOL=<<GETENV("SERVER_PROTOCOL")>></li>
<li>SERVER_PORT=<<GETENV("SERVER_PORT")>></li>
<li>REQUEST_METHOD=<<GETENV("REQUEST_METHOD")>></li>
<li>PATH_INFO=<<GETENV("PATH_INFO")>></li>
<li>PATH_TRANSLATED=<<GETENV("PATH_TRANSLATED")>></li>
<li>SCRIPT_NAME=<<GETENV("SCRIPT_NAME")>></li>
<li>QUERY_STRING=<<GETENV("QUERY_STRING")>></li>
<li>REMOTE_HOST=<<GETENV("REMOTE_HOST")>></li>
<li>REMOTE_ADDR=<<GETENV("REMOTE_ADDR")>></li>
<li>AUTH_TYPE=<<GETENV("AUTH_TYPE")>></li>
<li>REMOTE_USER=<<GETENV("REMOTE_USER")>></li>
<li>REMOTE_IDENT=<<GETENV("REMOTE_IDENT")>></li>
<li>CONTENT_TYPE=<<GETENV("CONTENT_TYPE")>></li>
<li>CONTENT_LENGTH=<<GETENV("CONTENT_LENGTH")>></li>
<li>HTTP_ACCEPT=<<GETENV("HTTP_ACCEPT")>></li>
<li>HTTP_USER_AGENT=<<GETENV("HTTP_USER_AGENT")>></li>
</ul>
ENDTEXT 
oResponse.Write( lcHtml )

你會看到許多有趣的資訊。 在 ASP 裡面,你可以用 Request.ServerVariables() 來取得這些資訊。 所以我們為 Request 添加一個 Method,命名為 ServerVariables

	PROCEDURE ServerVariables
		LPARAMETERS strVarName
		RETURN GETENV( strVarName )
	ENDPROC

再一次改寫 cgi07 為 cgi08 如下,並將 cgi08 設為主程式:

*
* cgi08
*
* Reference:
*   CGI Environment Variables http://hoohoo.ncsa.uiuc.edu/cgi/env.html
*

SET PROCEDURE TO cgilib

LOCAL oResponse, oRequest
oResponse = CREATEOBJECT( "RESPONSE" )
oRequest = CREATEOBJECT( "REQUEST" )

LOCAL lcHtml
TEXT TO lcHtml TEXTMERGE NOSHOW 
CGI Environment variables:
<ul>
<li>SERVER_SOFTWARE=<<oRequest.ServerVariables("SERVER_SOFTWARE")>></li>
<li>SERVER_NAME=<<oRequest.ServerVariables("SERVER_NAME")>></li>
<li>GATEWAY_INTERFACE=<<oRequest.ServerVariables("GATEWAY_INTERFACE")>></li>
<li>SERVER_PROTOCOL=<<oRequest.ServerVariables("SERVER_PROTOCOL")>></li>
<li>SERVER_PORT=<<oRequest.ServerVariables("SERVER_PORT")>></li>
<li>REQUEST_METHOD=<<oRequest.ServerVariables("REQUEST_METHOD")>></li>
<li>PATH_INFO=<<oRequest.ServerVariables("PATH_INFO")>></li>
<li>PATH_TRANSLATED=<<oRequest.ServerVariables("PATH_TRANSLATED")>></li>
<li>SCRIPT_NAME=<<oRequest.ServerVariables("SCRIPT_NAME")>></li>
<li>QUERY_STRING=<<oRequest.ServerVariables("QUERY_STRING")>></li>
<li>REMOTE_HOST=<<oRequest.ServerVariables("REMOTE_HOST")>></li>
<li>REMOTE_ADDR=<<oRequest.ServerVariables("REMOTE_ADDR")>></li>
<li>AUTH_TYPE=<<oRequest.ServerVariables("AUTH_TYPE")>></li>
<li>REMOTE_USER=<<oRequest.ServerVariables("REMOTE_USER")>></li>
<li>REMOTE_IDENT=<<oRequest.ServerVariables("REMOTE_IDENT")>></li>
<li>CONTENT_TYPE=<<oRequest.ServerVariables("CONTENT_TYPE")>></li>
<li>CONTENT_LENGTH=<<oRequest.ServerVariables("CONTENT_LENGTH")>></li>
<li>HTTP_ACCEPT=<<oRequest.ServerVariables("HTTP_ACCEPT")>></li>
<li>HTTP_USER_AGENT=<<oRequest.ServerVariables("HTTP_USER_AGENT")>></li>
</ul>
ENDTEXT 
oResponse.Write( lcHtml )

這次主要是實做 Request.ServerVariables 方法。

參考資料:

Day 11

善用別人造好的輪子之一

常會看到一些 +%76 之類的字串,會這樣編碼的原因,就是為了要避開一些特殊字元。 這部份的定義可以參考 [url=http://www.faqs.org/rfcs/rfc1738.html]RFC 1738 (rfc1738) - Uniform Resource Locators (URL)[/url]的 2.2 節,這份 rfc 的最後也提供了語法定義。

ok, 那麼 VFP 有沒有相關函數呢?答案是沒有。 可是幸運的是,已經有人寫好了:Url Decode - Visual FoxPro Wiki。 所以就...用吧~

下面的程式就直接轉貼自上面的連結,我們新增一個 httputility.prg,然後把程式放到裡面去:

Function URLDecode
    ***
    *** If tcInput contains "%00", the string will be terminated at that
    *** character.
    ***

    *** URLDecodes a text string:
    * Replaces %hh tokens with ascii characters

    *** Input: tcInput - Text string to decode
    *** Return: Decoded string
    * Author: Albert Ballinger

    Lparameter tcInput, nFlag

    tcInput = Chrtran(tcInput, "+", " ")

    *!* UrlUnescape
    *!* Converts escape sequences back into ordinary characters.

    *!* HRESULT UrlUnescape(
    *!* LPTSTR pszURL,
    *!* LPTSTR pszUnescaped,
    *!* LPDWORD pcchUnescaped,
    *!* DWORD dwFlags
    *!* );

    *!* Parameters
    *!* pszURL - [in/out] Pointer to a NULL-terminated string with
    *!* the URL. If dwFlags is set to URL_UNESCAPE_INPLACE, the
    *!* converted string is returned through this parameter.
    *!* pszUnescaped - [out] Pointer to a buffer that will receive a
    *!* NULL-terminated string containing the unescaped version
    *!* of pszURL. If URL_UNESCAPE_INPLACE is set in dwFlags,
    *!* this parameter is ignored.
    *!* pcchUnescaped - [in/out] Number of characters in the buffer
    *!* pointed to by pcchUnescaped. On entry, the value
    *!* pcchUnescaped points to is set to the size of the
    *!* buffer. If the function returns a success code, the
    *!* value that pcchUnescaped points to is set to the number
    *!* of characters written to that buffer, not counting the
    *!* terminating NULL character. If an E_POINTER error code
    *!* is returned, the buffer was too small, and the value
    *!* pcchUnescaped points to is set to the required number of
    *!* characters that the buffer must be able to contain. If
    *!* any other errors are returned, the value that
    *!* pcchUnescaped points to is undefined.
    *!* dwFlags - [in] Flags that control which characters are
    *!* unescaped. It can be a combination of the following
    *!* flags. Flag Description
    *!* URL_DONT_UNESCAPE_EXTRA_INFO Don't convert the #
    *!* or ? character, or any characters following them
    *!* in the string.
    *!* URL_UNESCAPE_INPLACE Use pszURL to return the
    *!* converted string instead of pszUnescaped.

    *!* Return Values
    *!* Returns an OLE success code if successful. If the
    *!* URL_UNESCAPE_INPLACE flag is not set, the value pointed to
    *!* by pcchUnescaped will be set to the number of characters in
    *!* the output buffer pointed to by pszUnescaped. Returns
    *!* E_POINTER if the URL_UNESCAPE_INPLACE flag is not set and
    *!* the output buffer is too small. The pcchUnescaped parameter
    *!* will be set to the required buffer size. Otherwise, returns
    *!* an OLE error value.

    *!* Remarks
    *!* An escape sequence has the form "%xy".

    *!* Requirements
    *!* Version 5.00 and later of Shlwapi.dll

    *!* Windows NT/2000: Requires Windows 2000 (or Windows NT 4.0
    *!* with Internet Explorer 5.0 or later).
    *!* Windows 95/98: Requires Windows 98 (or Windows 95 with
    *!* Internet Explorer 5.0 or later).
    *!* Header: Declared in shlwapi.h.
    *!* Import Library: shlwapi.lib.

    Declare Integer UrlUnescape In shlwapi.Dll As UrlUnescape ;
        string pszURL, ;
        string @ pszUnescaped, ;
        integer @ pcchUnescaped, ;
        integer dwFlags

    Local lcOutput, lnLength
    lnLength = Len(tcInput) + 1
    lcOutput = Replicate(Chr(0), lnLength)

    If 0 = UrlUnescape(tcInput, @lcOutput, @lnLength, nFlag)
        lcOutput = Left(lcOutput, lnLength)
        Return lcOutput
    Else
        Return ""
    Endif
ENDFUNC

從程式我們可以看到,作者也是很聰明的直接調用了 Windows API 來達成這件事情。 ok, 下次就來看要怎麼把這個函數套用到之前的 cgilib 裡面去。

Day 12

既然已經有了 UrlDecode 這個函數,我們再回頭來看 cgi06。

首先我們修改 Request 類別的 ParseData 方法

	HIDDEN PROCEDURE ParseData
		LPARAMETERS cInput as String , oCollection as Collection 
		LOCAL i, nStart, nPos, nEqualPos
		LOCAL lcStr, lcKey, lcValue 
		
		* parse it.
		i = 1
		nStart = 1
		nPos=AT( "&", cInput, i )
		IF nPos == 0 THEN 
			nPos = LEN( cInput )
		ENDIF 
		DO WHILE nPos!=0
			lcStr = SUBSTR( cInput, nStart, nPos-nStart )
			nEqualPos = AT( "=", lcStr )
			IF nEqualPos!=0 THEN 
				lcKey = SUBSTR( lcStr, 1, nEqualPos-1 )
				lcValue = UrlDecode( SUBSTR( lcStr, nEqualPos+1 ) )
				oCollection.Add( lcValue, lcKey )
			ENDIF 
			i=i+1
			nStart = nPos + 1
			nPos=AT( "&", cInput, i )
		ENDDO 
		IF nStart < LEN(cInput) THEN 
			nPos = LEN( cInput )
			lcStr = SUBSTR( cInput, nStart )
			nEqualPos = AT( "=", lcStr )
			IF nEqualPos!=0 THEN 
				lcKey = SUBSTR( lcStr, 1, nEqualPos-1 )
				lcValue = UrlDecode( SUBSTR( lcStr, nEqualPos+1 ) )
				oCollection.Add( lcValue, lcKey )
			ENDIF 
		ENDIF 
	ENDPROC 

基本上只修改兩行,用 UrlDecode 來處理 lcValue

lcValue = UrlDecode( SUBSTR( lcStr, nEqualPos+1 ) )
接著將 cgi06 最上面的
set procedure to cgilib
改為
set procedure to cgilib, httputility

編譯以後再執行看看,相信就已經可以處理中文了。 但是,我們還沒講到 unicode 與網頁編碼問題的部份...冏...

p.s. 之前有人問 Apache 的部份,我猜想可能是 Apache 設定裡面的 AddDefaultCharset 沒有註解掉的原因~ 如果沒有註解掉,Apache 都會假定網頁編碼是 utf-8。

Day 9

有了 Request class,我們把 cgi05 改寫成 cgi06,不用自己再處理,改由 request class 去處理。

少掉前面的一堆處理,看起來是比較好一點了~ 下次來改寫 Request 來讓她可以處理中文。

*
* cgi06
*

SET PROCEDURE TO cgilib

TRY 
	LOCAL oResponse, oRequest
	oResponse = CREATEOBJECT( "RESPONSE" )
	oRequest = CREATEOBJECT( "REQUEST" )

	* 取得目前的 Request Method
	LOCAL cInput
	cInput = "<p>REQUEST_METHOD=" + oRequest.RequestMethod + "</p>"

	* 因為要去處理的 collection 不一樣
	DO CASE 
		CASE oRequest.RequestMethod == "POST"
			oCollection = oRequest.FormField
		OTHERWISE 
			oCollection = oRequest.QueryString
	ENDCASE 
	cInput = cInput + "<p><ul>"
	FOR i=1 TO oCollection.Count
		cInput = cInput + "<li>" + oCollection.GetKey(i) + "=" + oCollection.Item(i) + "</li>"
	ENDFOR
	cInput = cInput + "</ul></p>"

	* 輸出成 HTML 囉~
	lcHtml = ""
	TEXT TO lcHtml NOSHOW ADDITIVE TEXTMERGE 
	<script language="javascript">
	function method_change( form )
	{
	 switch ( form.cboMethod.value ) {
	 case "GET": form.method = "GET"; break;
	 case "POST": form.method = "POST"; break;
	 }
	}
	</script>
	<form method="get" action="vfpcgi.exe">
	RequestMethod: <select name="cboMethod" onChange="return method_change(this.form);">
		<option value="GET" <<IIF(oRequest.RequestMethod=="GET", "selected", "")>> >GET</option>
		<option value="POST" <<IIF(oRequest.RequestMethod=="POST", "selected", "")>> >POST</option>
	</select><br/>
	<input type="text" name="txt" value=""/>
	<select name="cbo">
	<option value="0" selected>0</option>
	<option value="1">1</option>
	<option value="2">2</option>
	</select><br/>
	<input type="submit" value="Submit"/>
	<input type="reset" value="Reset"/>
	</form>
	<<cInput>>
	ENDTEXT 

	oResponse.Write( lcHtml )
CATCH TO oError
	oResponse.Write( oError.Message )
FINALLY 
ENDTRY