一个很简单的CGI实例--订货单

 从在线的表单中提取信息,然后处理这些信息,查错,再发送给其他人。虽然很简单但却需要比较全的相关知识。

在本页中,你将学习如下内容:
·如何对表单数据进行解码
·如何从输入表单中查出常见错误
·如何在CGI中嵌入HTML
·如何使用sendmail
·如何处理安全性问题
    处理更复杂的订货特性将在"购物车"中进行讨论。本章所述的是基础知识,包括客户跟踪,简单数据库集成,和动态表单生成。

1·表单和数据

    现在假定你已熟练在HTML中设计表单。以下所示的是一典型的简单表单,它与你要分析的任何表单很相似。我使用许多不同的 INPUT来强调这个事实,无论表单中的域使用什么类型,产生的数据格式基本上都是一样的。


简易订单

(在你阅读完以后,可以在此测试以下,十分希望得到您的建议。)

         姓名:
         住址:
         城市:
         省:
         国家:
         邮政编码:
         Email:
  

定购以下产品!
联想电脑
实达电脑
IBM

请选择免费礼品:

付款方式:
现金 转账 票汇

Card Number (如果可用的话):


注意:不能编写CGI并不防碍大量的人将其订货表单放到Internet上。他们采用
ACTION=mailto:recipient而不是ACTION=form的CGI文件头就可实现。


1.1 表单标记

    每个表单在表单的描述中指明一种方法(METHOD)和一种行为(ACTION),其中action将用于处理这张表单的CGI的地址;method指出如何传递数据。
<FORM METHOD=POST ACTIONS="Order.cgi">
     在这个表单中,我们使用POST方法将表单输出传递给Order.cgi, 这是执行HTML文件并与其位于同一个目录的程序。

    警告: 在一些系统中,你只能执行cgi-bin目录下的CGI程序。能否往那个目录中写信取决于系统的安全性和你的使用权限。有些系统管理员希望自己能先检查他们系统中的CGI,然后再提供给别人用。这将使编写和调试CGI 程序变得相当困难。然而,对于这个问题没有简单的解决办法,只能向系统管理员请求更高的权限。

1.2 方法(Methods)

    有两种方法可以将表单中的信息发送给CGI,它们是GET和POST。
    在GET方法中,信息会附加到ACTION中的URL上。GET比POST速度快,但会产生较长的unL。由于GET非常适用于internet上诸如搜索引擎之类的应用,而且附加到URL上的信息使标识CGI 的输出变得容易。当信息通过CGI方法传递时,数据将存储在环境变量QUERY-STRING中。
    POST方法是我们应用更广泛的方法,它不产生冗长的URL。填入表单中的信息将送到STDIN(标准输入)。这里没有诸如End of File之类的定义符,而是将输入串的长度存储在环境变量CONTENT-LENGTH中。

1.3 环境变量

    浏览器不断地往服务器发送环境变量。环境变量中含有正在运行的软件类型、浏览某个页面的用户IP地址以及其他信息。CGI 使你能够访问那些环境变量。大多数环境变量此时对你并不十分有用,但有几个环境变量你肯定需要。表1.列出本页中你要用到的环境变量。

表1 表单处理中用到的环境空量

CONTENT_LENGTH 传递给CGI程序的数据长度。通过该变量可以读取利用METHOD POST 提交的数据
QUERY_STRING 含有METHOD=GET方法提交的数据
REQUEST-METHOD 告诉我们传递数据采用的方法——GET或POST

    Perl自动地将所有环境变量转化成相关的%ENV数组。为确定环境变量的值,可使用$ENV{变量名}。

    提示:要找出测览器正在发送的环境变量,可使用快速简单的下述CGI程序:

    #! /usr/bin/per1

    print "Content-type:Text/HTML \n\n";
    foreach (keys %ENV) {
    print "$_ = $ENV{$_)";
    }


1.4 未加工的数据

    我们看一下用mai1to: 通过简单的e-mail发送的表单所输出的原始数据。这些数据与将要送给CGI的数据格式一样:

    applicant=John+Doe&ddress=520+Main+St.+Apt.%23204&city=Cicily&state=
    Alaska&zipcode=90210&country=USA&email=mqingyi@126.com&zines=Cooking+With+
    soy1ent+Green&zines=Asteroid+Living&gift=TriCOrder&payment_method=
    Frontiers+Crel_it+Card&card_number=123456789123&suggestions=Can+you+offer+
    %22Asteroid+Living+For+Kids%22%3F%0D%0A

    数据是URL编码。由表单生成的"名/值"对用"&"符号隔开,空格变成加号"+"号,有些字符以16进制表示成"%××"。我们的第一个目标是使这些数据可读。

