#!/usr/bin/perl

# History
#
# 1.2  19970910 added forward link reporting and broken link highlighting
#               and chunked up code into subroutines
#
# 1.1.3 added optional reading of compressed log files
#
# 1.1.2 added blanking filter for random URL things
#
# 1.1.1 added optional stripping of '?' parms from target
# and referring URLS
#
# 1.1 added command line parsing, support for multiple log formats
# and inclusion of previous reports and multiple log files. Made hotlinking 
# mandatory, and made the exclusion patterns case-insensitive
#
# 1.0.1 corrected a problem with double counting hits from
# people who use directoryname instead of directoryname/ to access
# default html files (caused by 1.4 counting both the
# original hit and the redirect), combined #anchor hits with
# their base documents and removed redundant ':80' port specifications
# from reported URLS. Thanks goes to richie@ljouwert.et.tudelft.nl
# (http://morra.et.tudelft.nl/~richie/) for these suggested improvements.
#
# Also, the report is now sorted by number of hits as well as by
# target page and the ability to exclude particular pages from
# the report has been added, both as refering pages and as target pages.

$version="1.2";

require "ctime.pl";

print "Content-type: text/html\n\n";

# For decompressing logs, if necessary
$ungzip = "/bin/gzip -cd ";

# Exclusion patterns (uncomment to use)
$EXCLUDEREFSTO='\.gif|\.jpg|\.css|\.png';
$EXCLUDEREFSFROM='^http:\/\/(fishbowl|test).pastiche.org|\-$';

# URL blanking filters for targets
# $BLANKTARGET	='(\/\d+\-\d+\-menu.asis|\/\d+\-\d+.asis|\?newsgroup.*)$';
# $BLANKTARGETREPLACE		= '(multiple)';

# URL blanking filters for referrer
# $BLANKREFER	='(\/\d+\-\d+\-menu.asis|\/\d+\-\d+.asis|\?newsgroup.*)$';
# $BLANKREFERREPLACE	= '(multiple)';

# Strip '?' variable parameters from target URLs
$STRIPTARGETPARMS=0;

# Strip '?' variable parameters from referring URLs
$STRIPREFPARMS=0;

# default path to the referer_log
$RefererLog="/var/log/apache2/fishbowl/access.log";

# Name of server
$HTTPDSERVER='fishbowl.pastiche.org';

# Minimum number of references for a ref to be included in the list
$MinRefs=1;

# Log type (combined, referer)
$LogType="combined";

# Roxen hack - for 400 series status codes, get the referrer from the
# RFC931 field because the Roxen server does stupid stuff
$roxenhack=0;

# Print the Broken Links Report
$PrintBrokenLinks=1;

# Print the Reverse Link Report
$PrintReverseLinks=1;

# Print the Forward Link Report
$PrintForwardLinks=0;

&Initialize;

&IncludeOldFiles($IncludeFile) if ($IncludeFile);

