OSSP CVS Repository

ossp - ossp-pkg/mct/mct.cgi 1.1
Not logged in
[Honeypot]  [Browse]  [Directory]  [Home]  [Login
[Reports]  [Search]  [Ticket]  [Timeline
  [Raw

ossp-pkg/mct/mct.cgi 1.1
#!/e/epaperwork/sw/bin/perl
##
##  OSSP mct - Multiple Choice Test Engine
##  Copyright (c) 2002 Ralf S. Engelschall <rse@engelschall.com>
##  Copyright (c) 2002 The OSSP Project <http://www.ossp.org/>
##  Copyright (c) 2002 Cable & Wireless Deutschland <http://www.cw.com/de/>
##
##  This file is part of OSSP mct, a multiple choice test engine
##  which can be found at http://www.ossp.org/pkg/tool/mct/.
##
##  Permission to use, copy, modify, and distribute this software for
##  any purpose with or without fee is hereby granted, provided that
##  the above copyright notice and this permission notice appear in all
##  copies.
##
##  THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
##  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
##  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
##  IN NO EVENT SHALL THE AUTHORS AND COPYRIGHT HOLDERS AND THEIR
##  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
##  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
##  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
##  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
##  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
##  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
##  OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
##  SUCH DAMAGE.
##
##  mct.cgi: multiple choice test CGI engine (language: Perl)
##

#   requirements
require 5.006;
use IO;               # standard in Perl 5.6
use POSIX;            # standard in Perl 5.6
use CGI;              # standard in Perl 5.6
#use Mail::Sendmail;   # standard in OpenPKG's Perl, else: CPAN!

#   program configuration
my $MY = {
    PROG_NAME => 'OSSP mct',
    PROG_VERS => '1.0.0',
    PROG_DESC => 'Multiple Choice Test Engine',
    PROG_HOME => 'http://www.ossp.org/pkg/tool/mct/',
    MAIL_FROM => 'mct@epaperwork.org',
    MAIL_HOST => 'mail.epaperwork.org',
    TEMPLATE  => "<html>\n<head>\n\@HEAD\@\n</head>\n<body>\n\@BODY\@\n</body></html>"
};

#   switch to unbuffered output
$|++;

#   establish CGI object
my $CGI = new CGI;

#   generate an HTTP response
sub http_response {
    my ($head, $body) = @_;

    #   fill in template page
    my $response = $MY->{TEMPLATE};
    $response =~ s|\@HEAD\@|$head|sg;
    $response =~ s|\@BODY\@|$body|sg;
    
    #   prepend with HTTP header
    $response = $CGI->header(
        -type             => 'text/html', 
        -expires          => 'now', 
        '-Content-length' => length($response)
    ) . $response;

    return $response;
}

#   activate a general error handler
$SIG{__DIE__} = sub {
    my ($err) = @_;

    $err =~ s|at\s+\S+\s+line\s+(\d+)|(line $1)|s;
    $err =~ s|\n|<br>\n|sg;
    print STDOUT &http_response(
        "<title>ePaperwork :: Multiple Choice Test: ERROR</title>",
        "<h1>ePaperwork :: Multiple Choice Test: ERROR</h1>\n" .
        "<pre>$err</pre>\n"
    );
    exit(0);
};

#   convert Sugar-style markup text to HTML text
sub sug2html {
	my ($text) = @_;

    #   escape HTML special characters
	$text =~ s|&|&amp;|sg;
	$text =~ s|<|&lt;|sg;
	$text =~ s|>|&gt;|sg;

    #   expand Sugar markup
	$text =~ s/\|\|(.+?)\|\|/<tt>$1<\/tt>/sg;
	$text =~ s|//(.+?)//|<i>$1</i>|sg;
	$text =~ s|\*\*(.+?)\*\*|<b>$1</b>|sg;
	$text =~ s|-&gt;(.+?)::(.+?)&lt;-|<a href="$2" target="mct-extern">$1</a>|sg;
	$text =~ s|-&gt;(.+?)&lt;-|<a href="$1" target="mct-extern">$1</a>|sg;

	return $text;
}

#   read and parse MCT specification
sub readmct {
    my ($name) = @_;

    #   read MCT specification
    my $fh = new IO::File "<$name"
        || die "unable to open MCT specification \"$name\"";
    my $mct = '';
    $mct .= $_ while (defined($_ = $fh->getline()));
    $fh->close();

    #   provide line continuation feature
    $mct =~ s|\s*\\\s*\n\s*| |sg;

    #   parse MCT specification
    my $MCT = {
        'HEADER'   => {},
        'RESULT'   => [],
        'QUESTION' => []
    };
    $mct =~ s|^(.+?\n)\n(.+)$|&parse_header($1), &parse_body($2), ''|sge;

    #   parse header lines
    sub parse_header {
        my ($mct) = @_;
        while ($mct =~ s|^([A-Za-z][A-Za-z0-9-]*):\s*(.*?)\n||s) {
            if (not defined($MCT->{HEADER}->{uc($1)})) {
                $MCT->{HEADER}->{uc($1)} = $2;
            }
            else {
                $MCT->{HEADER}->{uc($1)} .= $2;
            }
        }
    }

    #   parse body blocks
    sub parse_body {
        my ($mct) = @_;
        $mct = "\n" . $mct . "\n";
        $mct =~ s|\n\?\?\s+(.+?)\n(.+?\n)(?=\n)|&parse_question($1, $2), ''|sge;
        $mct =~ s|\n==\s+(.+?)\n(.+?\n)(?=\n)|&parse_result($1, $2), ''|sge;
    }

    #   parse a single question block
    sub parse_question {
        my ($title, $mct) = @_;
        my $e = { 
            'TITLE'   => $title, 
            'HINT'    => '',
            'CHOICES' => []
        };
        foreach my $line (split(/\n/, $mct)) {
            if ($line =~ m/^((?:!!))\s+(.+)$/s) {
                $e->{HINT} = $2;
            }
            elsif ($line =~ m/^((?:--|\+\+))\s+(.+)$/s) {
                my $c = { 'TYPE' => $1, 'TEXT' => $2 };
                push(@{$e->{CHOICES}}, $c);
            }
        }
        push(@{$MCT->{QUESTION}}, $e);
    }

    #   parse a single result block
    sub parse_result {
        my ($title, $mct) = @_;
        my $e = { 
            'TITLE'   => $title, 
            'CHOICES' => []
        };
        foreach my $line (split(/\n/, $mct)) {
            if ($line =~ m/^((?:--))\s+(\d+)-(\d+)\s+(.+)$/s) {
                my $c = { 
                    'TYPE'  => $1, 
                    'RANGE' => [$2, $3], 
                    'TEXT'  => $4 };
                push(@{$e->{CHOICES}}, $c);
            }
        }
        push(@{$MCT->{RESULT}}, $e);
    }

    return $MCT;
}

#   deterministically permutate an array
sub permute {
    my ($k, @a) = @_;

    my @b = ();
    $k = ($k * ($#a+1)) / 9;
    $k = 1 if ($k < 1);
    $k = ($k % ($#a+1));
    $k = 1 if ($k < 1);

    my $i = $k/2;
    while ($#a > -1) {
        for (my $j = 0; $j < ($#a+1); $j++) {
            my $x = shift(@a);
            if (($i % $k) == 0) {
                push(@b, $x);
            }
            else {
                push(@a, $x);
            }
            $i++;
        }
        $k-- if ($k > 1);
    }

    return @b;
}

#   adjust CGI environment to make sure that self-referencing
#   URLs go again through the direct URL of the MCT file!!
$ENV{SCRIPT_NAME} = $ENV{PATH_INFO};
delete $ENV{REQUEST_URI};

#   fetch CGI parameters
my $name   = $CGI->path_translated();
my $form   = $CGI->param('form')   || '';
my $perm   = $CGI->param('perm')   || '';
my $hint   = $CGI->param('hint')   || '';
my $note   = $CGI->param('note')   || '';
my $serial = $CGI->param('serial') || '';

#   sanity checks on CGI parameters
if ($name !~ m|\.mct$|) {
    die "MCT specification \"$name\": invalid path name";
}
if (not -f $name) {
    die "MCT specification \"$name\": file not found";
}
if ($perm ne '' and $perm !~ m/^[1-9]$/) {
    die "Invalid \"perm\" parameter value \"$perm\"";
}
if ($hint ne '' and $hint !~ m/^[01]$/) {
    die "Invalid \"hint\" parameter value \"$hint\"";
}
if ($note ne '' and $note !~ m/^[01]$/) {
    die "Invalid \"note\" parameter value \"$note\"";
}
if ($serial ne '' and $serial !~ m/^\d+$/) {
    die "Invalid \"serial\" parameter value \"$serial\"";
}

#   read MCT specification
my $MCT = &readmct($name);

#  XXX

#   optionally read HTML page template
my $template = $name;
$template =~ s|\.mct$|.html|s;
my $fh = new IO::File "<$template" || die;
$MY->{TEMPLATE} = '';
$MY->{TEMPLATE} .= $_ while (<$fh>);
$fh->close();

#   make sure the serial numbers match
if ($serial ne '') {
    if ($serial ne $MCT->{HEADER}->{SERIAL}) {
        die "Sorry, serial in request (#$serial) does not match current MCT serial (#".$MCT->{HEADER}->{SERIAL}.")";
    }
}
$serial = $MCT->{HEADER}->{SERIAL};

#   calculate query summary
my $summary = '';
sub summarynum {
    my ($num) = @_;
    return $num if ($num < 10);
    return "{$num}";
}
my $nq = 0;
foreach my $q (@{$MCT->{QUESTION}}) {
    $summary .= "," if ($summary ne '');
    $summary .= &summarynum($nq);
    $question = $CGI->param("q$nq");
    my $nc = 0;
    foreach my $c (@{$q->{CHOICES}}) {
        my $choice = ($CGI->param("q$nq.c$nc") || ($question eq "c$nc"));
        if ($choice) {
            $summary .= &summarynum($nc);
        }
        $nc++;
    }
    $nq++;
}

#   support Submit button
if ($form eq 'Send Notes') {
    my $body = '';
    $body .= "Dear author of the Multiple Choice Test \"".$MCT->{HEADER}->{NAME}."\",\n" .
             "my current choice (".$CGI->url(-full => 1)."?choice=$summary)\n" .
             "resulted in the following annotation notes for you.\n" .
             "\n";
    $nq = 0;
    foreach my $q (@{$MCT->{QUESTION}}) {
        my $an = ($CGI->param("q${nq}.an") || "");
        if ($an ne '') {
            $body .= "Question:\n" .
                     "| ".$q->{TITLE}."\n";
            $an =~ s/^/| /mg;
            $body .= "Note:\n".$an;
            $body .= "\n\n";
        }
        $CGI->param("q${nq}.an", "");
        $nq++;
    }
    my %mailcfg = ( 
        'To'      => $MCT->{HEADER}->{"REPLY-TO"},
        'From'    => $MY->{MAIL_FROM},
        'Subject' => "Notes on MCT \"".$MCT->{HEADER}->{NAME}."\"",
        'Message' => $body,
        'smtp'    => $MY->{MAIL_HOST}
    );
    sendmail(%mailcfg) or die $Mail::Sendmail::error;
    $CGI->param('note', 0);
    $note = 0;
    $form = "  Rate!  ";
}

#   support Mail button
if ($form eq 'Mail') {
    sub urlescape {
        my ($text) = @_;
        $text =~ s|([ \t&+?:/=\n\r])|sprintf("%%%02x", ord($1))|sge;
        return $text;
    }
    my $subject = &urlescape("Result in MCT \"".$MCT->{HEADER}->{NAME}."\"");
    my $body    = &urlescape("Result: ".$CGI->url(-full => 1)."?choice=$summary");
    print STDOUT $CGI->redirect(-uri => "mailto:".$MCT->{HEADER}->{"REPLY-TO"}."?subject=$subject&body=$body", -type => "text/plain");
    exit(0);
}

#   support Restart button by redirecting via GET 
if ($form eq 'Restart') {
    print STDOUT $CGI->redirect(-uri => $CGI->url(-full => 1), -type => "text/html");
    exit(0);
}

#   support Note button
if ($form eq 'Show Notes') {
    $CGI->param('note', 1);
    $note = 1;
}
elsif ($form eq 'Hide Notes') {
    $CGI->param('note', 0);
    $note = 0;
}

#   support Hint button
if ($form eq 'Show Hints') {
    $CGI->param('hint', 1);
    $hint = 1;
}
elsif ($form eq 'Hide Hints') {
    $CGI->param('hint', 0);
    $hint = 0;
}

#   calculate new permution index
if ($perm eq '') {
    $perm = int((rand 8) + 1);;
    $CGI->param('perm', $perm);
}

#   support Bookmark button
if ($form eq 'Bookmark') {
    #   redirect to bookmark URL
    # XXX print STDOUT $CGI->redirect($CGI->url(-full => 1)."?name=$name&perm=$perm&choice=$summary");
    print STDOUT $CGI->redirect(-uri => $CGI->url(-full => 1)."?serial=$serial&perm=$perm&choice=$summary", -type => "text/plain");
    exit(0);
}
if ($CGI->param('choice') ne '') {
    #   import choice from bookmark URL
    my $choice = $CGI->param('choice');
    sub nextnum {
        my ($str) = @_;
        my $num = '';
        if ($str =~ m|^\{(\d+)\}(.*)$|s) {
            ($num, $str) = ($1, $2);
        }
        elsif ($str =~ m|^(\d)(.*)$|s) {
            ($num, $str) = ($1, $2);
        }
        elsif ($str =~ m|^.(.*)$|s) {
            $str = $1;
        }
        return ($num, $str);
    }
    foreach $str (split(/,/, $choice)) {
        my ($nq, $str) = &nextnum($str);
        if ($nq ne '') {
            if (defined($MCT->{QUESTION}->[$nq])) {
                $q = $MCT->{QUESTION}->[$nq];
                my $multichoice = 0;
                foreach my $c (@{$q->{CHOICES}}) {
                    if ($c->{TYPE} eq '++') {
                        $multichoice++;
                    }
                }
                $multichoice = 0 if ($multichoice <= 1);
                if ($multichoice) {
                    my $nc;
                    while ($str ne '') {
                        ($nc, $str) = &nextnum($str);
                        $CGI->param("q$nq.c$nc", "1");
                    }
                }
                else {
                    my ($nc, $str) = &nextnum($str);
                    $CGI->param("q$nq", "c$nc");
                }
            }
        }
    }
    $form = "  Rate!  ";
}

#   start generating the web page
my $head = '';
my $body = '';

$head .= "<title>ePaperwork :: Multiple Choice Test: ".
         $MCT->{HEADER}->{TITLE}."</title>";

#   import CSS
sub addcss {
    my ($file) = @_;
    $head .= "<style type=\"text/css\"><!--\n";
    my $fh = new IO::File "<$file" || die;
    $head .= $_ while (<$fh>);
    $fh->close();
    $head .= "--></style>\n";
}
my $css = $name;
$css =~ s|\.mct$|.css|s;
if (-f "$css") {
    &addcss("$css");
}
if (-f "mct.css") {
    &addcss("mct.css");
}

$body .= "<table width=100% cellspacing=0 cellpadding=1 border=0>";
$body .= "<tr><td colspan=2 class=mct-border>";
# XXX $body .= $CGI->startform(-method => "POST", -action => $CGI->url(-full => 1)."?name=$name");
$body .= $CGI->startform(-method => "POST", -action => $CGI->url(-full => 1));
#$body .= $CGI->hidden(-name => 'name', -default => [$name]);
$body .= $CGI->hidden(-name => 'perm', -default => [$perm]);
$body .= $CGI->hidden(-name => 'hint', -default => [$hint]);
$body .= $CGI->hidden(-name => 'note', -default => [$note]);
$body .= $CGI->hidden(-name => 'serial', -default => [$serial]);

$body .= "<table width=100% cellspacing=0 cellpadding=4 border=0>";
$body .= "<tr><td colspan=2 class=\"mct-header\">\n";
$body .= "<blockquote>\n";
$body .= &sug2html($MCT->{HEADER}->{TITLE});
$body .= "</blockquote>\n";
$body .= "</td></tr>";

#   determine whether to enable cheat mode
my $cheat = 0;
if ($MCT->{HEADER}->{PASSWORD} ne '') {
    $nq = 0;
    foreach my $q (@{$MCT->{QUESTION}}) {
        my $an = ($CGI->param("q${nq}.an") || "");
        if (index($an, $MCT->{HEADER}->{PASSWORD}) > -1) {
            $cheat = 1;
            last;
        }
        $nq++;
    }
}

my $r_questions = 0;
my $r_score     = 0;

$nq = 0;
foreach my $q (@{$MCT->{QUESTION}}) {
	$r_questions++;
	my $col0 = ($nq % 2);
	$body .= "<tr>\n";
	$body .= "<td width=40% valign=top class=mct-qt$col0>" .
			 "<span class=\"mct-qt\">".&sug2html($q->{TITLE})."</span>";
    my $multichoice = 0;
	foreach my $c (@{$q->{CHOICES}}) {
        if ($c->{TYPE} eq '++') {
            $multichoice++;
        }
    }
    if ($multichoice <= 1) {
        $multichoice = 0;
    }
    if ($multichoice) {
        $body .= "<br>($multichoice answers expected)";
    }
    if ($hint) {
        if ($q->{HINT} ne '') {
            $body .= "<br><span class=\"mct-hint\">Hint: ".&sug2html($q->{HINT})."</span>\n";
        }
    }
    if ($note) {
        $body .= "<br>" . 
                 $CGI->textarea(-name => "q${nq}.an",
                              -default => ($CGI->param("q${nq}.an") || ""), 
                              -rows => 3, -columns => "30%");
    }
    else {
        $body .= $CGI->hidden(-name => "q${nq}.an", -default => [($CGI->param("q${nq}.an") || "")]);
    }
    $body .= "<p></td>\n";
	$body .= "<td width=60% valign=top class=mct-qt$col0>" .
			 "<span class=\"mct-qc\">\n";
	$body .= "<table width=100% cellspacing=0 cellpadding=0 border=0>\n";
	my $nc = 0;
	my $q_choices = 0;
	my $q_score   = 0;
	my $q_ok      = 0;
    my @cells = ();
	foreach my $c (@{$q->{CHOICES}}) {
	    $q_choices++;
		my $choice = $CGI->param("q$nq.c$nc");
        if (not $multichoice) {
		    $choice = $CGI->param("q$nq");
            if ($choice eq "c$nc") {
                $choice = 1;
            }
            else {
                $choice = 0;
            }
        }
        my $class = '';
		if ($choice) {
			if ($c->{TYPE} eq '++') {
				$class = "OK";
				$q_score++;
			}
			else {
				$class = "BAD";
				$q_score--;
			}
		}
        else {
			if ($c->{TYPE} eq '++') {
				$class = "CHEAT" if ($form ne '' && $cheat);
            }
        }
		if ($c->{TYPE} eq '++') {
			$q_ok++;
		}
        my $cell = '';
        if ($multichoice) {
		    $cell .= "<input ".($choice ? "checked " : "").
                     "type=\"checkbox\" name=\"q$nq.c$nc\" value=\"1\">";
        }
        else {
		    $cell .= "<input ".($choice ? "checked " : "").
                     "type=\"radio\" name=\"q$nq\" value=\"c$nc\">";
        }
		$cell .= "&nbsp;".&sug2html($c->{TEXT})."<br>\n";
        my $c = {
            'CLASS'  => $class,
            'HTML'   => $cell,
            'LENGTH' => length($c->{TEXT})
        };
        push(@cells, $c);
		$nc++;
	}

    @cells = &permute($perm, @cells);

    my $maxlen  = 0;
    foreach my $cell (@cells) {
        if ($maxlen < $cell->{LENGTH}) {
            $maxlen = $cell->{LENGTH};
        }
    }
    my $cols = 1;
    $cols = 2 if ($maxlen < 30);
    $cols = 3 if ($maxlen < 10);
    $cols = 4 if ($maxlen < 5);
    my $rows = sprintf("%d", (($#cells+1) + ($cols - 1)) / $cols);
    for (my $i = 0; $i < $rows; $i++) {
        $body .= "<tr>\n";
        for (my $j = 0; $j < $cols; $j++) {
            my $cell = ($cells[$i+($j*$rows)] || { 'CLASS' => '', 'HTML' => '&nbsp;'});
            my $class = $cell->{CLASS};
            if ($class eq '' or $class eq 'OK') {
		        my $col1 = ($i % 2);
		        $class = "$col0$col1";
            }
		    $body .= "<td class=mct-qc$class>";
            $body .= $cell->{HTML};
		    $body .= "</td>";
        }
        $body .= "</tr>\n";
    }
	$q_score = 0 if ($q_score < 0);
	my $q_score = sprintf("%d", (($q_score / $q_ok) * 100));
	$r_score += $q_score;
	$body .= "</table>\n";
	$body .= "</span><p></td>\n";
	$body .= "</tr>\n";
	$nq++;
}
$body .= "</table>\n";
my $result = sprintf("%d", ($r_score / $r_questions));

$body .= "</td></tr>";
$body .= "<tr>";
$body .= "<td align=left class=\"mct-button\">";
if ($form ne '') {
    if ($note) {
        $body .= "<span class=\"mct-button-switchnotes\">";
        $body .= $CGI->submit(-name => 'form', -value => "Hide Notes") . "\n";
        $body .= "</span>";
        $body .= "<span class=\"mct-button-sendnotes\">";
        $body .= $CGI->submit(-name => 'form', -value => "Send Notes") . "\n";
        $body .= "</span>";
    }
    else {
        $body .= "<span class=\"mct-button-switchnotes\">";
        $body .= $CGI->submit(-name => 'form', -value => "Show Notes") . "\n";
        $body .= "</span>";
    }
    if ($hint) {
        $body .= "<span class=\"mct-button-hints\">";
        $body .= $CGI->submit(-name => 'form', -value => "Hide Hints") . "\n";
        $body .= "</span>";
    }
    else {
        $body .= "<span class=\"mct-button-hints\">";
        $body .= $CGI->submit(-name => 'form', -value => "Show Hints") . "\n";
        $body .= "</span>";
    }
    $body .= "<span class=\"mct-button-bookmark\">";
    $body .= $CGI->submit(-name => 'form', -value => "Bookmark") . "\n";
    $body .= "</span>";
    $body .= "<span class=\"mct-button-mail\">" .
             $CGI->submit(-name => 'form', -value => "Mail") . 
             "</span>\n";
}
$body .= '&nbsp;';
$body .= "</td>";
$body .= "<td align=right class=\"mct-button\">";
if ($form ne '') {
    $body .= "<span class=\"mct-button-restart\">" .
             $CGI->submit(-name => 'form', -value => "Restart") .
             "</span>\n";
}
$body .= "<span class=\"mct-button-undo\">" .
         $CGI->reset(-value => "Undo") . 
         "</span>\n";
$body .= "<b><span class=\"mct-button-rate\">" . $CGI->submit(-name => 'form', -value => "  Rate!  ") . "</span></b>\n";
#$body .= "&nbsp;";
$body .= $CGI->endform;
$body .= "</td>";
$body .= "</tr>";
if ($form ne '') {
	#   display results
	$body .= "<tr>";
	$body .= "<td colspan=2 align=left class=\"mct-result\">";
	$body .= "<p><blockquote>";
	$body .= "You have achieved <span class=\"mct-result-percent\">${result}%</span> success in this multiple choice test.\n";
    foreach my $r (@{$MCT->{RESULT}}) {
	    $body .= $r->{TITLE} . " ";
	    foreach my $c (@{$r->{CHOICES}}) {
			if ($c->{RANGE}->[0] <= $result and $result <= $c->{RANGE}->[1]) {
				$body .= &sug2html($c->{TEXT});
				last;
			}
		}
		$body .= "\n";
	}
	$body .= "<br><br></blockquote>";
	$body .= "</td></tr>";
}
$body .= "<tr>";
$body .= "<td colspan=2 align=center class=\"mct-footer\">";
$body .= "<a href=\"".$MY->{PROG_HOME}."\">".$MY->{PROG_NAME}."</a> ".
         $MY->{PROG_VERS}." - ".$MY->{PROG_DESC}."<br>\n";
$body .= "</td>";
$body .= "</tr>";
$body .= "</table>\n";
$body .= "\n";

print STDOUT &http_response($head, $body);

exit(0);


CVSTrac 2.0.1