#!/usr/local/bin/perl

require 5.004;	# 本プログラムは(少なくとも)perl 5.004以上を必要とします。

;#+------------------------------------------------------------------------
;# fcount.cgi (ikkan ver 0.030804)
;# ikkan! On the Web ( http://www2u.biglobe.ne.jp/~ikkan/ )

;# efCount[efStatカウンタ部]
;# (C)1998-2000 不可思議絵の具(http://www.skipup.com/~fuka/)

$ver = '2.1.2';  # 本プログラムの版数。禁変更。

;#+------------------------------------------------------------------------
;# 設定項目
;#+------------------------------------------------------------------------

;# [動作モード]
;# SSIとして動作させるなら 1 ， CGIとして動作させるなら 0 。
$USER{'SSIMode'} = 0;

;# [ログファイルを格納したディレクトリの名前]
;# ログ用ディレクトリは fstat/ 以下に作って下さい。
$USER{'DIR_Log'} = 'log';

;# [アクセスログ最大保存数]
;# 1以上、3000未満の数値を入力して下さい。
$USER{'MaxLog'} = 500;

;# [再読み込み防止機構(IPチェック)を利用するか]
;# 1 = する(デフォルト) / 0 = しない(重複カウントを許す)
$USER{'IPCheck'} = 1;

    ;# (IPCheckが１の場合有効)
    ;# [再読み込み防止機構の有効期限] (分単位で指定)
    ;# 指定した時間が経つと、同じIPであってもカウントします。
    ;# 0を指定すると、同じIPの間はずっとカウントしません。
    $USER{'IPExpire'} = 1;

# [カウントアップさせないホスト名・IPアドレス]
# 複数指定できますが、完全一致しなければなりません。
# ここを空欄にしてはいけません。　適当で良いですから埋めて下さい。
@USER_DenyIP = (
	'abc123.ppp.test.ne.jp',
);

# [カウントアップさせないブラウザ]
# 複数指定できます。　前方比較を行います。
# ここを空欄にしてはいけません。　適当で良いですから埋めて下さい。
@USER_DenyAgent = (
	'kusaikusai',
);


;#+------------------------------------------------------------------------
;# CGIとして使うときの設定項目
;#+------------------------------------------------------------------------
;# [カウンタ用画像を格納したディレクトリの名前]
;# カウンタ画像用ディレクトリは fstat/ 以下に作って下さい。
$USER{'Dir_Img'} = 'image';

	;# [カウンタのイメージの名前(デフォルトで使用する物)]
	$USER{'DigitName'} = 'fuksan';


;#+------------------------------------------------------------------------
;# SSI として使うときの設定項目
;#+------------------------------------------------------------------------
;# [efStat一式を格納したディレクトリ]
;# ※呼び出すHTMLから見た相対パスでも良いが、ルートからのフルパスが望ましい。
;# ※最後のスラッシュ(/)は必ず必要。
$USER{'SSI_Pass'} = '/home/sites/home/users/fuka/web/cgi-bin/fstat/';


;#+------------------------------------------------------------------------
;# その他、補助的な項目
;#+------------------------------------------------------------------------

# [gifcat.plのありか] (通常は変更しないで下さい)

$USER{'GifCat'} = './lib/gifcat.pl';

# [環境変数の設定]
# 環境変数 $ENV{'TZ'} を使用できない場合（海外のサーバを使用している等）、
# 下を $UseGMT = 1; として下さい。

$UseGMT = 0;


#+------------------------------------------------------------------------
# (設定ここまで)
#+------------------------------------------------------------------------
# ※ここからは分かる人だけ弄って下さい。
# 　(タブのサイズ・[4]、折返し・[無し]で綺麗に表示されます)
#+------------------------------------------------------------------------
#|&main
#+------------------------------------------------------------------------
&Macro_Setup;				# 各種初期化
&Macro_Access;				# アクセス記録
&Macro_LoadData;			# ログファイルを読み込み
&Macro_Check;				# チェック
if ($OutputOnly) {			# 重複アクセスは記録しない
	&Macro_Output;			# 結果を出力
} else {
	&Macro_Count;			# カウント
	&Macro_SaveData;		# 保存
	&Macro_Output;			# 結果を出力
}
exit;


