P4CGI.pm #1

  • //
  • guest/
  • fredric_fredricson/
  • P4DB/
  • rel/
  • 2.1/
  • P4CGI.pm
  • View
  • Commits
  • Open Download .zip Download (41 KB)
#!/usr/bin/perl -w
# -*-perl-*-
##################################################################
# "P4CGI" perl module.
#
#
#
package P4CGI ;
###
# You might need to uncomment and set the lib to point to the  perl libraries
#
#use lib '/usr/local/lib/perl5/site_perl' ;

sub ConfigFileName()
{
    return "./P4DB.conf" ; # Change here to rename configuration file
}

use CGI ;
use CGI::Carp ;
use strict;

#### Conficuration file name
my $CONFIG_FILE ;


my @ERRLOG ;
my $errors ;

###
### Subroutine that prints the error log
###
sub prerrlog() {
    my $res = "" ;
    if(@ERRLOG > 0) {
	map { 
	    if(/^ERROR:/) { $_ = "<font color=red>" . &fixSpecChar($_) . "</font>" ; } 
	    else { $_ = "<font color=blue>" . &fixSpecChar($_) . "</font>" ; } 
	  }  @ERRLOG ;
	$res .= 
	    "<p><font color=blue>Log printout:<br><pre>" .
		join("\n",@ERRLOG) .
		    "</pre></font>\n" ;
    } ;    
    return $res ;
}

#### The following constants are set or updated by the init() routine


my $VERSION ;			# P4DB version
my $P4 ;			# Contains p4 command
my $PORT;			# Contains p4 port
my $CGI;			# Contains CGI object from CGI.pm 
my $lastChange ;  	        # Current change level
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 $USE_JAVA;			# Defined if JAVA should be used
my $SHORTCUT_FILE;		# Name of file containing shortcuts
my @RESTRICTED_FILES ;		# Files where view is restricted

my $SERVER_VERSION_YEAR ;	# Server year
my $SERVER_VERSION_NO ;		# Server count
my $SERVER_VERSION_CHANGE ;	# Server change No.

my $HTML_BGCOLOR;		# Background color
my $HTML_BACKGROUND ;		# Background picture (overrides BGCOLOR if defined)
my $HTML_TEXT_COLOR;		# Text color
my $HTML_LINK_COLOR;		# Link color
my $HTML_ALINK_COLOR;		# Active link color
my $HTML_LINK_TEXT_DEC;		# Link/vlink text decoration. 
my $HTML_TITLE1_BGCOLOR ;
my $HTML_TITLE1_COLOR   ; 
my $HTML_HDRFTR_BGCOLOR ;
my $HTML_HDRFTR_COLOR   ;
my $ICON_PATH;			# Path to icons (usually ../icons or something)
my $HELPFILE_PATH;		# Path to help file (html)
my $REDIRECT_ERROR_TO_NULL_DEVICE; # Part of command thar redirects errors to /dev/null
my $REDIRECT_ERROR_TO_STDOUT;   # Part of command thar redirects errors to stdout
my $DEBUG ;			# When true, prints log

my $UNUSEDCLIWL ;               # Unused client warning level
my $UNUSEDUSRWL ;               # Unused user warning level

my %PREF ;			# Preferences
my %PREF_LIST ;			# List of preferences


#### Other package variables

my $pageStartPrinted ;		# Used to avoid mutliple headers if an error occurres

my @P4DBadmin ;	# Admins for P4DB and Perforce server

my %CONF ;		# Hash containing configuration

my %EXTRAHEADER ;

my $helpTarget ;		# target for help text