@ARGV = ("$RefererLog") if ($#ARGV == -1);

&ReadLogFiles(@ARGV);

if ($OutputFile) {
	open (OUTPUTFILE,">$OutputFile") || 
		die ("Could not open $OutputFile for writing.\n$!");
	select(OUTPUTFILE);
}

print <<"EOF";
<html>
<head>
<title>Referring URL Statistics for ${HTTPDSERVER}</title>
</head>
<body bgcolor="#ffffff" text="#00000" link="#0000cc" alink="#cc0000"
      vlink="#990099">
<h1 align=center><a name="top">Referring URL Statistics for
${HTTPDSERVER}</a></h1>
<p>
$refscounter references examined this run.<br>
Last updated: 
EOF
	print &ctime(time);

print <<"EOF";
</p><p>

This is a report of what URL browsers reported as the
referring URL that directed them to a particular web page here. 
Inaccuracies are due mainly to some browsers reporting wrong 
information under some conditions.
</p><p>
EOF

if ($MinRefs > 1) {
	print "Only referring URLs with at least $MinRefs reports were included ",
		"in this list.\n<p>\n";
}

print "<center>\n<table>\n<tr align=left>\n<td>\n<ul>\n";
print "<li><a href=\"#broken\">Broken Links</a></li>\n" if ($PrintBrokenLinks);
print "<li><a href=\"#reverse\">Reverse Links</a></li>\n" if ($PrintReverseLinks);
print "<li><a href=\"#forward\">Forward Links</a></li>\n" if ($PrintForwardLinks);
print "</ul>\n</td>\n</tr>\n</table>\n</center>\n";

&DoBrokenLinks if ($PrintBrokenLinks);
&DoForwardLinks if ($PrintForwardLinks);
&DoReverseLinks if ($PrintReverseLinks);

print <<"EOF";
<address>
Statistics generated by 
<a href="http://www.nihongo.org/snowhare/utilities/">RefStats $version</a>
 / 
<a href="http://www.nihongo.org/snowhare/email/">snowhare\&#64;netimages.com</a>
</address>
</body>
</html>
EOF

select(STDOUT);

# All Done. Everything after this is subroutines.

# Print all the broken links
sub DoBrokenLinks {
	print <<"EOF";
<h2><a name="broken">Broken Links:</a></h2>
<dl>
<!-- Begin Broken Links -->
EOF

	$LastWebPage='';
	foreach $key (sort bytargetthenhits keys(%TargetCounter)) {
		next if ($StatusFlag{$key} != 404);
		($target,$referrer)=split(/ /,$key,2);
		if ("$target" ne "$LastWebPage") {
			print "<dt>$target</dt>\n";
		} 
		print "<dd><a href=\"$referrer\">$referrer</a>",
			" ($TargetCounter{$key} reference";
		if ($TargetCounter{$key} > 1) {
			print "s";
		}
		if ($StatusFlag{$key} == 404) {
			print ', <strong><font color="#ff0000">Broken Link</font></strong>';
		}
		print ")</dd>\n";
		$LastWebPage=$target;
	}
	print <<"EOF";
<!-- End Broken Links -->
</dl>
<center><a href="#top">Return To Top</a></center>
<hr>
EOF
}

# Print all the reverse links
sub DoReverseLinks {
	local ($Status,$LastWebPage,$key,$target,$referrer);

	print <<"EOF";
<h2><a name="reverse">Reverse Links:</a></h2>
<dl>
$StartReverse
EOF

	$LastWebPage='';
	foreach $key (sort bytargetthenhits keys(%TargetCounter)) {
		($target,$referrer)=split(/ /,$key,2);
		next if ($TargetCounter{$key} < $MinRefs);
		if ("$target" ne "$LastWebPage") {
			print "<dt>$target</dt>\n";
		} 
		print "<dd><a href=\"$referrer\">$referrer</a>",
			" ($TargetCounter{$key} reference";
		if ($TargetCounter{$key} > 1) {
			print "s";
		}
		$Status=$StatusFlag{$key};
		if ($Status == 404) {
			print ', <strong><font color="#ff0000">Broken Link</font></strong>';
		}
		print ")<!-- status=$Status --></dd>\n";
		$LastWebPage=$target;
	}
	print <<"EOF";
$EndReverse
</dl>
<center><a href="#top">Return To Top</a></center>
<hr>
EOF
}

# Print all the forward links
sub DoForwardLinks {
	local ($Status,$LastWebPage,$key,$referrer,$target);

	print <<"EOF";
<h2><a name="forward">Forward Links</a></h2>
<dl>
$StartForward
EOF

	$LastWebPage='';
	foreach $key (sort bysourcethenhits keys(%SourceCounter)) {
		($referrer,$target)=split(/ /,$key,2);
		next if ($SourceCounter{$key} < $MinRefs);
		if ("$referrer" ne "$LastWebPage") {
			print "<dt><a href=\"$referrer\">$referrer</a></dt>\n";
		} 
		print "<dd>$target ($SourceCounter{$key} reference";
		if ($SourceCounter{$key} > 1) {
			print "s";
		}
		$Status=$StatusFlag{"$target $referrer"};
		if ($Status == 404) {
			print ', <strong><font color="#ff0000">Broken Link</font></strong>';
		}
		print ")<!-- status=$Status --></dd>\n";
		$LastWebPage=$referrer;
	}
	print <<"EOF";
$EndForward
</dl>
<center><a href="#top">Return To Top</a></center>
<hr>
EOF
}

# for sorting the reverse report
sub bytargetthenhits {
	($targeta)=($a=~m#^(.+)\s#o);
	($targetb)=($b=~m#^(.+)\s#o);

	$inequality=($targeta cmp $targetb);
	if ($inequality) {
		$inequality;
	} else {
		$TargetCounter{$b}<=>$TargetCounter{$a};
	}
}

# for sorting the forward report
sub bysourcethenhits {
	($sourcea)=($a=~m#^(.+)\s#o);
	($sourceb)=($b=~m#^(.+)\s#o);

	$inequality=($sourcea cmp $sourceb);
	if ($inequality) {
		$inequality;
	} else {
		$SourceCounter{$b}<=>$SourceCounter{$a};
	}
}

# Merge old reports with this one. Is order of operation dependant
# - you can end up with bogus 'Status' codes if you merge new 
# to old instead of old to new.

sub IncludeOldFiles {
	local (@IncludeFilesList)=@_;

	local ($target,$source,$line,$Section,$GotData,$IncludeFile);


	foreach $IncludeFile (@IncludeFilesList) {
		if (! open (INCLUSION,$IncludeFile)) {
			warn "Could not open $IncludeFile for inclusion.\n$!";
			return;
		}
		$Section=0;
		while ($line = <INCLUSION>) {
			chop $line;

			# The funky if statements are so you
			# can include or omit everything except the
			# broken links report and still get your data
			# The bailout is so the order of the reports
			# can be changed without breaking the code

			if ($line eq $StartForward) {
				$Section=1;
				next;
			} 
			if ($line eq $EndForward) {
				$Section=0;
				last;
			}
			if ($line eq $StartReverse) {
				$Section=2;
				next;
			} 
			if ($line eq $EndReverse) {
				$Section=0;
				last;
			}
			next if (! $Section);

			if ($Section == 2) { # Reverse
				if ($line =~ m#^<dt>(.*)</dt>#o) {
					$target = $1;
					next;
				}
				next if (! $target);
				if ($line=~
m#^<dd><a\s+href=\"([^"]*)\">.*</a>\s*\((\d+)\s*reference.*\).*<!--\s*status=(\d*)\s*--></dd>$#oi){
					$TargetCounter{"$target $1"} += $2;
					$SourceCounter{"$1 $target"} += $2;
					$refscounter                 += $2;
					$StatusFlag{"$target $1"}     = $3;
				}
			}

			if ($Section == 1) { # Forward
				if ($line =~
m#^<dt><a\s+href=\"([^"]*)\">.*</a></dt>#o) {
					$source = $1;
					next;
				}
				next if (! $source);
				if ($line=~
m#^<dd>(\S*)\s+\((\d+)\s*reference.*\).*<!--\s*status=(\d*)\s*--></dd>$#oi){
					$TargetCounter{"$1 $source"} += $2;
					$SourceCounter{"$source $1"} += $2;
					$refscounter                 += $2;
					$StatusFlag{"$1 $source"}     = $3;
				}
			}
		}
		close(INCLUSION);
	}
}

sub Initialize {
	&ReadCommandLine('include:output:name:logtype:exfrom:exto:minrefs:stripref:striptarget');

	# Old file to read (optional)
	$IncludeFile = $opt{'include'} if ($opt{'include'});

	# Output file (optional - goes to STDOUT if not specified)
	$OutputFile = $opt{'output'} if ($opt{'output'});

	# Type of log (legal: combined, referer)
	$LogType = $opt{'logtype'} if ($opt{'logtype'});

	# Exclusion patterns
	$EXCLUDEREFSFROM= $opt{'exfrom'} if ($opt{'exfrom'});

	$EXCLUDEREFSTO= $opt{'exto'} if ($opt{'exto'});

	# '?' parameter stripping options
	$STRIPTARGETPARMS = $opt{'striptarget'} if (defined($opt{'striptarget'}));

	$STRIPREFPARMS = $opt{'strip'} if (defined($opt{'stripref'}));

	# Name of server
	$HTTPDSERVER=$opt{'name'} if ($opt{'name'});

	# Minimum number of references for a reference to be included in the list
	$MinRefs = $opt{'minrefs'} if ($opt{'minrefs'});

	# Markers for the various log sections
	$StartForward="<!-- Start Forward Links -->";
	$EndForward  ="<!-- End Forward Links -->";
	$StartReverse="<!-- Start Reverse Links -->";
	$EndReverse  ="<!-- End Reverse Links -->";

	$refscounter=0;
}

sub ReadCommandLine {

        # parse list has the form 'a:b:c'
        # flags with parse list entries must take values

        local($parselist)=$_[0];
        local(@CommandLine)=@ARGV;
        local(@ParseList,%ParseRules,@GenericList);

        (@ParseList)=split(/:/,$parselist);

        foreach $item (@ParseList) {
                $ParseRules{$item}=1;
        }

        while ($parm=shift(@CommandLine)) {
                if ($parm =~ m#^\-([a-zA-Z]+)$#o) {
                        $parm=$1;
                        $opt{$parm}=1;
                        if ($ParseRules{$parm}) {
                                $value=shift(@CommandLine);
                                if ($value eq "") {
                                        die ("Invalid comand line switch usage, '-$parm' requires value\n");
                                }
                                $opt{$parm}=$value;
                        }
                        next;
                }
                push(@GenericList,$parm);
        }
        @ARGV=@GenericList;
}

sub ReadLogFiles {

	local (@LogFilesList)=@_;

	local ($Domain,$rfc931,$authuser,$TimeDate,$Request,
		$Status,$Bytes,$referrer,$Agent,$Method,$target,
		$Protocal,$LogFile,$OpeFile,$line,$keyform,$key,
		$value);

	foreach $LogFile (@LogFilesList) {
	        $OpeFile=$LogFile;
	        if ($LogFile =~ m#\.gz$#o) {
			$OpeFile="$ungzip $LogFile |";
		}
		if (! open(REFSLOG,$OpeFile) ) {
			warn "Can't open $LogFile. Skipped.\n $!";
			next;
		}
	
		while($line=<REFSLOG>) {
			$refscounter++;
			chop $line;

		if ($LogType eq 'combined') {
			($Domain,$rfc931,$authuser,$TimeDate,$Request,$Status,$Bytes,$referrer,$Agent) = $line =~ 
			/^(\S+) (\S+) (\S+) \[([^\]\[]+)\] \"([^"]*)\" (\S+) (\S+) \"?([^"]*)\"? \"([^"]*)\"/o;
			($Method,$target,$Protocal)=split(/\s/,$Request,3);
		} elsif ($LogType eq 'referer') {
		        ($referrer,$target)=split(/ -> /,$line,2);
		}
			$referrer =~ s/^\-$//o;
			$target =~ s/^\-$//o;
			if (! ($referrer && $target) ) {
				next if (! $roxenhack);
				($Domain,$referrer,$authuser,$TimeDate,$Request,$Status,$Bytes)
				= $line =~ 
				/^(\S+) \"?([^"]*)\"? (\S+) \[([^\]\[]+)\] \"([^"]*)\" (\S+) (\S+)/o;
				($Method,$target,$Protocal)=split(/\s/,$Request,3);
				$referrer =~ s/^\-$//o;
				$target =~ s/^\-$//o;
				next if (! ($referrer && $target));
			}

			$target=~ s/\%7[eE]/~/o;	# Caniconalize %7E and %7e as ~
			$target=~ s#//#/#go;		# Remove any extra slashes
			$target=~ s#^ *$#/#o;		# fix root ref if needed
			$target=~ s/#.+$//o;		# combine #anchor refs with root doc
			$target=~ s/\?.*$//o if $STRIPTARGETPARMS;	# strip '?' parameters
			if ($BLANKTARGET) {
				$target =~ s/$BLANKTARGET/$BLANKTARGETREPLACE/o;
			}

			next if ($EXCLUDEREFSTO && ($target=~m#$EXCLUDEREFSTO#io));

			$referrer=~ s/\%7[eE]/~/o;	# Caniconalize %7E and %7e as ~
			$referrer=~ s#^(http://[^/]+):80/#$1/#o;	# remove unneeded :80 port specification
			$referrer=~ s/#.+$//o;		# combine #anchor refs with root doc
			$referrer=~ s/\?.*$//o if $STRIPREFPARMS;	# strip '?' parameters

			if ($BLANKREFER) {
				$referrer =~ s/$BLANKREFER/$BLANKREFERREPLACE/o;
			}
			next if ($EXCLUDEREFSFROM && ($referrer=~m#$EXCLUDEREFSFROM#oi));
	
	                next if ($referrer=~/^http:\/\/[a-z]+\.google\./);
			next if ($referrer=~/^http:\/\/search.yahoo.com/);
			next if ($referrer=~/^http:\/\/(www.)?bloglines.com/);

			$keyform=$target.' '.$referrer;
			$keyform=~ s#<#\&lt\;#og;	# prevent accidents with '<'
			$keyform=~ s#>#\&gt\;#og;	# prevent accidents with '>'
			$keyform=~ s#\&#\&amp\;#og;	# prevent accidents with '&'
			$keyform=~ s#"#\&quot\;#og;	# prevent accidents with '"'
			$TargetCounter{$keyform}++;
			$StatusFlag{$keyform}=$Status;
			$keyform= $referrer.' '.$target;
			$keyform=~ s#<#\&lt\;#og;	# prevent accidents with '<'
			$keyform=~ s#>#\&gt\;#og;	# prevent accidents with '>'
			$keyform=~ s#\&#\&amp\;#og;	# prevent accidents with '&'
			$keyform=~ s#"#\&quot\;#og;	# prevent accidents with '"'
			$SourceCounter{$keyform}++;
		}
		close(REFSLOG);
	}

	# Remove false hits caused by people using incorrect URLs
	# that are redirected by the server
	
	while(($key,$value) = each %TargetCounter) 