#+------------------------------------------------------------------------
#|プログラムの流れとしてのサブルーチン
#+------------------------------------------------------------------------
### 各種初期化
sub Macro_Setup {

	# ENV{'TZ'} を使用できる場合のみ実行
	unless ($UseGMT) {
		$ENV{'TZ'} = 'JST-9';	# 環境変数TZを日本時間に設定
	}

	$Digit = 0;
	$OutputOnly = 0;		# 1=出力のみ

	### 現在時刻の取得
	$RUN_TIME   = time;						# 現在時刻(秒形式)

	# GMT を利用する場合。
	if ($UseGMT) {
		$RUN_TIME = $RUN_TIME + 32400 ; # 日本とGMTの差を足す
		@RUN_TIME = gmtime($RUN_TIME);
	}else{
		@RUN_TIME = localtime($RUN_TIME);
	}

	$RUN_TIME_E = &C62_Encode($RUN_TIME);	# エンコード済現在時刻

	### 引数の解釈
	foreach $data (split(/&/, $ENV{'QUERY_STRING'})) {
		($key , $val) = split(/=/,$data);
		$P{$key} = $val;
	}
	$Filename = $P{'LOG'};	# ログファイル名

	## SSIモード
	if ($USER{'SSIMode'}) {
		# ログ格納ディレクトリ
		$USER{'DIR_Log'} = "${USER{'SSI_Pass'}}${USER{'DIR_Log'}}/";

		# 動作モード
		if    ($P{'MODE'} eq '-') { $OutputOnly = 1; }	# "-"ならカウント無し
		elsif ($P{'MODE'} eq 'h') { $OutMode = 'h' ; }		# "h"なら出力無し
	}

	## CGIモード
	else {
		# 動作モード
		if ($P{'MODE'} =~ /^-([atyw])$/) {		# 頭に - があれば出力のみ
			$OutMode = $1;
			$OutputOnly = 1;
		} elsif ($P{'MODE'} =~ /^([atyw])$/) {	# 無ければ普通にチェック
			$OutMode = $1;
		} else {								# 満たさなければ強制的に a に
			$OutMode = 'a';
		}

		if ($P{'DIGIT'} > 20) { &Macro_PutError('e0001'); }	# 桁数
		else                  { $Digit = $P{'DIGIT'}-1; }

		$ENV{'HTTP_REFERER'} = $P{'REF'};	# 参照元

		$screen = $P{'SCR'};				# 画面情報

		$USER{'DigitName'} = $P{'FONT'} if ($P{'FONT'} ne '');	# フォント名

		## ディレクトリ名を修正
		# ログ格納ディレクトリ
		$USER{'DIR_Log'} = "./${USER{'DIR_Log'}}/";
		# フォント格納ディレクトリ
		$USER{'Dir_Img'} = "./${USER{'Dir_Img'}}/${USER{'DigitName'}}/";

		### 時刻表示モードだったらここで終わり
		if ($OutMode eq 'w') {
			$Digit = 0;
			print &Func_PutGIF(sprintf("%02dc%02d", $RUN_TIME[2], $RUN_TIME[1]));
			exit(0);
		}
	}
}


### [アクセス記録]
sub Macro_Access {
	### 訪問者のリモートホスト取得
	$host = $ENV{'REMOTE_ADDR'};

	if ($host eq '') {
		$host = '-';
	} else {
		# proxyチェック
		# HTTP_FORWARDED (DeleGate , Squid)
		$host = $1 if ($ENV{'HTTP_FORWARDED'} =~ / for (.*)/);

		# IP -> HOST
		$host = gethostbyaddr(pack("C4", split(/\./, $host)), 2) || $host;
	}

	### ユーザエージェント取得
	$agent = $ENV{'HTTP_USER_AGENT'};
	$agent =~ s'^Mozilla/'!';
	$agent =~ s"\(compatible; MSIE (.+)\)"(!$1)";
	$agent = '-' if ($agent eq '');	#エージェント名がなければ'-'に置き換える

	### 参照元取得
	$ref = $ENV{'HTTP_REFERER'};

	# CGI モードでのみ URL デコード
	unless ($USER{'SSIMode'}) {
		$ref =~ s/%([0-9A-Fa-f][0-9A-Fa-f])/pack("C",hex($1))/ge;	# URLDecode
	}

	$ref =~ s/\?$//;
	$ref =~ s"(/index\.)html$|\1htm$|\1shtml$|\1php3$"/"i;
	$ref =~ s'^http://'!'i;

	# リンク元無し, '[unknown origin]', 'bookmark'なら'-'に置き換える
	if ( ($ref eq '') || ($ref eq '[unknown origin]') || ($ref eq 'bookmarks') || ($ref eq "',ref,'") ) { $ref = '-'; }

	### 画面モード
	if (($screen eq '') || ($screen !~ /^[0-9]/)) {
		$screen = '-';
	} else {
		($screen_x, $screen_y, $screen_color) = split(/,/, $screen);
		$screen_x		= &C62_Encode($screen_x);
		$screen_y		= &C62_Encode($screen_y);
		$screen_color	= &C62_Encode($screen_color);
		$screen = "$screen_x,$screen_y,$screen_color";
	}
}