sub init( )
{
    #
    # Set config file name
    #
    $CONFIG_FILE = &ConfigFileName ;
			## death handler
    $SIG{'__DIE__'} = sub {	# Thank you Ron Shalhoup for the idea
	my($error) = shift; 
	&P4CGI::bail("Signal caught: $error") ;	
	exit 0; 
    }; 

    #
    # clear error counter
    #
    $errors = 0 ;

    #
    # Set version
    #
#    $VERSION="2.1" ;
    $ VERSION="2.1 (snapshot :VER-HERE:)" ;

    #
    # Set configuration defaults
    #
    $HTML_BGCOLOR      = "#f0f0e0" ;
    $HTML_BACKGROUND   = undef ;
    $HTML_TEXT_COLOR   = "#000000" ;
    $HTML_LINK_COLOR   = "#000099" ;
    $HTML_ALINK_COLOR  = "#000099" ;
    $HTML_LINK_TEXT_DEC= "none" ;
    $SHORTCUT_FILE     = "" ;
    $ICON_PATH         = "./" ;
    $HELPFILE_PATH     = "./" ;
    $HTML_TITLE1_BGCOLOR = "#ccffff" ;
    $HTML_TITLE1_COLOR   = "blue" ;
    $HTML_HDRFTR_BGCOLOR = "#FFFF99" ;
    $HTML_HDRFTR_COLOR   = "#e02020" ;
    $UNUSEDCLIWL       = 10 ;
    $UNUSEDUSRWL       = 10 ;
    
    #
    # Initiate CGI module 
    #
    $CGI = new CGI ;
    $CGI->autoEscape(undef) ;

    #
    # Setup preference list
    #
    %PREF_LIST = 
	(
	 "JV"   => ["x:BOOL",
		    "Enable experimental java depot tree browser.<br>(not always available)",0],
	 "DP"   => ["a:LIST","P4 Depot",0],
	 "SF"   => ["b:LIST","Shortcut file to use",0],
	 "ST"   => ["c:BOOL","Shortcuts on top of main page",0],
	 "IC"   => ["d:BOOL","Ignore case (like NT)",0],
	 "MX"   => ["d:INT","Max changes to show",100],
	 "NW"   => ["d:BOOL","Open changes in new window",0],
	 "TB"   => ["d:INT","Default tab stop",8],
	 "FD"   => ["e:BOOL","Show full descriptions in file log by default",0],
	 "HD"   => ["e:BOOL","Hide deleted files in tree browser by default",0],
	 "TT"   => ["e:BOOL","Show tooltips",1],
	 "AR"   => ["e:BOOL","Show silly arrow next to selected link",1],
	 "UL"   => ["f:BOOL","Underline links",0],
	 "VC"   => ["f:BOOL","View files with colors",1],
	 "CL"   => ["f:LIST","Color schemes",0],
	 "DBG"  => ["z:BOOL","Print log information (for development)",0],
	 ) ;
    
				### Set preferences
    %PREF=$CGI->cookie(-name=>"P4DB_PREFERENCES") ; # First try cookie...
    my $p ;
    foreach $p (keys %PREF_LIST) {
	if(! defined $PREF{$p}) { 
	    $PREF{$p} = $ {$PREF_LIST{$p}}[2];
	    ERRLOG("Set $p!") ;
	}
    }  ;
    foreach $p (keys %PREF) {
	if(exists $PREF_LIST{$p}) {
	    ERRLOG("PREF: $p => $PREF{$p} (${$PREF_LIST{$p}}[1])") ;
	}
	else {
	    delete $PREF{$p} ;
	} ;
    } ;
    if(defined $CGI->param("SET_PREFERENCES")) {
	my $c ;
	foreach $c (keys %PREF) {
	    my $val =  $CGI->param($c) ;
	    if(defined $val) {
		my $type = $ {$PREF_LIST{$c}}[0] ;
		if($type eq "INT") {  $val =~ /^\d+$/ or next ;	} ;
		if($type eq "BOOL") { $val =~ /^[01]$/ or next ; } ;
		$PREF{$c} = $val ;
	    }
	}
    }

				### Set up data structure for configuration file read
    my %configReaderData = ( "P4PATH"         => \$P4,
			     "HTML_ICON_PATH" => \$ICON_PATH,
			     "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"},
			     "SHORTCUT_FILE"  => $PREF_LIST{"SF"},
			     "COLORS"         => $PREF_LIST{"CL"},
			     "\@BGCOLOR"        => $PREF_LIST{"BGC"},
			     "\@BGCOLORT"       => $PREF_LIST{"TC1"},
			     "\@BGCOLORT2"      => $PREF_LIST{"TC2"},
			     "\@TEXTCOLOR"      => $PREF_LIST{"TC"},
			     "\@LINKCOLOR"      => $PREF_LIST{"LC"},
			     "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(<F>) {
	$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 config value 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 ;
	    } ;
# Potetial security hole, any user can se p4 user and password	    
#	    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 ;
    
    $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 " ;
    } ;

# Potential security hole, any user can se the log..
#    push @ERRLOG,"P4 command set to: \"$P4\"" ;
    
				### Set up shortcut file
    $SHORTCUT_FILE = $ {$PREF_LIST{"SF"}}[$PREF{"SF"}+3] ;
    if(!defined $SHORTCUT_FILE) {
	$SHORTCUT_FILE = $ {$PREF_LIST{"SF"}}[3] ;
	$PREF{"SF"} = 0 ;
    }
    $SHORTCUT_FILE =~ s/^(\S+).*/$1/ ;
    
				### Get colors
    my $colors = $ {$PREF_LIST{"CL"}}[$PREF{"CL"}+3] ;
    if(!defined $colors) {
	$colors = $ {$PREF_LIST{"CL"}}[3] ;
	$PREF{"CL"} = 0 ;
    }
    if($colors =~ /^(\S+)\s(\S+)\s(\S+)\s(\S+)\s(\S+)\s(\S+)\s(\S+)\s(\S+)\s(\S+)\s/) {
	$HTML_BGCOLOR    = $1 ;
	$HTML_TEXT_COLOR = $2 ;
	$HTML_LINK_COLOR = $3 ;
	$HTML_ALINK_COLOR = $4 ;	
        $HTML_TITLE1_BGCOLOR = $5 ;
	$HTML_TITLE1_COLOR = $6 ;
	$HTML_HDRFTR_BGCOLOR = $7 ;
	$HTML_HDRFTR_COLOR = $8 ;
    } ;
    
				### fix undelines
    if($PREF{"UL"}) {
	$HTML_LINK_TEXT_DEC = "underline" ;
    } ;
    

    $IGNORE_CASE   = $PREF{"IC"} ? "Yes" : "No" ;
    $TAB_SIZE      = $PREF{"TB"} ;
    $TAB_SIZE = 16 if $TAB_SIZE > 16 ;
    $TAB_SIZE = 0 if $TAB_SIZE <= 0 ;
    $USE_JAVA      = $PREF{"JV"} ? "Yes" : undef ;
    $MAX_CHANGES   = $PREF{"MX"} ;

    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 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_CHANGE = $5 ; }
    } ;
} ;


#################################################################
### Documentation start

=head1 NAME

P4CGI - Support for CGI's that interface p4. Written specifically for P4DB'

=cut ;

