#!/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 move or rename configuration file
}
use CGI ;
use CGI::Carp ;
use strict;
#### Conficuration file name
my $CONFIG_FILE ;
my @ERRLOG ;
my $errors ;
#### 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 $P4_HOSTNAME; # Contains p4 host name
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 @RESTRICTED_FILES ; # Files where view is restricted
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 $REQUIRED_SERVER_VERSION_YEAR ;
my $STYLE_SHEET ; # Style sheet file name
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 %SHORTCUTS ; # Shortcuts (or favorites)
my %PREF ; # Preferences
my %PREF_LIST ; # List of preferences
my $LEGEND ; # Legend supplied to header
my $NO_CONTACT_ERROR ; # Defined if problem with server
my $SETCOOKIES ; # Set to 1 if cookies should be set
my $RELOADTHIS ; # Set to 1 if this page should be reloaded
# according to the current parameters
#### 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 $ONLOADSCRIPT ;
my $helpTarget ; # target for help text
###
### 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( )
{
$SETCOOKIES = 0 ;
$RELOADTHIS = 0 ;
$REQUIRED_SERVER_VERSION_YEAR = 2001 ;
#
# 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="3.0beta1" ;
#
# Set configuration defaults
#
$HELPFILE_PATH = "./" ;
$UNUSEDCLIWL = 10 ;
$UNUSEDUSRWL = 10 ;
#
# Initiate CGI module
#
$CGI = new CGI ;
$CGI->autoEscape(undef) ;
#
# Setup preference list
#
%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],
"DBG" => ["z:BOOL","Print log information (debug)",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 ;
}
}
$SETCOOKIES = 1 ;
}
### Set up data structure for configuration file read
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 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 ;
$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 " ;
} ;
# Potential security hole, any user can se the log..
# push @ERRLOG,"P4 command set to: \"$P4\"" ;
### fix undelines
$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 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) {
$NO_CONTACT_ERROR = "P4DB REQUIRES VERSION $REQUIRED_SERVER_VERSION_YEAR.1 OR NEWER" ;
} ;
} ;
### Handle shortcuts
%SHORTCUTS=$CGI->cookie(-name=>"P4DB_F_$P4_HOSTNAME") ;
my $addShortCut=$CGI->param(-name=>"ADDSHORTCUT") ;
if(defined $addShortCut) {
my ($target,$name) = split(":::",$addShortCut) ;
$SHORTCUTS{$target} = $name ;
$SETCOOKIES = 1 ;
$CGI->delete("ADDSHORTCUT") ;
$RELOADTHIS = 1 ;
} ;
my $rmShortCut=$CGI->param(-name=>"RMSHORTCUT") ;
if(defined $rmShortCut) {
my ($target,$name) = split(":::",$rmShortCut) ;
foreach (keys %SHORTCUTS) {
if($SHORTCUTS{$_} eq $name) {
delete $SHORTCUTS{$_} ;
last ;
}
}
$SETCOOKIES = 1 ;
$CGI->delete("RMSHORTCUT") ;
$RELOADTHIS = 1 ;
};
my $clearShortCuts=$CGI->param(-name=>"CLRSHORTCUTS") ;
if(defined $clearShortCuts) {
my %empt ;
%SHORTCUTS = %empt ;
$SETCOOKIES = 1 ;
$CGI->delete("CLRSHORTCUTS") ;
$RELOADTHIS = 1 ;
};
} ;
#################################################################
### 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 RESTRICTED() { return @RESTRICTED_FILES ; } ;
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 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 SET_HELP_TARGET($ ) { $helpTarget = shift @_ ; } ;
sub IGNORE_CASE() { return $IGNORE_CASE ; } ;
sub UNUSEDCLIWL() { return $UNUSEDCLIWL ; } ;
sub UNUSEDUSRWL() { return $UNUSEDUSRWL ; } ;
sub VERSION() { return $VERSION ; } ;
sub SERVER_VERSION() { return ($SERVER_VERSION_YEAR,
$SERVER_VERSION_NO,
$SERVER_VERSION_QUAL) ; } ;
sub SHORTCUTS() { return %SHORTCUTS ; } ;
sub PREF() { return %PREF ; } ;
sub PREF_LIST() { return %PREF_LIST ; } ;
sub SET_COOKIES() { $SETCOOKIES = 1 ; } ;
###################################################################
### 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
###
=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/) {
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 ;
}
###################################################################
### UrlEncode
###
=head2 urlDecode
C<&P4CGI::urlDecode(>BC<)>
Reverses 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. ################
################################################################
################################################################
################################################################
### page_header
###
=head2 page_header
C<&P4CGI::page_header()>
Print header of page. Called by start_page().
=cut ;
sub page_header()
{
}
###################################################################
### start_page
###
=head2 start_page
C<&P4CGI::start_page(>B[C<,>B