### [ログファイルを読み込み]
sub Macro_LoadData {
	unless (open(LOG,"+<${USER{'DIR_Log'}}${Filename}.log")) {
		&Macro_PutError('e0000');
	}
	flock(LOG,2);
	for ($i=0 ; $i<8 ; $i++) { chomp($count[$i] = <LOG>); }		# 各種カウント数
	chomp(@log = <LOG>);										# アクセスログ

	###新規ログならフォーマット
	if ($count[0] eq '') {
		$count[0] = "FC2\t${RUN_TIME_E}";
		$count[1] = "${RUN_TIME_E}\t0";
		$count[2] = "0\t0\t0\t0\t0\t0\t0\t0";
		$count[3] = "0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0";
		$count[4] = "0\t0\t0\t0\t0\t0\t0";
		$count[5] = "0\t0\t0\t0\t0\t0";
		$count[6] = "0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0";
		$count[7] = "0\t0\t0\t0\t0\t0";
	}

	###データを各配列に格納
	($LOG_ID, $LOG_SINCE_E)			= split(/\t/, $count[0]);		#ヘッダ
	$LOG_SINCE						= &C62_Decode($LOG_SINCE_E);
	# ログ形式check(機能しない?)
	&Macro_PutError('e1110') if ($LOG_ID ne 'FC2');

	($LOG_TIME_E, $ALL_E, $LOG_IP)	= split(/\t/, $count[1]);		# 各種情報
	$LOG_TIME						= &C62_Decode($LOG_TIME_E);
	$ALL							= &C62_Decode($ALL_E);

	@DAILY							= &split($count[2]);	# 日別集計
	@HOUR							= &split($count[3]);	# 時間別集計
	@YOUBI							= &split($count[4]);	# 曜日別集計
	@WEEK							= &split($count[5]);	# 週別集計
	@MONTH							= &split($count[6]);	# 月別集計
	@YEAR							= &split($count[7]);	# 月別集計

	# GMT を利用する場合。
	if ($UseGMT) {
		@LOG_TIME = gmtime($LOG_TIME);
	}else{
		@LOG_TIME = localtime($LOG_TIME);
	}


	# 文字列を切り分け、10進数に戻す
	sub split {
		my($str) = @_;
		my(@array);

		@array = split(/\t/, $str);
		foreach (@array) { $_ = &C62_Decode($_); }
		return @array;
	}
}


### カウントすべき訪問者かチェック
sub Macro_Check {
	# 重複カウントを許さない
	if ($USER{'IPCheck'}) {
		# 前回訪問者と同じか？
		if ($host eq $LOG_IP) {

			if ($USER{'IPExpire'} == 0) {
				$OutputOnly = 1;
			} else {
				# IPは有効期限外か？
				if ($RUN_TIME - $LOG_TIME < $USER{'IPExpire'} * 60) {
					$OutputOnly = 1;
				} else {
					$OutputOnly = 0;
				}
			}
		} else {
			$OutputOnly = 0;
		}
	# 重複カウントを許す
	}else {
		$OutputOnly = 0;
	}

	# 弾くべきIPか？
	foreach (@USER_DenyIP) {
		if ($host eq $_) { $OutputOnly = 1; last; }
	}
	# 弾くべきブラウザか？
	foreach (@USER_DenyAgent) {
		if ($agent =~ /^$_/) { $OutputOnly = 1;	last; }
	}
}