sub CURRENT_CHANGE_LEVEL() { return $lastChange ? $lastChange : -1 ; } ;
sub RESTRICTED()           { return @RESTRICTED_FILES ;    } ;
sub USER_P4PORT()          { return $PORT ;                } ;
sub ICON_PATH()            { return $ICON_PATH ;           } ;
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 SHORTCUT_FILE()        { return $SHORTCUT_FILE ;       } ;
sub VIEW_WITH_COLORS()     { return $PREF{"VC"} ;          } ;
sub SHORTCUTS_ON_TOP()     { return $PREF{"ST"} ;          } ;
sub SHOW_FULL_DESC()       { return $PREF{"FD"} ;          } ;
sub HIDE_DELETED()         { return $PREF{"HD"} ;          } ;
sub CHANGES_IN_SEPPARATE_WIN() { return $PREF{"NW"} ;      } ;
sub USE_JAVA()             { return $USE_JAVA ;            } ;
sub MAX_CHANGES()          { return $MAX_CHANGES ;         } ;
sub HDRFTR_BGCOLOR()       { return $HTML_HDRFTR_BGCOLOR ; } ;
sub BGCOLOR()              { return $HTML_BGCOLOR ;        } ;
sub ERRLOG                 { push @ERRLOG,@_ ; }; 
sub ERROR                  { &ERRLOG(map { "ERROR: $_" } @_) ; $errors++ ; }; 
sub EXTRAHEADER(% )        { %EXTRAHEADER = @_ ; } ;
sub SET_HELP_TARGET($ )    { $helpTarget = shift @_ ; } ;
sub IGNORE_CASE()          { return $IGNORE_CASE ; } ;
sub UNUSEDCLIWL()          { return $UNUSEDCLIWL ; } ; 
sub UNUSEDUSRWL()          { return $UNUSEDUSRWL ; } ;
sub SERVER_VERSION()       { return ($SERVER_VERSION_YEAR,$SERVER_VERSION_NO) ; } ;

sub PREF()                 { return %PREF ; } ;
sub PREF_LIST()            { return %PREF_LIST ; } ;


=head1 SUBROUTINES

=cut ;

###################################################################
###  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 ;
}


###################################################################
###  p4call
###

=head2 p4call

C<&P4CGI::p4call(>B<result>C<,>B<command>C<)>

Request data from p4. Calls p4 with command B<command> and returns data in B<result>.

This function is really three different functions depeding in the type of the
B<result> 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(<P4>) {
	    chomp ;
	    push @$par,$_ ;
	} ;
	close P4 ;
	return ;
    } ;
    "SCALAR" eq $partype and do {
	$$par = "" ;
	local *P4 ;
	open( P4, "$P4 @command|" ) || &bail( "$P4 @command failed" );
	while(<P4>) {
	    $$par .= $_ ;
	} ;
	close P4 ;	
	return ;
    } ;
    die("Called with illegal parameter ref: $partype") ;
} ;

###################################################################
###  p4readform
###

=head2 p4readform

C<&P4CGI::p4readform(>B<command>,B<resulthash>C<)>

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 = <F>;
    while($cline) {
	chomp $cline ;
	$_ = $cline ;
	if(/^\#/ or /^\s*$/) {	# skip comments and empty line
	    $cline = <F>;
	    next ;
	}
	if(/^(\S+):\s*(.*)\s*$/) {
	    my $fld=$1 ;
	    my $val=$2 ;
	    push @result,$fld ;
	    my $ws ;
	    if($val eq "") {
		$val = undef ;
		while(defined ($cline = <F>)) {
		    $_ = $cline ;
		    chomp ;
		    last if /^\w/ ;
		    s/^\s+// ;
		    if(defined $val) {
			$val .= "\n$_" ;
		    }
		    else {
			$val = "$_" ;
		    }
		}
	    }
	    else {
		$cline = <F>;
	    }
	    $$href{$fld}=$val ;
	}
	else {
	    $cline = <F>;
	}
    }
    close *F ;
    return @result ;
} ;

###################################################################
###  start_page
###

=head2 start_page

C<&P4CGI::start_page(>B<title>[C<,>B<legend>]C<)>

Start a page. Print http header and first part of HTML.

=over 4

=item title

Title of page. If the text contains " -- " it is removed and all text 
is used for the page title but only the text before " -- " is printed 
in the page header.

=item legend (Optional)

Short help text to be displayed at top of page

=back

Example:

 my $start = P4CGI::start_page("Title of page",
			       &P4CGI::dl_list("This","Goto this",
                                               "That","Goto that")) ;
 print $start ;

=cut ;