2 用Perl处理数据

    Perl专门用于字符串的处理和操作。这使它成为CGI 应用程序的首选语言。Perl的另一个强有力的特性是关联数组,数组元素中的下标可用字符串来代替。

    注意: 一些现成的工具有助于通过CGI 处理表单及其他功能。cgi-1ib.pl,一个Perl函数库,提供给你一个资柜,使处理表单变得简单。近期Perl5 版本带有一个实时表单分析和处理特性的库cgi.pm。

2.1 分析一个简单的CGI程序

这个Perl程序读取我们用METHOD=POST方式传递的订货表单,然后在屏幕上显示"名/值"对。
这些代码可用于任何由METHOD=POST提交的表单。

    #!/usr/bin/perl
    print "Content-type:text/html \n\n";
    read(STDIN, $input, $ENV{"CONTENT_LENGTH"};
    @input = split(/&/, $input);
    foreach $i(0..$#input) {
    $input[$i] = ~s/\+//g;
    ($name, $value) = split(/=/, $input[&i],2);
    $input{$name} .= $value;
    }
    print <<EOF;
    <HTML><HEAD>
    <TITLE>0rder 0utput</TITLE>
    </HEAD>
    <BODY>
    EOF
    foreach (keys %input){
    print "$_=$input{$_)<br>\n";
    }
    print <<EOF;
    </BODY>
    </HTML>
    EOF

我们仔细分析这些代码:

#!/usr/bin/perl
print "Content-type:text/html \n\n";

    这是用Perl编写CGI 程序的最前面两行,第一行指出该CGI是一个Perl程序,并为Perl解释器指明路径。你需要知道系统中Perl解释器的安装位置。通常可用如下Shell 命令找出其安装位置:
which perl
    如果仍然找不烈相应的路径,询问系统管理员。
    代码的第二行告诉所有的人这个特定的Perl程序将执行HTML。
    因为我们使用的是METHOD=POST,从表单中获取的信息通过STDIN(标准输入)传递给CGI。表单数据被读到变量$input中。我们需要用环境变量CONTENT-LENGTH来确定读取的输入流长度。
read(STDIN, $input, $ENV{"CONTENT_LENGTH"};

    注意: 无论何时你用到关联数组,一定要记住使用大括号{}将正引用的数组元素名括起来。

    正如在前面看到的用mailto: 标记将表单输出那样,"名/值"对用"&"号隔开。获取输入串$input,然后将其分成一个数组,由"&"号分开各元素。这个数组的每个元素包含一个由等号隔开的"名/值"对。必须注意,"&"符号通过如下处理从输入串中删去。
@input = split(/&/,$input);
对输入数组(@input)循环处理, 使数据更可读,分离"名/值"对,并将它们放入关联数组中。在Perl中,数组元素个数包含在变量$#array-n-me.foreach $i(o..$#inpu)中。在传递表单数据的URL编码处理中,将所有的空格转化成加号(+)。对@input的每个元素进行全局替换,将+ 号换成空格。在这种情况下,在+前加一个反斜杠(\)很重要,因为+的缺省用途是作为一个通配符使用,"g"表示全局替换:替换所有的+号;而并非只有第一个。

    ch.$input[$i] = ~s/\+/

    我们现在将"名/值"对分开,存入变量$name和$value中,并将值放入到一个关联数组中,以便引用。因为名字和值由一个等号隔开,我们用这个符号分隔变量。由于我们知道要将元素分成多少个域,在这里将LIMIT参数指定为2。注意,这在前面使用的sp1it函数中做不到,因为我们不知道将访问STDIN流的次数。
    通过使用大括号给关联数组元素赋值,可以将上述变量放入到关联数组中。注意我们并不是简单地将值分配到关联数组中,而是还要加上.=,下面讲述原因:

    ($name, $value) = split(/=/, $input[$i], 2);
    $input{$name} .= $value;
    )

    有了Perl,你可以很容易地在CGI 中嵌入HTML,这是因为你可以指明你要将整个文本清单打印出来。这是标准的HTML文件头信息。我用EOT来指明"End Of TexT"(文本结束),但你可使用任何唯一标识的字符串。

    print <<EOT;
    <HTML><HEAD>
    <TITLE>0R>r 0utput</TITLE>
    </HEAD>
    <BODY>
    EOT

    警告: 在Perl中,许多命令可用一串字符作为参数,例如print<<EOT;如果你缩排代码,一定不要缩排含有EOT标识符的行。只要使用perl可接受的标识符,它就能识别出清单结束标识符。这就是说,你不能在结束标识符前面或后面加空格或制表符。

    现在将显示关联数组%input的内容。关联数组元素名为keys。通常关联数组的关键字存在keys中。在foreach循环的重复操作中,当前的关键字(key)存放在特定变量$_中,print 的声明中同时包含一个HTML的换行符(<br>)和一个perl的换行符(\n)。这将在屏幕输出及源码清单中强制换行。

    foreach (keys %input){
    print "$_=$input{$_)<br>\n";
    }


剩下的代码只是对我们的HTML底部作清空处理。

2.2 简单的CGI程序输出

    通过观察这个简单示例程序的数据输出,你可以看到其可读性大大改进。这是通过按下表单中的提交(submit)按钮得到的一些典型数据:

    state = CA
    card_number = 123456789
    countrv = USA
    address =520 Main St,%23204
    email = ken.hunt@westbevhigh.edu
    city = Beverly Hills
    zines = Cooking with Soylent GreenAsteroid Living
    gift = TriCOrder
    suggestions = Can you offer%22Asteroid Living for Kids%22 Magazine%sF%0D%0A
    applicant = Ken Hunt
    payment_methoh = Frontiers Credit Card
    zipcode = 90210


2.3 分析数据

    数据还存在一点问题。它的顺序很随机,表单中复复选框所指的两份杂志由我们所用的附加运算符链接到zines变量中,而且有些字符仍是十六进制形式。
    数据的随机顺序是由Perl存储关联数组哈希(hash)表的方式造成的。在foreach 循环中可以使它们按关键字的字母顺序排序。

foreach (sort keys %input)

    zines 这种变量可能包含多个值,我们需要一种分离这些值的方法。通过在创建关联数组时,检查某个特定变量是否已经定义可以分离这些值。如果是这样,我们将在多个值之间添加逗号","

$input{$name} .= "," if (defined($input{$name}));

    十六进制码在URL编码中用%××格式表示,这里××是十六进制数。我们通过使用对应值替换的方法将此类串变成字符。

$input[$i] = ~s/%(..)/pack("C", hex($i)))/ge;