### [カウントする]
sub Macro_Count {

	my($kdays, $ksecs, $kweeks, $n);

	# 年(5)と経過日(7)が同じなら同じ日。日数計算しない。
	unless ( ($RUN_TIME[7] == $LOG_TIME[7]) && ($RUN_TIME[5] == $LOG_TIME[5]) ) {
	
		### 何日間経過したか求める

		$kdays = $RUN_TIME - $LOG_TIME;	# 経過秒 = RUN秒 - LOG秒
		$kdays = int( $kdays/60/60/24 );	# 経過日数 = 経過秒/60sec/60min/24h
		
			## 起動(RUN)時刻がログ(LOG)時刻より前だと日数足りない、対策。
			# 時刻 > 秒 に変換
			$RUN_hms = ($RUN_TIME[2] * 3600) + ($RUN_TIME[1] * 60) + $RUN_TIME[0];
			$LOG_hms = ($LOG_TIME[2] * 3600) + ($LOG_TIME[1] * 60) + $LOG_TIME[0];
			
			$ksecs = $RUN_hms - $LOG_hms;	# 時刻の差（秒）
			if ($ksecs < 0) { $kdays++; }	# マイナスなら日数足す。
		

		### 何週間経過したか求める

		$kweeks = $LOG_TIME[6] + $kdays;	# 曜日データ = 曜日(LOG) + 経過日数

		if ($kweeks < 7) { $kweeks = 0; } # 同じ週
		else { $kweeks = int($kweeks/7); } # 違う週

		### 何年間経過したか求める
		$n = $RUN_TIME[5] - $LOG_TIME[5];

	} # //unless 同じ日

	### データ挿入

	# 日データ挿入
	if ($kdays == 0) { $DAILY[0]++; }	# 同じ日
	elsif ($kdays > 0) {				# kdays日経過
		for (1 .. $kdays) { unshift(@DAILY, 0); }
		$DAILY[0] = 1;
	}

	# 週データ挿入
	if ($kweeks == 0) { $WEEK[0]++; }	# 同じ週
	elsif ($kweeks > 0) {				# n週間経過
		for (1 .. $kweeks) { unshift(@WEEK, 0); }
		$WEEK[0] = 1;
	}

	# 年データ挿入
	if (($n == 0) || ($n < 0)) {	# 同じ年(サーバ側時計が古い場合もこの処理)
		$YEAR[0]++;
	}elsif ($n > 0) {	# n年経過
		@MONTH = (0) x 12;	# 月別集計をreset
		for (1 .. $n) { unshift(@YEAR, 0); }
		$YEAR[0] = 1;
	}

	++$ALL;
	++$HOUR [ ${RUN_TIME[2]} ];
	++$YOUBI[ ${RUN_TIME[6]} ];
	++$MONTH[ ${RUN_TIME[4]} ];
}


### [カウント結果を配列に格納]
sub Macro_SaveData {
	$ALL_E = &C62_Encode($ALL);

	$count[0] = "FC2\t$LOG_SINCE_E";			# 各種情報
	$count[1] = "$RUN_TIME_E\t$ALL_E\t$host";	# 各種情報
	$count[2] = &Func_Array2Str(8,  \@DAILY);	# 日別集計
	$count[3] = &Func_Array2Str(24, \@HOUR);	# 時間別集計
	$count[4] = &Func_Array2Str(7,  \@YOUBI);	# 曜日別集計
	$count[5] = &Func_Array2Str(6,  \@WEEK);	# 週別集計
	$count[6] = &Func_Array2Str(12, \@MONTH);	# 月別集計
	$count[7] = &Func_Array2Str(6,  \@YEAR);	# 年別集計
	# 新規ログ行
	$new_log  = "$ALL_E\t$RUN_TIME_E\t$host\t$agent\t$ref\t$screen";

	# 新しいログを付け足し、
	unshift(@log, $new_log);
	# 古いログは消し去る
	splice(@log, $USER{'MaxLog'});

	seek(LOG,0,0);

	foreach (@count) { print LOG "$_\n"; }
	foreach (@log)   { print LOG "$_\n"; }

	truncate(LOG,tell);

	flock(LOG,8);
	close(LOG);

	### 配列の内容を一行の文字列に直す関数
	### $limitより大きなデータは切り捨てる
	sub Func_Array2Str {
		my($limit, $array) = @_;
		splice(@$array, $limit);

		foreach (@$array) { $_ = &C62_Encode($_); }
		return join("\t", @$array);
	}
}