sub start_page($;$)
{
    my $title  = shift @_ ;
    my $legend = shift @_ ;
    $legend = "" unless defined $legend ;
    my $n = 0 ;
    
    my $helpURL="${HELPFILE_PATH}/P4DB_Help.html" ;
    if(defined $helpTarget) {
	$helpURL .= "#$helpTarget" ;
    } ;
    
    my $p4port = "" ;
    $p4port = join("\n",("<small><table>",
			 "  <td align=center>",
			 "<small>Click here for<br></small>",
			 ahref(-url=>$helpURL,
			       "<font size=+1><B>Help</B></font>"),
			 "  </td></tr>",
			 "</table></small>\n")) ;

				# Set up cookie and print header
    my $prefcookie = $CGI->cookie(-name=>"P4DB_PREFERENCES",
				  -value=>\%PREF,
				  -path=>"/",
				  -expires=>'+6M');
    my $ret = $CGI->header(-cookie=>$prefcookie,
			   %EXTRAHEADER). "\n"  ;
    
    my $t = "$title" ;		# Take title and removed all HTML tags 
    $t =~ s/<br>/ /ig ;
    $t =~ s/<[^>]*>//g ;
    $t =~ s/ -- / /;
    $title =~ s/ -- .*$/ / ;
    
    my %header ;		# Fill in header fields
    $header{"-title"}  = "P4DB: $t" ;
    $header{"-author"} = "fredric\@mydata.se" ;
    $header{"-bgcolor"} =  $HTML_BGCOLOR ;
    $header{"-background"} = $HTML_BACKGROUND if defined $HTML_BACKGROUND ;
    $header{"-text"} =  $HTML_TEXT_COLOR ;
    $header{"-style"} = join("\n",("A ",
				   "{color:$HTML_LINK_COLOR; text-decoration:$HTML_LINK_TEXT_DEC;}",
				   "A:hover",
				   "{color:$HTML_ALINK_COLOR; text-decoration:underline;}",
				   "BODY, TABLE, TD, TH, TR, P ",
				   "{font-family:      Arial, Helvetica ; }")) ;
    $header{"-head"} = CGI::meta({"-http-equiv" => 'Content-Script-Type',
				  "-content" => 'text/javascript' }) ;
    my $Jscript=<<END;

    if (!document.layers&&!document.all&&!document.getElementById)
	event="test" ;
    function showtip(current,e,text){
	if (document.all||document.getElementById){ 
	    thetitle=text.split('<br>') ;
	    if (thetitle.length>1) {
		thetitles='';
		for (i=0;i<thetitle.length;i++) {
		    thetitles+=thetitle[i];
		}
		current.title=thetitles
		}
	    else
		current.title=text;
	}
		
	else if (document.layers){
	    document.tooltip.document.write('<layer bgColor="white" style="border:1px solid black;font-size:12px;">'+text+'</layer>') ;
	    document.tooltip.document.close();
	    document.tooltip.left=e.pageX+5;
	    document.tooltip.top=e.pageY+5;
	    document.tooltip.visibility="show" ;
	}
    }
    function hidetip(){
	if (document.layers)
	    document.tooltip.visibility="hidden";
    } ;
    
END

    if($PREF{"TT"}) {
	$header{"-script"} = $Jscript ;
    } ;
    $ret .=  $CGI->start_html(%header) ;
    if($PREF{"TT"}) {
	$ret .= '<div id="tooltip" style="position:absolute;visibility:hidden"></div>' ;
    } ;
    
   
    if(defined $lastChange) {
	$ret .= start_table("width=100% bgcolor=\"$HTML_HDRFTR_BGCOLOR\" border=0 cellspacing=0 cellpadding=4") ;
	$ret .= table_row(-valign  => "top",
			  {-align  => "center",
			   -valign => "center",
			   -width  => "20%",
			   -text   => join("\n",("<a name=pagetop></a>",
						 "<font color=$HTML_HDRFTR_COLOR>",
						 "  <B>P4DB </B><i><small>Ver. $VERSION</small></i><br>",
						 "  <small>Current change level:</small> $lastChange",
						 "</font>"))},
			  {-align  => "center",
			   -valign => "center",
			   -width  => "60%",
			   -bgcolor=> "$HTML_TITLE1_BGCOLOR",
			   -text   => "<font size=+1 face=\"Arial,Helvetica\" color=$HTML_TITLE1_COLOR><b>$title</b></font>\n"},
			  {-align  => "center",
			   -valign => "center",
			   -width  => "20%",
			   -text   => "<font color=$HTML_HDRFTR_COLOR>$p4port</font>"}) ;
	my $leg = "" ;
	$leg = "<font face=\"Arial,Helvetica\">" . $legend . "</font>" if defined $legend ;
	my $homelink = "&nbsp;";
	if($CGI->url(-relative=>1) ne "index.cgi") {
	    $homelink = ahref(-url=>"index.cgi","Back To<br>Main Page") ;
	} ;
	$ret .= table_row(-bgcolor => "$HTML_HDRFTR_BGCOLOR",
			  undef,
			  {-align  => "left",
			   -text   => $leg},
			  {-align  => "right",
			   -valign => "bottom",
			   -text   => $homelink}) ;
	$ret .= end_table() ;
	$pageStartPrinted = 1 ;
	return $ret . "\n" ;    
    }
    else {
	return $ret . "<HR><H1 align=center><FONT COLOR=red><blink>No contact with p4 server<br><Font face=Courier>$PORT</font></blink></FONT></H1><HR>\n" ;
    }
} ;
    
###################################################################
###  end_page
###

=head2 end_page

C<&P4CGI::end_page()>

End a page. Print HTML trailer.

Example:

 print P4CGI::end_page() ;

=cut ;

