#!/usr/bin/perl -w
# -*-perl-*-
##################################################################
# "P4CGI" perl module.
#
# This package is documented using perlpod.
#
package P4CGI ;
###
# You might need to uncomment and modify this to set the lib
# to point to the perl libraries.
#
#use lib '/usr/local/lib/perl5/site_perl' ;
###
# Specify configuration file path
#
sub ConfigFileName()
{
return "./P4DB.conf" ; # Change here to move or rename configuration file
}
use CGI ;
use CGI::Carp ;
use strict;
# A feeble attempt to fence off the most blatant DOS attacks.
$CGI::POST_MAX=1024 * 100; # max 100K posts
$CGI::DISABLE_UPLOADS = 1; # no uploads
#### Conficuration file name
my $CONFIG_FILE ;
#### Store error information
my @ERRLOG ; ## Error log
my $ERRORS ; ## Error counter
#### The following variables are set or updated by the init() routine.
my $CGI; # Contains CGI object from CGI.pm
# Constants
my $VERSION ; # P4DB version
my $CHANGELEVEL ; # P4 change level for relesed version
my $REQUIRED_SERVER_VERSION_YEAR ; # Required server version year
my $REQUIRED_SERVER_VERSION_NUMBER ; # Required server version number
my $STYLE_SHEET ; # Style sheet file name
my $HELPFILE_PATH; # Path to help file (html) ### OBSOLETE???
# Information read from p4 depot:
my $LASTCHANGE ; # Current change level
my $SERVER_VERSION_YEAR ; # Server year
my $SERVER_VERSION_NO ; # Server count
my $SERVER_VERSION_QUAL ; # Server version qualifier
my $SERVER_VERSION_CHANGE ; # Server change No.
my $NO_CONTACT_ERROR ; # Defined if problem with
# Information from configuration file.a
my %CONF ; # Hash containing configuration
my $P4 ; # Contains p4 command
my $PORT; # Contains p4 port
my $P4_HOSTNAME; # Contains p4 host name
my $UNUSEDCLIWL ; # Unused client warning level (from configuration file)
my $UNUSEDUSRWL ; # Unused user warning level (from configuration file)
my $REDIRECT_ERROR_TO_NULL_DEVICE; # Command that redirects errors to /dev/null
my $REDIRECT_ERROR_TO_STDOUT; # Command that redirects errors to stdout
my @P4DBadmin ; # Admins for P4DB and Perforce server
# Information store in cookies (user preferences etc).
my %SHORTCUTS ; # Shortcuts (from cookie)
my %PREF ; # Current user preferences
my %PREF_LIST ; # List of available preferences
my $IGNORE_CASE; # "YES" if case should be ignored
my $MAX_CHANGES; # Max changes for change list
my $TAB_SIZE; # Tab size for file viewer
my $DEBUG ; # Debug mode, When true, prints log (from preferences)
# Misc info.
my $LEGEND ; # Legend supplied to header (from CGi script)
#### Other package variables
my $pageStartPrinted ; # Mark that html header is printed
# Used to avoid mutliple headers if an error occurres
my %EXTRAHEADER ;
my $ONLOADSCRIPT ;
my $helpTarget ; # target for help text
my $printHeaderButtons ;
###
### Subroutine that prints the error log
###
sub prerrlog() {
my $res = "" ;
if(@ERRLOG> 0) {
map {
if(/^ERROR:/) { $_ = "" . &htmlEncode($_) . "" ; }
else { $_ = "" . &htmlEncode($_) . "" ; }
} @ERRLOG ;
$res .=
"
Log printout:
" .
$CGI->self_url() .
join("\n",@ERRLOG) .
"
\n" ;
} ;
return $res ;
}
###
### Initialization code, called from BEGIN().
###
sub init( )
{
$printHeaderButtons="Y" ;
# We require server from 2001 or newer
#
$REQUIRED_SERVER_VERSION_YEAR = 2002 ;
$REQUIRED_SERVER_VERSION_NUMBER = 2 ;
# Set config file name
#
$CONFIG_FILE = &ConfigFileName() ;
## death handler
$SIG{'__DIE__'} = sub { # Thank you Ron Shalhoup for the tip
my($error) = shift;
&P4CGI::bail("Signal caught: $error") ;
exit 0;
};
# clear error counter
#
$ERRORS = 0 ;
# Set P4DB version
#
$VERSION="3.1b1" ;
$CHANGELEVEL=4018 ;
# Set some configuration defaults
#
$HELPFILE_PATH = "./" ;
$UNUSEDCLIWL = 10 ;
$UNUSEDUSRWL = 10 ;
# Initiate CGI module
#
$CGI = new CGI ;
$CGI->autoEscape(undef) ;
# Setup list of preferences
#
%PREF_LIST =
(
"DP" => ["a:LIST","P4 Depot", 0],
"IC" => ["d:BOOL","Ignore case", 0],
"MX" => ["d:INT" ,"Max changes to show", 400],
"TB" => ["d:INT" ,"Default tab stop", 8],
"HD" => ["e:BOOL","Hide deleted files by default", 0],
"VC" => ["f:BOOL","View files with colors", 1],
"ST" => ["f:LIST","Style Sheet File", 0],
"PH" => ["f:LIST","Header style", 0,"Pulldown Menues","Text Links"],
# Comment out line below to disable log printouts completely.
"DBG" => ["z:BOOL","Print log information (debug)", 0],
) ;
# Read user preferences
#
%PREF=$CGI->cookie(-name=>"P4DB_PREFERENCES") ; # First try cookie...
my $p ;
foreach $p (keys %PREF_LIST) { # Fill in defaults for those missing in cookie
if(! defined $PREF{$p}) {
$PREF{$p} = $ {$PREF_LIST{$p}}[2];
ERRLOG("Set default for $p!") ;
}
} ;
foreach $p (keys %PREF) { # Check that all specified prefereces really exists...
if(exists $PREF_LIST{$p}) {
ERRLOG("PREF: $p => $PREF{$p} (${$PREF_LIST{$p}}[1])") ;
}
else {
delete $PREF{$p} ;
} ;
} ;
# If new preferences are specified, parse and modify
if(defined $CGI->param("SET_PREFERENCES")) {
my $c ;
foreach $c (keys %PREF) {
my $val = $CGI->param($c) ;
if(defined $val) {
$CGI->delete($c) ;
my $type = $ {$PREF_LIST{$c}}[0] ;
if($type eq "INT") { $val =~ /^\d+$/ or next ; } ;
if($type eq "BOOL") { $val =~ /^[01]$/ or next ; } ;
if($c eq "DP" and
$PREF{$c} != $val) {
# if we get here the user as modified depot, we should restart with index.cgi
&ONLOADSCRIPT("location='index.cgi';") ;
$CGI->delete("SET_PREFERENCES") ; # To stop SetPreferences.cgi from replacing the ONLOADSCRIPT
}
$PREF{$c} = $val ;
}
}
}
# Set up data structure for configuration file
#
my %configReaderData = ( "P4PATH" => \$P4,
"HTML_HELPFILE_PATH" => \$HELPFILE_PATH,
"P4DB_ADMIN" => \@P4DBadmin,
"SHELL" => \$ENV{"SHELL"},
"REDIRECT_ERROR_TO_NULL_DEVICE" => \$REDIRECT_ERROR_TO_NULL_DEVICE,
"REDIRECT_ERROR_TO_STDOUT" => \$REDIRECT_ERROR_TO_STDOUT,
"DEPOT" => $PREF_LIST{"DP"},
"STYLES" => $PREF_LIST{"ST"},
"UNUSED_CLIENT_WARNING_LEVEL" => \$UNUSEDCLIWL,
"UNUSED_USER_WARNING_LEVEL" => \$UNUSEDUSRWL
) ;
# Read configuration file
#
local *F ;
my $line = 0 ;
open(F,"<$CONFIG_FILE") or &P4CGI::bail("Could not open config file \"$CONFIG_FILE\" for read") ;
while() {
$line++ ;
chomp ; # Remove newline
s/^\s+// ; # Remove leading spaces
next if (/^\#/ or /^\s*$/) ; # Skip if comment or empty line
s/\s+/ /g ; # Normalize all spaces to a single space
s/ $// ; # Remove trailing spaces
# Check syntax and get data
/^(\S+):\s*(.*)/ or
&P4CGI::bail("Parse error in config file \"$CONFIG_FILE\" line $line:\n\"$_\"") ;
# Get values
my ($res,$val) = ($1,$2);
# Make sure configuration exist
if(! exists $configReaderData{$res}) {
&P4CGI::bail("Parse error in config file \"$CONFIG_FILE\" line $line:\n\"$_\"") ;
} ;
# Get config value and check type
my $ref = $configReaderData{$res} ;
my $type = ref($ref) ;
$type eq "SCALAR" and do { $$ref = $val ; ERRLOG("$res=$val") ; next ; } ;
$type eq "ARRAY" and do {
if($res =~ /^\@/) {
push @$ref,split /\s+/,$val ;
}
else {
push @$ref,$val ;
} ;
# Security note: any user can se p4 user and password in log. Uncomment for debug only.
# ERRLOG("push $res,$val") ;
next ;
} ;
&P4CGI::bail("Illegal config type $type line $line:\n\"$_\"") ;
}
close F ;
# Set port and p4 command
#
$PORT = $ {$PREF_LIST{"DP"}}[$PREF{"DP"}+3] ;
if(!defined $PORT) {
$PORT = $ {$PREF_LIST{"DP"}}[3] ;
$PREF{"DP"} = 0 ;
}
bail("DEPOT NOT DEFINED") unless defined $PORT ;
$P4_HOSTNAME=$PORT ;
$P4_HOSTNAME =~ s/:.*$//;
$PORT =~ /(\S+)\s(\S+)\s(\S+)\s(\S+)/ or do { bail("DEPOT line not correct ($PORT)") ; } ;
$PORT = $1 ;
$P4 .= " -p $1 -u $2 -c $3 " ;
if($4 ne "*") {
$P4 .= "-P $4 " ;
} ;
# Security note: any user can see p4 user and password in log. Uncomment for debug only.
# push @ERRLOG,"P4 command set to: \"$P4\"" ;
# Set variables from config
$IGNORE_CASE = $PREF{"IC"} ? "Yes" : "No" ;
$TAB_SIZE = $PREF{"TB"} ;
$TAB_SIZE = 16 if $TAB_SIZE > 16 ;
$TAB_SIZE = 0 if $TAB_SIZE <= 0 ;
$MAX_CHANGES = $PREF{"MX"} ;
my @t =split(/ /,$ {$PREF_LIST{"ST"}}[3+$PREF{"ST"}]) ;
$STYLE_SHEET = $t[0] ;
push @ERRLOG,"Style sheet file: $STYLE_SHEET" ;
foreach (keys %ENV) {
push @ERRLOG,"Environment variable $_: \"". $ENV{$_} . "\"" ;
} ;
# Check that we have contact with p4 server
#
$LASTCHANGE= undef ;
my $d ;
p4call(\$d,"changes -m 1") ;
$d =~ /Change (\d+)/ and do { $LASTCHANGE=$1 ;} ;
# Get server version
#
$SERVER_VERSION_YEAR = 0 ;
$SERVER_VERSION_NO = 0 ;
$SERVER_VERSION_CHANGE = 0 ;
my @tmp ;
p4call(\@tmp,"info") ;
foreach $d (@tmp) {
$d =~ /^Server version: (.+?)\/(.+?)\/(\d+)\.(\d+)([^\/]*)\/(\d+)/ and
do { $SERVER_VERSION_YEAR = $3 ;
$SERVER_VERSION_NO = $4 ;
$SERVER_VERSION_QUAL = $5 ;
$SERVER_VERSION_CHANGE = $6 ; }
} ;
if($SERVER_VERSION_YEAR == 0) {
$NO_CONTACT_ERROR = "NO CONTACT WITH SERVER \"$PORT\"" ;
}
else {
if(($SERVER_VERSION_YEAR < $REQUIRED_SERVER_VERSION_YEAR) or
(($SERVER_VERSION_YEAR == $REQUIRED_SERVER_VERSION_YEAR) and
($SERVER_VERSION_NO < $REQUIRED_SERVER_VERSION_NUMBER))) {
$NO_CONTACT_ERROR =
"P4DB REQUIRES VERSION $REQUIRED_SERVER_VERSION_YEAR.$REQUIRED_SERVER_VERSION_NUMBER OR NEWER " .
"(Current server $SERVER_VERSION_YEAR.$SERVER_VERSION_NO)" ;
} ;
} ;
# Handle shortcuts
#
%SHORTCUTS=$CGI->cookie(-name=>"P4DB_F_$P4_HOSTNAME") ; # read shortcuts cookie
my $addShortCut=$CGI->param(-name=>"ADDSHORTCUT") ; # Add shortcut if specified
if(defined $addShortCut) {
my ($target,$name) = split(":::",$addShortCut) ;
my $urlpath=&P4CGI::cgi()->url() ;
$urlpath =~ s|[^/]+$|| ;
$target =~ s/^$urlpath// ;
$SHORTCUTS{$name} = $target ;
$CGI->delete("ADDSHORTCUT") ;
} ;
my $rmShortCut=$CGI->param(-name=>"RMSHORTCUT") ; # Delete shortcut if specified
if(defined $rmShortCut) {
my ($target,$name) = split(":::",$rmShortCut) ;
if($SHORTCUTS{$name} eq $target) {
delete $SHORTCUTS{$name} ;
}
$CGI->delete("RMSHORTCUT") ;
};
my $clearShortCuts=$CGI->param(-name=>"CLRSHORTCUTS") ; # Clear all shortcuts if specified
if(defined $clearShortCuts) {
my %empt ;
%SHORTCUTS = %empt ;
$CGI->delete("CLRSHORTCUTS") ;
};
print STDERR "Exit init()\n"; ## DEBUG
} ;
#################################################################
### Documentation start
=head1 About
P4CGI - Support for CGIs that interface p4. Written specifically for P4DB
=cut
;
################################################################
## Short access functions used to get preferences and such ##
## in CGIs. ##
################################################################
=head1 General functions
General functions.
=cut
;
sub CURRENT_CHANGE_LEVEL() { return $LASTCHANGE ? $LASTCHANGE : -1 ; } ;
sub USER_P4PORT() { return $PORT ; } ;
sub HELPFILE_PATH() { return $HELPFILE_PATH ; } ;
sub REDIRECT_ERROR_TO_NULL_DEVICE() { return $REDIRECT_ERROR_TO_NULL_DEVICE ; } ;
sub REDIRECT_ERROR_TO_STDOUT() { return $REDIRECT_ERROR_TO_STDOUT ; } ;
sub VIEW_WITH_COLORS() { return $PREF{"VC"} ; } ;
sub HEADER_STYLE() { return $PREF{"PH"} ; } ; # 1=pulldown, 2=text links
sub SHOW_FULL_DESC() { return $PREF{"FD"} ; } ;
sub HIDE_DELETED() { return $PREF{"HD"} ; } ;
sub CURR_DEPOT_NO() { return $PREF{"DP"} ; } ;
sub DEPOT_NAME($) { my $s = ${$PREF_LIST{"DP"}}[$_[0]+3] ;
$s =~ s/^.*;\s+// ;
return $s ;} ;
sub NO_OF_DEPOTS() { return scalar(@{$PREF_LIST{"DP"}}) - 3 ; } ;
sub CHANGES_IN_SEPARATE_WIN() { return $PREF{"NW"} ; } ;
sub MAX_CHANGES() { return $MAX_CHANGES ; } ;
sub ERRLOG { push @ERRLOG,@_ ; };
sub ERROR { &ERRLOG(map { "ERROR: $_" } @_) ; $ERRORS++ ; };
sub EXTRAHEADER(% ) { %EXTRAHEADER = @_ ; } ;
sub ONLOADSCRIPT($ ) { $ONLOADSCRIPT= shift @_ ; } ;
sub SKIPHEADERBUTTONS() { $printHeaderButtons = undef ; } ;
sub SET_HELP_TARGET($ ) { $helpTarget = shift @_ ; } ;
sub IGNORE_CASE() { return $IGNORE_CASE ; } ;
sub UNUSEDCLIWL() { return $UNUSEDCLIWL ; } ;
sub UNUSEDUSRWL() { return $UNUSEDUSRWL ; } ;
sub VERSION() { return $VERSION ; } ;
sub CHANGELEVEL() { return $CHANGELEVEL ; } ;
sub SERVER_VERSION() { return ($SERVER_VERSION_YEAR,
$SERVER_VERSION_NO,
$SERVER_VERSION_QUAL) ; } ;
sub REQUIRED_SERVER_VERSION() { return "$REQUIRED_SERVER_VERSION_YEAR.$REQUIRED_SERVER_VERSION_NUMBER" ; } ;
sub SHORTCUTS() { return %SHORTCUTS ; } ;
sub PREF() { return %PREF ; } ;
sub PREF_LIST() { return %PREF_LIST ; } ;
sub STARTPAGE_MARKER { return "__ Default start page __" ; } ;
###################################################################
### cgi
###
=head2 cgi
C<&P4CGI::cgi()>
Return CGI reference
Example:
my $file = P4CGI::cgi()->param("file") ;
print "File parameter value: $file\n" ;
=cut
;
sub cgi() {
confess "CGI not initialized" unless defined $CGI ;
return $CGI ;
}
###################################################################
### Fix some special characters to html entities
###
=head2 htmlEncode
C<&P4CGI::htmlEncode(>BC<)>
Convert all '>' to "C<>>", '<' to "C<<>" and '&' to "C<&>".
=over 4
=item str
String to convert
=back
Example:
my $cvstr = &P4CGI::htmlEncode("String containing <,> and &") ;
=cut
;
sub htmlEncode($ )
{
my $d = &rmTabs(shift @_) ;
$d =~ s/&/&/g ; # & -> &
$d =~ s/\"/"/g;# " -> "
$d =~ s/</g ; # < -> <
$d =~ s/>/>/g ; # > -> >
return $d ;
}
###################################################################
### Replace tabs with spaces
###
=head2 rmTabs
C<&P4CGI::rmTabs(>BC<)>
Returns B with all tabs converted to spaces
=over 4
=item I
String to convert
=back
=cut
;
sub rmTabs($ )
{
# This algorithm is kind of, well, the first thing I came up
# with. Should be replaced with a smarter (== more efficient)
# eventually.......
my $l = shift @_ ;
if($l =~ /\t/) { # only do this if there really are tabs in the text
my $tabsz=$TAB_SIZE ;
$tabsz = 8 unless $tabsz ;
my $pos = 0 ;
$l = join('',map
{
if($_ ne "\t") {
$pos++ ;
$_ ;
} else
{
my $p = $pos % $tabsz ;
$pos += $tabsz-$p ;
substr(" ",0,$tabsz-$p) ;
} ;
} split('',$l)) ;
# For those that wonder what is going on:
# 1. Split string to an array (of characters)
# 2. For each entry of array, map a function that returns value
# for entry or, if value is , returns a number of spaces
# depending on position in string
# 3. Make string (scalar) of array returned from map using join().
# (Note that the steps appear in the reverse order in the code)
}
return $l ;
}
###################################################################
### Set magic buttons
###
=head2 magic
C<&P4CGI::magic(>BC<)>
Returns B with some magic "patterns" substituted with links.
Currently the pattern "change I" (and some variants) is replaced with a link to the
change browser.
Example:
my $t = "Integrated change 4711 to this codeline" ;
print &P4CGI::magic($t) ; # inserts a link to change 4711
=cut ;
sub magic($;\@)
{
my $t = shift @_ ;
my %found ;
my $res = "" ;
my $hot = 0 ;
my $max = &P4CGI::CURRENT_CHANGE_LEVEL() ;
while($t =~ s/^([\s\n]*)(no\.|\.|ch\.|[a-zA-Z-0-9]+|[^a-zA-Z-0-9]+)//i) {
$res .= $1 ;
my $tok = $2 ;
if($hot == 0) {
$hot = 3 if $tok =~ /^(ch[\.]?|change|integrate|submit)/i ;
}
else {
$hot-- ;
if($tok =~ /^\d+$/ and !($t =~ /\.\d+/)) {
if($tok > 0 and $tok < $max) {
$hot = 3 ;
$found{$tok} = 1 ;
$tok = ahref(-url => "changeView.cgi",
"CH=$tok",
"HELP=View change $tok",
"$tok") ;
}
}
elsif($tok eq ".") {
$hot = 0 ;
}
}
$res .= $tok ;
} ;
$res .= $t ;
my $ar ;
if($ar = shift @_) {
@$ar = sort { $a <=> $b } keys %found ;
} ;
return $res ;
}
###################################################################
### UrlEncode
###
=head2 urlEncode
C<&P4CGI::urlEncode(>BC<)>
Returns B with characters like space substituted with "%".
Example:
my $t = "/File with spaces" ;
print &P4CGI::urlEncode($t) ; # prints: /File%20with%20spaces
=cut
;
sub urlEncode($)
{
my $t = shift @_ ;
$t =~ s/%(?![\da-fA-F][\da-fA-F])/%25/g ;
$t =~ s/\?/%3f/g ;
$t =~ s/&/%26/g ;
$t =~ s/ /%20/g ;
$t =~ s/;/%3b/g ;
$t =~ s/\+/%2b/g ;
$t =~ s/-/%2d/g ;
$t =~ s/_/%5f/g ;
$t =~ s/~/%7e/g ;
return $t ;
}
###################################################################
### urlDecode
###
=head2 urlDecode
C<&P4CGI::urlDecode(>BC<)>
Reverse the operation of C. See above.
=cut
;
sub urlDecode($)
{
my $t = shift @_ ;
my $r = "" ;
while($t =~ /(.*)\%(..)(.*)/) {
my ($start,$code,$end) = ($1,$2,$3) ;
$r .= $start ;
$t = $end ;
if($code eq "25") {
$r .= "%" ;
}
else {
if($code =~ /[\da-fA-F][\da-fA-F]/) {
$r .= char("0x$code") ;
}
else {
$r .= "%$code" ;
}
}
}
return $r . $t ;
}
################################################################
##
=head1 Functions for p4 access
These fuctions used the "p4" command to access the depot.
=cut
###################################################################
### p4call
###
=head2 p4call
C<&P4CGI::p4call(>BC<,>BC<)>
Request data from p4. Calls p4 with command B and returns data in B.
This function is really three different functions depeding in the type of the
B parameter.
=over 4
=item result
This parameter can be of three different types:
=over 4
=item Filehandle (typeglob)
Data from command can be read from filehandle. NOTE! File must be closed by
caller.
=item Reference to array
Returns result from command into array (newlines stripped)
=item Reference to scalar
Returns result from command into scalar. (lines separated by newline)
=back
Any other type of parameter will abort operation
=item command
Command to send to p4 command line client.
=back
Example:
my $d ;
&P4CGI::p4call(\$d,"changes -m 1") ;
$d =~ /Change (\d+)/ or &bail("No contact with P4 server") ;
$LASTCHANGE=$1 ;
=cut
;
sub p4call {
my ( $par, @command ) = @_;
my $partype = ref $par ;
push @ERRLOG,"p4call(<$partype>,@command)" ;
if(!$partype) {
open( $par, "$P4 @command|" ) || &bail( "$P4 @command failed" );
return ;
} ;
"ARRAY" eq $partype and do {
local *P4 ;
@$par = () ;
open( P4, "$P4 @command|" ) || &bail( "$P4 @command failed" );
while() {
chomp ;
push @$par,$_ ;
} ;
close P4 ;
return ;
} ;
"SCALAR" eq $partype and do {
$$par = "" ;
local *P4 ;
open( P4, "$P4 @command|" ) || &bail( "$P4 @command failed" );
while() {
$$par .= $_ ;
} ;
close P4 ;
return ;
} ;
die("Called with illegal parameter ref: $partype") ;
} ;
###################################################################
### p4readform
###
=head2 p4readform
C<&P4CGI::p4readform(>B,BC<)>
Reads output from a P4 command and assumes the data is a form (e.g. "client -o").
The form is stored in a hash and the function returns an array containing all
field names in the order they appeared. The hash will contain the field names as
key and field values as data.
=over 4
=item command
Command to send to p4 command line client.
=item resulthash
Reference to a hash to receive reults
=back
Example:
my %fields ;
my @fields = &P4CGI::p4readforml("client -o",\%fields) ;
my $f ;
foreach $f (@fields) {
print "field $f: $fields{$f}\n" ;
}
=cut
;
sub p4readform($\% )
{
my $cmd = shift @_ ;
my $href = shift @_ ;
my @result ;
# clear hash
%$href = () ;
local *F ;
p4call(*F,$cmd) ;
my $cline = ;
while($cline) {
chomp $cline ;
$_ = $cline ;
if(/^\#/ or /^\s*$/) { # skip comments and empty line
$cline = ;
next ;
}
if(/^(\S+):\s*(.*)\s*$/) {
my $fld=$1 ;
my $val=$2 ;
push @result,$fld ;
my $ws ;
if($val eq "") {
$val = undef ;
while(defined ($cline = )) {
$_ = $cline ;
chomp ;
last if /^\w/ ;
s/^\s+// ;
if(defined $val) {
$val .= "\n$_" ;
}
else {
$val = "$_" ;
}
}
}
else {
$cline = ;
}
$$href{$fld}=$val ;
}
else {
$cline = ;
}
}
close *F ;
return @result ;
} ;
################################################################
################################################################
################ Page header, footer and common ################
################ routines. ################
################################################################
################################################################
###################################################################
### start_page
###
=head2 start_page
C<&P4CGI::start_page(>B[C<,>B