通过改变,现在的输出为:

    address = 5 20 Main St. # 204
    applicant = Ken Hunt
    card-number = 123456789
    city = BeverhHills
    country = USA
    email = hunt@westbevhiRh.edu
    gift = TriCOrder
    payment_method = Frontiers Credit Card
    state = CA
    suggestions = Can you offer " Asteroid Living for Kids II Magazine?
    zines = Cooking with Soylent Green,Asteroid Living
    zipcode = 90210



2.4 通过METHOD=GET接收表单

     前面所写的代码将适用于任何从表单中通过METHOD-POST 方式传来的数据集。对代码做一些修改,使其同时适用于POST和GET方法。
    回忆一下环境变量,提交表单的方法存于环境变量REQUEST-METHOD中。对于METHOD=GET,由表单传来的数据跟在URL中的"?"后。数据本身存于环境变量QUERY_STRING中。

参见列表清单2,我们得到一个程序,用来分析来自表单的数据,并将其显示在屏幕上。

列表清单2 一个通用表单分析程序

    #!/usr/bin/perl
    print "Content-type:text/html \n\n";
    if ($ENV{REQUEST_METHOD} eq "GET") {
    $input = $ENV{QUERY_STRING}; }
    elsif ($ENV{REQUEST_METHOD} eq "POST") {
    read(STDIN, $input, $ENV{"CONTENT_LENGTH"};}
    else {
    print "Request metnod Unknown";
    }

    @input = split(/&/, $input);
    foreach $i(0..$#input) {
    $input[$i] = ~s/\+//g;
    $input[$i] = ~s/%(..)/pack("C", hex($l))/ge;
    ($name, $value) = split(/=/, $input[&i],2);
    $input{$name} .= "~" if (defined($input{$name}));
    $input{$name} .= $value;
    }
    print <<EOF;
    <HTML><HEAD>
    <TITLE>0R>r 0utput</TITLE>
    </HEAD>
    <BODY>
    EOF
    foreach (keys %input){
    print "$_=$input{$_)<br>\n";
    }
    print <<EOF;
    </BODY>
    </HTML>
    EOF




注意:如果使用cgi-lib库,我们可以编写一个与通用表单分析器类似的程序,而且行数大大减少:

    #!/usr/bin/perl
    require cgi-lib.pl;
    &printHeader;
    &ReadParse(*input);
    &HtmlTop("Order Form 0utput");
    &printVariables(%input);
    &Htmlbot;



    但这样我们学不到Perl的许多知识。cgi-1ib.pl库是一个强大的工具箱,它使程序员的工作大为简化。一旦你对Perl程序运作熟悉后,最好使用它们。关于这个强有力的而且不断发展的cgi-lib.pl库的更多描述,可访问以下cgi-1ib.pl web站点:

http://www.bio.com.ac.uk/cgi-lib

3 查错

    到目前为止,我们实际上仍没有处理订货表单,我们只是将表单的输出显示到屏幕上。我们真正需要的是从某个订货的用户那里获得信息,并将表单信息传递给有关人员。在这张订货表单中,大多数域都要填入指定的信息;每个表单域都必须填入数据,除了card_number 域外,它只有在顾客使用信用卡时才需要填入信息。如果它含有信息,肯定采用一点种特殊的方式。
    首先,让我们记录所有空数据域。我们将在建立关联数组时检查空域。若一个域是空的,我们将把这个域名加到一个错误数组(@errors)中。

push (@errors, "$name") if $value eq "";

    然后,如果有错误,我们将转入一个子程序,告诉用户哪些信息缺少,并要求用户再输入缺少的数据:

    if ($#errors != -1) {
    &printerrors(*errors)}
    else {
    foreach (sort keys %input){
    print "$_ = $input{$_)<br>\n"; }
    }



    注意:如果一个数组是空的,数组的大小即$#errors将为-1而不是零。$#array_name不是@array_name中元素的个数,而是@array_name中最后一个元素的下标。由于Perl数组下标从元素0开始,因此只有一个元素的数组将产生$#array_name=0。另外,*errors是errors变量的全局表示;因此,任何名为"errors"的变量,无论它是一个变量数组还是关联数组,都将包含在其中(@errors,$errors,$#errors,%errors等等)。
    这是printerrors子程序。专用变量@_包含传递给子程序的变量清单。local局部地分配变量。local(*errors)可以是local(*foo)或local(*bar)或其他,只要命名和局部变量在这个模块中一致即可。

    sub printerrors {
    local(*errors) = @_;
    print <<EOT;
    <h2>Your Order could not be processed because the following
    information was either not supplied or was in an incorrect format.<h2>
    <b>
    EOT
    print join("<br>", @errors),"</b><br>\n";
    print "please go back and complete the Order form.";
    }



    因为我们正在捕获所有空数据行,如果card_number域没填入数据,就会出现一个错误,无论顾客指明使用现金还是支票支付都是如此。检查一下这个选项,以确保当域中信息且与信用卡的正确格式相一致时不要出错。我们现在用的方法不捕获那些单选按钮使用的数据或复选框中未输入的数据。在这些情况下,表单并不发送任何信息。

3.1 在表单中嵌入信息

    提示:这是一个私人秘密。我经常在表单自身嵌入一些信息,使信息的排序和存储变得容易。
    我发现这种方法非常有用。在payment_type的值域中,含有付款方式名和信用卡号码card_number的长度。这使检查card_number域长度变得容易得多。你可以用这种技术嵌入诸如价格、型号、信用程度或任何你所需的数据信息。

    <b>Payment Method:</b>
    <br>
    <INPUT TYPE="radio" NAME="payment_method"
    VALUE="FooBar_Charge_Card 8 CHECKED>FooBar Charge Card<br>
    <INPUT TYPE="radio" NAME="payment_method"
    VALUE="Fronts_Credit_Card 6">Fronts Credit Card<br>
    <INPUT TYPE="radio" NAME="payment_methd" VALUE="COD 0">C.0.D.<br>
    <INPUT TYPE="radio" NAME="payment_methd" VALUE="Check 0">Check<br>
    <INPUT TYPE="radio" NAME="payment_methd" VALUE="Money_Order 0">Money Order<br>


    在payment_method中嵌入的信息得到分析,并将其存入变量$method和$number_size中。使用Perl的常规表达式的强大模式匹配特性,(\W+)匹配整个词,(\d)匹配单个数字。

    if ($name eq "card_number") {
    ($method, $number_size)=$input{payinent_method}=~/(\W+)(\d)/;
    if ($value eq "") {
    push (@errors,$name) if ($number-size>0);
    else {
    push (@errors, $name) if ($number_size == 0);
    push (@errors, $name) if (length($value) != $number_size;
    }
    }
    else {
    push (@errors, $name) if ($value eq "0");
    }

    push (@errors, "zines") unless dined $input{zines};

3.2 数据的用途

    既然表单中含有所需的各种数据,我们必须决定怎样处理这些数据,处理这些数据有三种可能的方式: 在屏幕上显示出来,通e-mail发送给某个订货的人,或将结果存入文件中,我们将使用这三种方式。

    这个子程序将订货信息显示到屏幕上,用精致的小表单信件感谢顾客的定购。

    sub printorder {
    local (*input) = @_;
    print <<EOT;
    <h2>Thank you $input{applicant}.</h2>
    The following Order has been placed.Thank you for shopping the Frontier.
    <pre>
    <b>Address:</b>
    <b>$input{applicant}</b>
    $input{address}
    $input{city}, $input{state}
    $input{zipcode}
    $input{country}<p>
    <b>email:</b>$input{email}
    <b>Magazines Ordered:</b>$input{zines}
    <b>Free Gift:</b>$input{gift}
    <b>payment by:</b>$input{paytment_method},$input{card_numhr}<p>
    <b>Comments:</6>
    $input{suggestions}<br>
    </pre>
    <p>
    EOT
    }


    这个子程序使用UNIX系统的sendmail功能,将订货表单通过e-mail发给负责处理订购事务的人员。关于这个子程序有两点你要特别注意。
    第一,必须在e-mail接收者地址的@符号前加一个反斜杠(\),这是因为Perl将@ 解释成一个数组变量的开始。
    第二,调用sendmail的方法。这里所用的方法是将输出逐个发送到sendmail程序中。考虑到安全因素,这比使用系统函数调用sendmail要好得多。下一节将对此作更详细的描述。

    sub emailorder {
    local(*input) = @_;
    $newOrders = "mqingyi@126.com";
    open (MAIL, "|/usr/sbin/sendmail -t");

    print MAIL <<EOM;
    To:$newOrders
    Subject:Order from website

    The following Order has been submitted:
    Name: $input{applicant}
    Address: $input{address}
    $input{city}, $input{state}
    $input{zipcode}
    $input{country}
    email: $input{email}
    Zines: $input{zlnes}
    Free Gift: $input{gift}
    Paying By: $input{payment_method}, $input{card_number}
    Comments: $input{suggestions}
    EOM
    close(MAIL);
    }

3.3 保留的内容

    一旦我们收集到所有变量的信息,我们也可以保存一些。保存一份在线订购人员的姓名和e-mail地址的清单很有用,这样当我们能提供新产品时,可以通知他们。通常将诸如此类的内数据以制表符或逗号分开的格式保存,因为那些格式可广泛用于电子表格和读取信息的数据库程序。
    下面的代码段可以在读取数据后随时插入:

    open (OUTFILE,">>email_list.txt");
    print OUTFILE "$input{applicant}, $input{email}\n";
    close (OUTFILE);

4 安全问题

    Internet和WWW的确具有许多功能,但信息在Internet上传输也存在安全问题,CGI程序员也必须了解安全方面的知识。同时,关于C61脚本的安全性也有一些重要的问题。

4.1 交易安全性

    对于每个人,他们的信用卡号码在Internet上使用时,都会担心信用卡号码被窃取,安全预防很有必要。下面是几个基本的预防措施,你必须采纳,以确具有最高层次的安全防范:

·运行一个支持RSA加密的服务器。
·不要将顾客的信用卡号码以未加密文件的形式存在系统中某个不安全的地方。
·不要用e-mail或其他传输手段传输未加密的重要数据。

4.2 CGI安全性

    另一个方面是CGI 本身的安全性。通过允许全世界用户往我们的机器中输人信息,我们为那些试图送给我们垃圾信息的人打开了方便之门。最常用的方法是采用UNIX shell命令,通过与shell自身交互的功能,达到访问系统的目的。
    避免系统对所有信息都开放的最好方法,是永远不要相信用户发送来的数据就是你想要的。你必须运用本页介绍过的查错技术,在你调用诸如Sendmail之类的危险应用程序之前,对所有输入信息进行仔细的检查。

关于安全问题更多的描述请参阅本站"CGI 安全" cgisafety.htm。