sub end_page()
{
    my $Padmin="" ;
    my $PadminPrompt="" ;
    if(@P4DBadmin > 0) {
	$PadminPrompt = "P4DB&nbsp;administrator" ;
	if(@P4DBadmin > 1) {
	    $PadminPrompt .= "s" ;
	}    
	my $a ;	
	foreach $a (@P4DBadmin) {
	    push @ERRLOG,"P4DB admin: $a" ;
	    my ($email,@name) = split /\s+/,$a ;
	    my $name = join(' ',@name) ;
	    $Padmin .= "<a href=\"mailto:$email\">$name</a><br>" ;
	}
    } ;
    my $e = "" ;
    if($PREF{"DBG"} or $errors > 0) {
	if($errors > 0) {
	    $e = "<P><font color=red size=+2>$errors has occurred. Printing log information</font>" ;
	}
	$e .= prerrlog() ; 
    } ;

    my ($host,$port) = split /:/,$PORT ;
    return join("\n",
		("<table width=100% bgcolor=white cellspacing=0 cellpadding=0>",
		 "  <tr><td>",
		 "    <table bgcolor=\"$HTML_HDRFTR_BGCOLOR\" width=100% border=0 cellspacing=0 cellpadding=0>",
		 "      <tr><td align=left>",
		 "<table>",
		 "  <tr><th align=right valign=top>",
		 "    <font color=$HTML_HDRFTR_COLOR>",
		 "      Host:<br>",
		 "      Port:<br>",
		 "      &nbsp;</font></th>",
		 "  <td >",
		 "    <font color=$HTML_HDRFTR_COLOR valign=top>",
		 "      $host<br>",
		 "      $port<br>",
		 "      P4&nbsp;server&nbsp;$SERVER_VERSION_YEAR.$SERVER_VERSION_NO/$SERVER_VERSION_CHANGE",
		 "    </font>",
		 "</td></tr></table>",
		 " </td><td align=right>",
		 "        <table width=70%  border=0 cellspacing=2 cellpadding=4>",
		 "          <tr><td align=right valign=top >",
		 "            <small><font face=\"Arial,Helvetica\">$PadminPrompt:</font></small>",
		 "          </td>",
		 "          <td valign=top>",
		 "            <small><font face=\"Arial,Helvetica\">", $Padmin, "</font></small>",
		 "          </td>",
		 "          <td valign=center align=right>",
		 "            <a href=#pagetop>Top Of<br>Page</a>",
		 "          </td></tr>",		 
		 "        </table>",
		 "      </td></tr>",
		 "    </table>",
		 "  </td></tr>",
		 "</table>",
		 $e,
		 $CGI->end_html())) ;
} ;

###################################################################
### bail
###

=head2 bail

C<&P4CGI::bail(>B<message>C<)>

Report an error. This routine will emit HTML code for an error message, print the error log  and exit.

This rouine is intended to report internal errors in the code (much like assert(3) in c). 

=over 4

=item message
Message that will be displayed to user

=back

Example:

 unless(defined $must_be_defined) { 
     &P4CGI::bail("was not defined") ; 
 } ;

=cut ;

my $bailed ;

sub bail {
    unless(defined $bailed) {
	$bailed = 1 ;
	my $message = shift @_ ;
	my $text = shift @_ ;    
	unless(defined $pageStartPrinted) {
	    print 
		"",
		$CGI->header(),
		$CGI->start_html(-title   => "Error in script",
				 -bgcolor => "white");
	    $pageStartPrinted = 1 ;
	} ;    
	$message = &fixSpecChar($message) ;
	print 
	    "<br><hr color=red><p align=center><font color=red size=+2>An error has occurred<br>Sorry!</font><p><font color=red>Message:<BR><pre>$message</pre><br>" ;
	if(defined $text) { 
	    $text = &fixSpecChar($text) ;
	    print "<pre>$text</pre><br>\n" ; 
	} ;
	print
	    "<p>Parameters to script:<br>",
	    $CGI->dump() ;
	print "</font>\n",prerrlog(), end_page() ;	
	die($message) ;
    }
} ;

###################################################################
### signalError
###

=head2 signalError

C<&P4CGI::signalError(>B<message>C<)>

Report an operator error in a reasonable fashion.
SignalError can be called before or after start_page() but if it is called 
before start_page() a "default" page header will appear. It is recommended 
to call signalError() after start_page() to make it more obvious to the
operator what the problem was.

=over 4

=item message
Message that will be displayed to user

=back

Example:

 unless(defined $must_be_defined) { 
     &P4CGI::signalError("was not defined") ; 
 } ;

=cut ;

sub signalError {
    my $message = shift @_ ;
    my $text = shift @_ ;    
    unless(defined $pageStartPrinted) {
	print "",start_page("Error","") ;
	$pageStartPrinted = 1 ;
    } ;    
    $message = &fixSpecChar($message) ;
    print 
	"<p align=center><font color=red size=+2>$message</font><br><br>" ;
    if(defined $text) { 
	$text = &fixSpecChar($text) ;
	print "<pre>$text</pre><br>\n" ; 
    } ;
    print "", end_page() ;	
    exit 0 ;
} ;

###################################################################
### help_link
###  
sub help_link($ ) {
    my $helpURL="$HELPFILE_PATH/P4DB_Help.html#" . shift @_ ; ;
    return ahref(-url=>$helpURL,
		 "<font size=+2 style=fixed><B>?</B></font>") ;
}


###################################################################
### start_table
###  

=head2 start_table

C<&P4CGI::start_table(>B<table_attribute_text>C<)>

Start a table with optional table attributes

=over 4

=item table_attribute_text

This text will be inserted as attributes to table tag

=back

Example:

    print P4CGI::start_table("align=center border") ;

=cut ;
sub start_table
{
    my $attribs = shift @_ ;
    my $ret = "<table " ;
    if($attribs) {
	$ret .=  " $attribs" ;
    }
    return $ret . ">\n";
}

###################################################################
### end_table
###  

=head2 end_table

C<&P4CGI::end_table()>

Return end of table string. (trivial function included mostly for symmetry)

=cut ;

sub end_table() 
{
    return "</table>\n" ;
}

###################################################################
### table_row
###

=head2 table_row

C<&P4CGI::table_row(>B<options>C<,>B<listOfValues>C<)>

Insert a row in table.

=over 4

=item options

A list of key/value pairs (a hash will do just fine) containing options for 
the row. 

The key must start with a "-".

Most key/value pairs are treated as attributes to the <TR>-tag.
The following keys are recognized as special:

=over 4

=item C<-type>

Type of cells. Default is <TD>-type. 

=item C<->I<anykey>

I<anykey> will be assumed to be a row option and will be inserted in the TR-tag. 
The value for the option is the key value, unless value is empty or undefined, in which
case the option anykey is assumed to have no value.

=back

=item listOfValues

Row data. Remaining values are assumed to be data for each cell. 
The data is typically the text in the cell but can also be:

=over 4

=item undef

An undefined value indicates that the next cell spans more than one column. 

=item Reference to a hash

The hash contains two keys: "-text" for cell text and "-type" for cell type.
All other key/value pairs are treated as attributes to the <TD> or <TH> tag.

=back

=back

Example:

 print P4CGI::start_table("align=center") ;
                                   ### print header row
 print P4CGI::table_row(-type   => "th",
			-valign => "top",
			-align  => "left",
                        "Heading 1","Heading 2",undef,"Heading 3") ;
                                   ### print data
 my %h = (-text    => "text in hash", 
	  -bgcolor => "blue") ;
 print P4CGI::table_row(-valign  => "top",
			-bgcolor => "white",
                        "Cell 1",
			{-text    => "Cell 2",
			 -bgcolor => "red"},
			\%h,
			"Cell 3-2") ;
 print P4CGI::end_table() ;

=cut ;

sub table_row 
{   
    confess ("P4CGI::table_row() Parameters required!") if @_ == 0 ;
    my @ret ;
    my $n = 0 ;
    my $ec = 0 ;
    my $option = shift @_ ;
    my %options ;
    while(defined $option and ($option =~ s/^-//)) {	
	confess ("P4CGI::table_row() Option value required!") if @_ == 0 ;
	$options{lc($option)} = shift @_ ;
	$option = shift @_ ;
    }
    unshift @_,$option ;
    
    my $type      = "td" ;
    $type = $options{"type"} if defined $options{"type"} ;
    delete $options{"type"} ;

    push @ret,"<tr" ;
    my $attr ;
    foreach $attr (keys %options) {
	push @ret," $attr" ;
	if($options{$attr}) {
	    push @ret,"=$options{$attr}" ;
	}
    }
    push @ret,">\n" ;

    my $colspan = 0 ;
    my $cell ;
    foreach $cell (@_) {
	$colspan++ ;
	if(defined $cell) {
	    my $COLSPAN="colspan=$colspan" ;
	    $colspan=0 ;
	    if(ref $cell) {
		my $reftyp = ref $cell ;
		"HASH" eq $reftyp and do { my $txt = $$cell{"-text"} ;
					   confess "P4CGI::table_row() Missing text argument" 
					       unless defined $txt ;
					   delete $$cell{"-text"} ;
					   my $tp = $type ;
					   $tp = $$cell{"-type"} if defined 
					       $$cell{"-type"} ;
					   delete $$cell{"-type"} ;
					   push @ret,"<$tp $COLSPAN" ;
					   my $attr ;
					   foreach $attr (keys %$cell) {
					       ($a = $attr) =~ s/^-// ;
					       push @ret," $a=$$cell{$attr}" ;
					   }
					   push @ret,">$txt</$tp>\n" ;
					   next ; } ;
		confess "Illegal cell data type \"$reftyp\"" ;
	    } 
	    else {
		push @ret,"<$type $COLSPAN>$cell</$type>\n" ;
	    }
	}
    }
    push @ret,"</tr>\n" ;
    return join("",@ret) ;
}

###################################################################
### table_header
###

=head2 table_header

C<&P4CGI::table_header(>B<list of label/hint>C<)>

Create a table header row with a a description and an optional hint for each column. 

=over 4

=item list of label/hint

A list of column labels optionally followed by a '/' and a hint.

=back

Example:

 print P4CGI::start_table("align=center") ;
                                   ### print header row
 print P4CGI::table_header("File/click for story","Revision/click to view") ;
                                   ### print data
 my %h = (-text    => "text in hash", 
	  -bgcolor => "blue") ;
 print P4CGI::table_row(-valign  => "top",
			-bgcolor => "white",
                        "Cell 1",
			{-text    => "Cell 2",
			 -bgcolor => "red"},
			\%h,
			"Cell 3-2") ;
 print P4CGI::end_table() ;

=cut ;

sub table_header
{   
    my @cols ;
    my @tmp = @_ ;
    my $tmp ;
    my $n ;
    while(@tmp > 0) {	
	$tmp = shift @tmp ;
	if(defined $tmp) {
	    my $label = $tmp ;
	    my $hint = "&nbsp;" ;
	    if($label =~ s|/(.*)$||) {
		$hint = "($1)" ;
	    } ;	
	    push @cols,"<B>$label</B><BR><small>$hint</small>" ;
	}
	else {
	    push @cols,$tmp ;
	}
    }
    return table_row(-valign  => "top",@cols) ;
} ;

###################################################################
### Make a list
###

=head2 ul_list

C<&P4CGI::ul_list(>B<list>C<)>

Return a bulleted list.

=over 4

=item I<list>

Lits of data to print as bulleted list

=back

Example:

 print P4CGI::ul_list("This","is","a","bulleted","list") ;

=cut ;

sub ul_list(@ ) 
{
    my @ret ;
    if($_[0] eq "-title") {
	shift @_ ;
	push @ret, shift @_ ;
    }
    push @ret,"<ul>\n" ;
    my $a ;
    foreach $a (@_) {
	push @ret,"<li>$a\n" ;
    }
    push @ret,"</ul>\n" ;
    return join("",@ret) ;
}

###################################################################
### Make a dl list
###

=head2 dl_list

C<&P4CGI::dl_list(>B<list_of_pairs>C<)>

Returns a definition list.

=over 4

=item list_of_pairs

List of data pairs to print as a definition list. 
A hash will do just fine, only you have no control over the order in the list. 

=back

Example:

 print P4CGI::dl_list("This","Description of this",
                      "That","Description of that") ;

=cut ;

sub dl_list
{
    my @ret ;
    if($_[0] eq "-title") {
	shift @_ ;
	push @ret,shift @_ ;  
    }
    if($_[0] eq "-compact") {
	push @ret,"<dl compact>\n" ;
	shift @_ ;
    } 
    else {
	push @ret,"<dl>\n" ;
    }
    while(@_ > 1) {
	push @ret,"<dt>",shift @_,"<dd>",shift @_,"\n" ;
    }
    push @ret,"</dl>\n" ;
    return join("",@ret) ;
}

###################################################################
### Fix some special characters
###

=head2 fixSpecChar

C<&P4CGI::fixSpecChar(>B<str>C<)>

Convert all '>' to "C<&gt;>", '<' to "C<&lt;>" and '&' to "C<&amp;>".

=over 4

=item str

String to convert

=back

Example:

    my $cvstr = &P4CGI::fixSpecChar("String containing <,> and &") ;

=cut ;

sub fixSpecChar($ )
{    
    my $d = &rmTabs(shift @_) ;
    $d =~ s/&/&amp;/g ; # & -> &amp;
    $d =~ s/\"/&quot;/g;# " -> &quot;
    $d =~ s/</&lt;/g  ; # < -> &lt;
    $d =~ s/>/&gt;/g  ; # > -> &gt;
    return $d ;
}

###################################################################
### Replace tabs with spaces
###

=head2 rmTabs

C<&P4CGI::rmTabs(>B<str>C<)>

Returns B<str> with all tabs converted to spaces

=over 4

=item I<str>

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/) {
	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 <TAB>, returns a number of spaces
	#     depending on position in string
	# 3.  Make string (scalar) of array returned from map using join().
    }
    return $l ;
}