### [カウント数出力]
sub Macro_Output {
	my($s, $m, $h, $d, $mo, $y, $w) = @RUN_TIME;
	my($today, $yesterday) = @DAILY;
	my($all) = $ALL;
	my(@w);

	unless ($OutputOnly) {
		$today      = &C62_Decode($today);
		$yesterday  = &C62_Decode($yesterday);
	}

	### SSIモードなら
	if ($USER{'SSIMode'}) {
		print "Content-type: text/plain\n\n";
		if ($OutMode ne 'h') {
			$agent =~ s'^!'Mozilla/';
			$agent =~ s"\(!(.+)\)"(compatible; MSIE $1)";
			$ref   =~ s'^!'http://';
			++$mo;
			$y += 1900;

			;#+--[ * メッセージ(ユーザ変更可) * ]------------[ ここから ]-+

			# 好きな曜日の表記を選んで下さい。
			@w = ('日', '月','火','水','木','金','土');  $w = $w[$w];
			# @w = ('Sun','Mon','Tue','Wed','Thu','Fri','Sat');  $w = $w[$w];

			;#+-----------------------------------------------------------+
			;#(メッセージ中の変数の意味)
			;#  ${all}      …総ヒット数    ${y} …年    ${h}…時
			;#  ${today}    …本日ヒット数  ${mo}…月    ${m}…分
			;#  ${yesterday}…昨日ヒット数  ${d} …日    ${s}…秒
			;#  ${host}     …訪問者ホスト  ${w} …曜日
			;#  ${agent}    …ブラウザ名
			;#  ${ref}      …参照元        ※ " は \" に置き換えましょう！
			;#+-----------------------------------------------------------+

			@mes = (
				#"${all}\n${today}\n${yesterday}\n${host}\n${agent}\n${ref}\n${y}年${mo}月${d}日(${w})\n${h}時${m}分${s}秒\n",
				"<FONT FACE=\"Arial\">Total:<B>${all}</B> / Today:<B>${today}</B> / Yesterday:<B>${yesterday}</B></FONT>",
			);

			;#+--[ * メッセージ(ユーザ変更可) * ]------------[ ここまで ]-+

			# ランダムに選ぶ
			srand(time + $$);
			$n = int(rand($#mes+1));

			# 表示
			print $mes[$n];
		}
	}

	### CGIモードなら
	else {
		# 総アクセス数
		if    ($OutMode eq 'a') { print &Func_PutGIF($all); }
		# 今日アクセス数
		elsif ($OutMode eq 't') { print &Func_PutGIF($today); }
		# 昨日アクセス数
		elsif ($OutMode eq 'y') { print &Func_PutGIF($yesterday); }
	}
}


### [gifを出力]
sub Func_PutGIF {
	my($Data) = @_;
	my($i, $n, @array);

	require $USER{'GifCat'};
	print "Content-type: image/gif\n";
	print "Expires: 01/01/1970 00:00:00 JST\n\n";	# キャッシュを無効にする
	binmode(STDOUT);

	### 桁数を指定されたら、足りない分を0で補う
	$Data = ('0'x($Digit-(length($Data)-1))).$Data if (length($Data)-1 < $Digit);

	for ($i=0 ; $i < length($Data) ; $i++) {
		$n = substr($Data, $i, 1);
		push(@array, "${USER{'Dir_Img'}}${n}${USER{'DigitName'}}.gif");
	}

	return &gifcat'gifcat(@array);
}


### [エラー出力]
# e0000 = ファイルオープン失敗
# e0001 = 桁数あふれ
# e1110 = ログの形式が不正
# e1111 = 無効なオプション
sub Macro_PutError {
	my($code) = @_;

	if ($USER{'SSIMode'}) {
		print "Content-type: text/plain\n\n";
		if ($code eq 'e0000') {
			print "<P><B>[efCount $ver(SSI)]ログを開くことが出来ませんでした</B></P>\n";
		} elsif ($code eq 'e1110') {
			print "<P><B>[efCount $ver(SSI)]ログの形式が不正です</B></P>\n";
		} elsif ($code eq 'e1111') {
			print "<P><B>[efCount $ver(SSI)]無効なオプションです</B></P>\n";
		} else {
			print "<P><B>[efCount $ver(SSI)]未定義のエラーです</B></P>\n";
		}
	} else {
		require $USER{'GifCat'};
		print "Content-type: image/gif\n\n";
		binmode(STDOUT);
		for ($i=0 ; $i < length($code) ; $i++) {
			$n = substr($code, $i, 1);
			push(@Digit, "./lib/${n}.gif");
		}
		print &gifcat'gifcat(@Digit);
	}
	exit(1);
}


;### 62進数→10進数
sub C62_Decode {
	my $str = reverse($_[0]);
	my($digit, $i);

	for ($i = 0; $i < length($str); $i++) {
		$digit += index('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', substr($str, $i, 1)) * (62 ** $i);
	}

	return $digit;
}


;### 10進数→62進数
sub C62_Encode {
	my($digit) = $_[0];
	my($str);

	if (!$digit) {
		return 0;
	} else {
		while ($digit) {
			$str .= substr('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ($digit % 62), 1);
			$digit = int($digit / 62);
		}
		return reverse($str);
	}
}