###################################################################
### Create a href tag
###

=head2 ahref

C<&P4CGI::ahref(>B<options>C<,>B<parameters>C<,>B<text>C<)>

Returns a <A HREF...>...</A> tag pair.

=over 4

=item I<options>

Optional list of option-value pairs. Valid options are:

=over 4

=item C<-url>

Url for link. Default is current.

=item C<-anchor>

Anchor in url. Default is none.

=back

Any non-valid option marks the end of the options

=item I<parameters>

Optional list of parameters for link.

=item I<text>

The last parameter is used as text for link. 

If the next to the last parameter has the format: C<"HELP=Help text"> the
help text is displayed as a tooltip (unless the tooltip is turned of in user 
preferences).

=back

Example:

    print &P4CGI::ahref("Back to myself") ; # link to this. No parameters.

    print &P4CGI::ahref("-url","www.perforce.com",
			"To perforce") ; # link to perforce

    print &P4CGI::ahref("-anchor","THERE",
			"Go there") ; # link to anchor THERE

    print &P4CGI::ahref("-url","changeList.cgi",
			"FSPC=//.../doc/...",
			"HELP=Click here",               # a tooltop help text
			"Changes for all documentation") ; # url with parameter

=cut ;

sub ahref
{    
    my $args=@_ ;
    
    my @tmp = @_ ;
    my $url = $ENV{SCRIPT_NAME} ;
    my $anchor = "" ;
    my $pars   = "" ;
    my $params = "" ;
    while($tmp[0] =~ /^-/) {
	$tmp[0] =~ /^-url$/i and do {
	    shift @tmp ;
	    $url = shift(@tmp) ;
	    next ;
	} ;
	$tmp[0] =~ /^-anchor$/i and do {
	    shift @tmp ;
	    $anchor = "#" . shift @tmp ;
	    next ;
	} ;
	$tmp[0] =~ /^-(.*)/ and do {
	    my $p = $1 ;
	    shift @tmp ;
	    my $v = shift @tmp ;
	    $params .= " $p=$v" ;
	    next ;
	} ;
	last ;
    }
    my $tooltips="" ;
    while(@tmp > 1) {
	if($tmp[0] =~ /HELP=(.*)/) {
	    if($PREF{"TT"}) {
		$tooltips=
		    " onMouseOver=\"showtip(this,event,'$1') ; return true ;\" " .
			"onMouseOut=\"hidetip() ; return true ;\" " ;
	    } ;
	    shift @tmp ;
	    next ;
	}
	if(length($pars) > 0) {
	    $pars .= "&" ;
	}
	else {
	    $pars = "?" ;
	} ;
	$pars .= fixspaces(shift @tmp) ;
    } ;
    my $txt = shift @tmp ;
    $pars =~ s/ /\+/g ;
    return "<a href=\"${url}${pars}${anchor}\"$params$tooltips>$txt</a>" ;
}

###################################################################
### Create link with "arrow"
###

=head2 ahrefWithArrow

C<&P4CGI::ahrefWithArrow(>B<parametsrs>C<)>

Returns a <a href...>text</a> with an arrow that "lights up" when the mouse enters the element.

=over 4

=item I<parameters>

Same as C<&P4CGI::ahref(>B<parameters>C<)>.

=back

Example:

    print &P4CGI::ahrefWithArrow("Back to myself") ; # link to this. No parameters.

    print &P4CGI::ahrefWithArrow("-url","www.perforce.com",
				 "HELP=Click now", # a tooltop help text
				 "To perforce") ; # link to perforce

    print &P4CGI::ahrefWithArrow("-anchor","THERE",
				 "Go there") ; # link to anchor THERE
=cut ;

my $imgcnt=1 ;
sub ahrefWithArrow
{
    my $text = pop @_ ;    
    my $showtip="" ;
    my $hidetip="" ;
    if($_[@_-1] =~ /^HELP=/) {
	$showtip = pop @_ ;
	$showtip =~ s/^HELP=// ;
	$showtip = "; showtip(this,event,'$showtip') " ;
	$hidetip = "; hidetip()" ;
    }
    $text =~ s/ /&nbsp;/g ;
    my $image="img${imgcnt}" ;
    $imgcnt++ ;
    my $setarr = "" ;
    my $clrarr = "" ;
    my $img= "" ;
    if($PREF{"AR"}) {
	$setarr = "document['$image'].src='icons/rarr.png'" ;
	$clrarr = "document['$image'].src='icons/noarr.png'" ;
	$img = "<img src=\"icons/noarr.png\" name=\"$image\" border=\"0\">&nbsp;" ;
    } ;
    return ahref(-onMouseOver => "\"$setarr $showtip ; return true\"",
		 -onMouseOut  => "\"$clrarr $hidetip ; return true\"",
		 @_,
		 "$img$text") ;
} ;

###################################################################
### Same as ahrefWidthArrow above but with arrow to the right
###

=head2 ahrefWithArrowR

C<&P4CGI::ahrefWithArrowR(>B<parametsrs>C<)>

Returns a <a href...>text</a> with an arrow that "lights up" when the mouse enters the element.

=over 4

=item I<parameters>

Same as C<&P4CGI::ahref(>B<parametsrs>C<)>.

=back

Example:

    print &P4CGI::ahrefWithArrowR("Back to myself") ; # link to this. No parameters.

    print &P4CGI::ahrefWithArrowR("-url","www.perforce.com",
				 "HELP=Click now", # a tooltop help text
				  "To perforce") ; # link to perforce

    print &P4CGI::ahrefWithArrowR("-anchor","THERE",
				  "Go there") ; # link to anchor THERE
=cut ;

sub ahrefWithArrowR
{
    my $text = pop @_ ;    
    $text =~ s/ /&nbsp;/g ;
    my $showtip="" ;
    my $hidetip="" ;
    if($_[@_-1] =~ /^HELP=/) {
	$showtip = pop @_ ;
	$showtip =~ s/^HELP=// ;
	$showtip = "; showtip(this,event,'$showtip') " ;
	$hidetip = "; hidetip()" ;
    }
    my $image="img${imgcnt}" ;
    $imgcnt++ ;
    my $setarr = "" ;
    my $clrarr = "" ;
    my $img= "" ;
    if($PREF{"AR"}) {
	$setarr = "document['$image'].src='icons/larr.png'" ;
	$clrarr = "document['$image'].src='icons/noarr.png'" ;
	$img = "&nbsp;<img src=\"icons/noarr.png\" name=\"$image\" border=\"0\">" ;
    } ;
    return ahref(-onMouseOver => "\"$setarr $showtip ; return true\"",
		 -onMouseOut  => "\"$clrarr $hidetip ; return true\"",
		 @_,
		 "$text$img") ;
} ;


###################################################################
### Insert image
###

=head2 image

C<&P4CGI::image(>B<image>[C<,>B<text>]C<)>

Returns <IMG>-tag

Example:

    &P4CGI::image("picture.gif","Picture Here") ;

=cut ;

sub image
{    
    my $img = shift @_ || bail("P4CGI::image called without parameters!") ;
    my $text = shift @_ ;
    if($text) {
	$text = " alt=\"$text\"" ;
    }
    else {
	$text = "" ;
    }
    return "<IMG src=$ICON_PATH/$img$text border=0>" ;
}

###################################################################
### Set magic buttons
###

=head2 magic

C<&P4CGI::magic(>B<text>C<)>

Returns B<text> with some magic "patterns" substituted with links.

Currently the pattern "change I<number>" (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",
				 "<b>$tok</b>") ;
		}		
	    }
	    elsif($tok eq ".") {
		$hot = 0 ;
	    }	    
	}
	$res .= $tok ;	
    } ;
    $res .= $t ;

    my $ar ;
    if($ar = shift @_) {
	@$ar = sort { $a <=> $b } keys %found ;
    } ;
    return $res ;
}

###################################################################
### Fixspaces
###

=head2 fixspaces

C<&P4CGI::fixspaces(>B<text>C<)>

Returns B<text> with characters like space substituted with "%<ASCII value>".

Example:

    my $t = "/File with spaces" ;

    print &P4CGI::fixspaces($t) ; # prints: /File%20with%20spaces

=cut ;
sub fixspaces($)
{    
    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 ;
}

###################################################################
### BEGIN
###
sub BEGIN ()
{
    init() ;
} ;

1;



# Change User Description Committed
#1 1933 Fredric Fredricson P4DB: Created 2.1 "release branch".
(The quotation marks are
there because this is not really a release. Yet. Perhaps.)
//guest/fredric_fredricson/P4DB/main/P4CGI.pm
#7 1931 Fredric Fredricson P4DB: Small user interface improvement,
#6 1929 Fredric Fredricson P4DB: Minor user interface improvement.
Real minor.
#5 1920 Fredric Fredricson P4DB: Mainly some user interface fixes:
* Added a small arrow that points to selection in list of options
* Added tooltip help
* Added user prefereces to turn the above off (or on)
* Some other user interface fixes
And fixed a bug in jobList.cgi and some minor bugs in label and branch
viewers.
#4 1647 Fredric Fredricson P4DB: Made it possible to select "hide deleted" or "show deleted" as
default in depot tree browser.
#3 1646 Fredric Fredricson P4DB: file log can now show full descriptions.
Added a new "preference" that makes the full descriptions default or
not.
#2 1645 Fredric Fredricson P4DB: Cosmetic change to table headers (valign top by default)
#1 1638 Fredric Fredricson P4DB: Added all (I think) files for P4DB