#!/usr/bin/env perl

use strict;

#==================================================================
# 名称： WwwCount 4.0
# 作者： 杜甫々
# 最新版入手先： https://www.tohoho-web.com/wwwsoft.htm
# 取り扱い： フリーソフト。利用/改造/再配布可能。確認メール不要。
# 著作権：Copyright (C) 1996-2021 杜甫々
#==================================================================

#==================================================================
# 使いかた：
#==================================================================
#   (書式1) wwwcount.cgi?test
#	CGIが使用できるかテストを行う。
#
#   (書式2) wwwcount.cgi?text
#	カウントアップを行い、カウンタをテキストで表示する。
#
#   (書式3) wwwcount.cgi?gif
#	カウントアップを行い、カウンタをGIFで表示する。
#
#   (書式4) wwwcount.cgi?hide+xxx.gif
#	カウントアップを行い、xxx.gifを表示する。
#
#   (オプション) wwwcount.cgi?(略)+name+counter2
#   複数カウンターを設置する場合にカウンター名を指定する。
#
#   (オプション) wwwcount.cgi?(略)+ref+xxxxxx
#   リンク元情報をカウンターに伝える

#==================================================================
# カスタマイズ：
#==================================================================

# ★ このファイルの 1行目の「#!/usr/local/bin/perl」を perl のパス
#    名にあわせて適切に書き換えてください。パス名が分らない場合は、
#    プロバイダやサーバー管理者に問い合わせてください。#! の前には
#    空行もスペース文字も入れないようにしてください。（必須）

# ★ Windows NT で IIS を使用する場合は、wwwcount.cgi がインストー
#    ルされているフォルダ名を 'C:/HomePage/cgi-bin' などのように指
#    定してください。（必須）
my $g_chdir = '';

# ★ SSIのテキストモードで使用する場合は、$g_mode = "text"; としてく
#    ださい。（必須）
my $g_mode = "";

# ★ 表示桁数を例えば5桁に指定する場合は「$g_figure = 5;」のように指
#    定してください。0 を指定すると桁数自動調整になります。
my $g_figure = 6;

# ★ ファイルロック機能をオンにする場合は 1 を、オフにする場合は 0
#    を指定してください。通常は 1 でよいでしょう。
my $g_lock_flag = 1;

# ★ 同アドレスチェック機能をオンにする場合は 1 を指定してください。
#    同じ日に同じ IP アドレスからのアクセスをカウントアップしなく
#    なります。
my $g_address_check = 0;

# ★ 省略時のカウンター名を指定します。カウンター名は *.cnt や *.dat
#    などのファイル名に対応しています。
my $g_counter_name = "wwwcount";

# ★ レポート機能を使う場合は「$g_mailto = 'admin@example.com';」の
#    ように自分のメールアドレスを設定してください。サーバーで
#    sendmailコマンドがサポートされている必要があります。
#    レポート機能を使用しない場合は空文字('')を指定してください。
my $g_mailto  = '';

# ★ レポート機能の送信元メールアドレス（通常は自分のアドレス）を
#    指定してください。省略時はカウンタ名になりますが、プロバイダ
#    によっては、送信元メールアドレスが適切なものかチェックしてい
#    るケースがあります。
my $g_mailfrom = 'admin <admin@example.com>';

# ★ レポート機能で、sendmail コマンドのパス名が /usr/lib/sendmail
#    と異なる場合は、適切に修正してください。
my $g_sendmail = '/usr/sbin/sendmail';

# ★ レポート機能で、詳細情報を添付せず、アクセス件数のみをレポー
#    トする場合は、0 を指定してください。
my $g_report_detail = 1;

# ★ レポート機能で、アクセス元のホスト名を取得できない場合に、は、
#    この値を 1 に変更すると、IPアドレスからホスト名への変換を試み
#    るようになります。ただし、ホスト名変換は、サーバー負荷の原因
#    になるのでご注意ください。
my $g_addr_to_host = 0;

# ★ レポート機能において、「$g_my_url = 'http://www.yyy.zzz/';」とす
#    ると、このアドレスにマッチするサイトからの FROM は表示しなくな
#    ります。
my $g_my_url = '';

# ★ レポート機能で、%7E などのエンコード文字をデコードして記録する
#    場合は 1 を、そのまま記録する場合は 0 を指定してください。
my $g_decode_url = 1;

my $count_dir = $ENV{WWWCOUNT_DIR} // ".";

# ★ ロックファイルを作成するフォルダ名を指定します。
my $g_lock_dir = "$count_dir/lock";

#==================================================================
# その他変数
#==================================================================

# 隠しカウンターで表示するGIFファイル名
my $g_gif_file = "";

# アクセス元情報
my $g_referer = "";

# カウンターファイル名(*.cnt)
my $g_file_count = "$count_dir/${g_counter_name}.cnt";

# 最終アクセス日記録ファイル名(*.dat)
my $g_file_date = "$count_dir/${g_counter_name}.dat";

# アクセス情報記録ファイル名(*.acc)
my $g_file_access = "$count_dir/${g_counter_name}.acc";

# ロックファイル名(*.loc)
my $g_file_lock = "${g_lock_dir}/${g_counter_name}.loc";

#==================================================================
# 処理部：
#==================================================================

#
# メインルーチン
#
{ 
	my($count, $last_access_date, $now_date, $now_time, $do_countup);

	# 環境変数TZを日本時間に設定する
	$ENV{'TZ'} = "JST-9";

	# カレントフォルダを変更する。
	if ($g_chdir ne "") {
		chdir($g_chdir);
	}

	# 引数を解釈する
	parseArguments();

	# テストモードであればテストを呼び出す
	if ($g_mode eq "test") {
		test();
		exit(0);
	}

	# ロックをかける
	doLock();

	# カウンターを読みだす
	$count = readCount();

	# 最終アクセス日を読みだす
	$last_access_date = readLastAccessDate();

	# 今日の日付と時刻を得る
	($now_date, $now_time) = getCurrentDateAndTime();

	# 日付が異なる、つまり、今日初めてのアクセスであれば
	if ($last_access_date ne $now_date) {

		# レポートメールを送信する
		sendReportMail($last_access_date);

		# アクセスログをクリアする
		clearAccessLog();

		# 今日の日付を日付ログファイルに書き出す
		saveLastAccessDate($now_date);
	}

	# 同一IPからのアクセスはカウントアップしないモードの場合、
	# カウントアップするか否かを確認する。
	$do_countup = checkCountup();

	# カウントアップする場合
	if ($do_countup) {

		# カウンターをインクリメントする
		$count++;

		# カウンターを記録する
		saveCount($count);

		# アクセスログを記録する
		saveAccessLog($count, $now_time);
	}

	# CGIの結果としてカウンターを書き出す
	outputCounter($count);

	# ロックを解放する
	unlockLock();
}

#
# 引数を解釈する
#
sub parseArguments {
	my(@argv) = split(/\+/, $ENV{'QUERY_STRING'});

	for (my $i = 0; $i <= $#argv; $i++) {
		# テストモード
		if ($argv[$i] eq "test") {
			$g_mode = "test";

		# テキストモード
		} elsif ($argv[$i] eq "text") {
			$g_mode = "text";

		# GIFモード
		} elsif ($argv[$i] eq "gif") {
			$g_mode = "gif";

		# 隠しカウンターモード
		} elsif ($argv[$i] eq "hide") {
			$g_mode = "hide";
			$g_gif_file = $argv[++$i];
			if (!($g_gif_file =~ /\.gif$/i)) {
				exit(1);
			}
			if ($g_gif_file =~ /[<>|&]/) {
				exit(1);
			}

		# カウンター名
		} elsif ($argv[$i] eq "name") {
			$g_counter_name = $argv[++$i];
			if ($g_counter_name !~ /^[a-zA-Z0-9]+$/) {
				exit(1);
			}
			$g_file_count  = "$g_counter_name" . ".cnt";
			$g_file_date   = "$g_counter_name" . ".dat";
			$g_file_access = "$g_counter_name" . ".acc";
			$g_file_lock   = "$g_lock_dir/$g_counter_name" . ".loc";

		# リンク元
		} elsif ($argv[$i] eq "ref") {
			$g_referer = $argv[++$i];
		}
	}
}

#
# カウンターファイルからカウンター値を読み出す。
#
sub readCount {
	my($count) = 0;
	local(*IN);

	if (open(IN, "< $g_file_count")) {
		$count = <IN>;
		close(IN);
	}
	return $count;
}

#
# カウンタをカウンタファイルに書き戻す
#
sub saveCount {
	my($count) = @_;

	if (open(OUT, "> $g_file_count")) {
		print(OUT "$count");
		close(OUT);
	}
}

#
# 日付ファイルから最終アクセス日付を読み出す。
#
sub readLastAccessDate {
	my $last_access_date;
	if (open(IN, "< $g_file_date")) {
		$last_access_date = <IN>;
		close(IN);
	} else {
		$last_access_date = "";
	}
	return $last_access_date;
}

#
# 今日の日付を日付ファイルに書き出す
#
sub saveLastAccessDate {
	my($now_date) = @_;

	open(OUT, "> $g_file_date");
	print(OUT "$now_date");
	close(OUT);
}

#
# 今日の日付を得る
#
sub getCurrentDateAndTime {
	my($sec, $min, $hour, $mday, $mon, $year) = localtime(time());
	my($now_date) = sprintf("%04d/%02d/%02d", 1900 + $year, $mon + 1, $mday);
	my($now_time) = sprintf("%02d:%02d:%02d", $hour, $min, $sec);
	return $now_date, $now_time;
}

#
# アクセスログを初期化する
#
sub clearAccessLog {
	open(OUT, "> $g_file_access");
	close(OUT);
}

#
# アクセスログを記録する
#
sub saveAccessLog {
	my($count, $now_time) = @_;
	my($addr, $host, $referer);
	local(*OUT);

	open(OUT, ">> $g_file_access");

	# カウント
	print(OUT "COUNT = [ $count ]\n");

	# 時刻
	print(OUT "TIME  = [ $now_time ]\n");

	# IPアドレス
	$addr = $ENV{'REMOTE_ADDR'};
	print(OUT "ADDR  = [ $addr ]\n");

	# ホスト名
	$host = $ENV{'REMOTE_HOST'};
	if ($g_addr_to_host && (($host eq "") || ($host eq $addr))) {
		$host = gethostbyaddr(pack("C4", split(/\./, $addr)), 2);
	}
	if (($host ne "") && ($host ne $addr)) {
		print(OUT "HOST  = [ $host ]\n");
	}

	# エージェント名
	print(OUT "AGENT = [ $ENV{'HTTP_USER_AGENT'} ]\n");

	# リンク元(SSI)
	$referer = $ENV{'HTTP_REFERER'};
	if (($g_mode eq "text") && ($referer ne "")) {
		if ($g_decode_url) {
			$referer =~ s/%([0-9a-fA-F][0-9a-fA-F])/pack("C", hex($1))/eg;
		}
		print(OUT "REFER = [ $referer ]\n");
	}

	# リンク元(CGI)
	$g_referer =~ s/\\//g;
	if ($g_referer && (!$g_my_url || ($g_referer !~ /$g_my_url/))) {
		if ($g_decode_url) {
			$g_referer =~ s/%([0-9a-fA-F][0-9a-fA-F])/pack("C", hex($1))/eg;
		}
		print(OUT "FROM  = [ $g_referer ]\n");
	}

	print(OUT "\n");
	close(OUT);
}

#
# アクセスログをメールで送信する
#
sub sendReportMail {
	my($last_access_date) = @_;
	my($access_count);
	local(*IN, *OUT);

	if ($g_mailto eq "") {
		return;
	}

	# アクセス件数を読み取る
	open(IN, "< $g_file_access");
	$access_count = 0;
	while (<IN>) {
		if (/^COUNT/) {
			$access_count++;
		}
	}
	close(IN);

	# レポートメールを送信する
	open(OUT, "| $g_sendmail -t -i");
	print OUT "To: $g_mailto\n";
	if ($g_mailfrom eq "") {
		print OUT "From: $g_counter_name\n";
	} else {
		print OUT "From: $g_mailfrom\n";
	}
	print OUT "Subject: ACCESS $last_access_date $access_count\n";
	print OUT "\n";
	if ($g_report_detail) {
		open(IN, "< $g_file_access");
		while (<IN>) {
			print OUT $_;
		}
		close(IN);
	} else {
		print OUT "Access = $access_count\n";
	}
	close(OUT);
}

#
# カウントアップするか否かを判断する
# すでに同アドレスからのアクセスがあればカウントアップしない
#
sub checkCountup {
	my($do_countup) = 1;
	local(*IN);

	if ($g_address_check) {
		open(IN, "$g_file_access");
		while (<IN>) {
			if ($_ eq "ADDR  = [ $ENV{'REMOTE_ADDR'} ]\n") {
				$do_countup = 0;
				last;
			}
		}
		close(IN);
	}
	return $do_countup;
}

#
# CGIスクリプトの結果としてカウンターを書き出す
#
sub outputCounter {
	my($count) = @_;
	my($count_str, @files, $size, $n, $buf);

	# カウンター文字列(例:000123)を得る
	if ($g_figure != 0) {
		$count_str = sprintf(sprintf("%%0%dld", $g_figure), $count);
	} else {
		$count_str = sprintf("%ld", $count);
	}

	# テキストモード
	if ($g_mode eq "text") {
		printf("Content-type: text/html\n");
		printf("\n");
		printf("$count_str\n");

	# GIFモード
	} elsif ($g_mode eq "gif") {
		printf("Content-type: image/gif\n");
		printf("\n");
		@files = ();
		for (my $i = 0; $i < length($count_str); $i++) {
			$n = substr($count_str, $i, 1);
			push(@files, "$n.gif");
		}
		require "./gifcat.pl";
		binmode(STDOUT);
		print gifcat'gifcat(@files);

	# 隠しカウンターモード
	} elsif ($g_mode eq "hide") {
		printf("Content-type: image/gif\n");
		printf("\n");
		$size = -s $g_gif_file;
		open(IN, $g_gif_file);
		binmode(IN);
		binmode(STDOUT);
		read(IN, $buf, $size);
		print $buf;
		close(IN);
	}
}

#
# ロックを得る
#
sub doLock {
	my($mtime);

	if ($g_lock_flag) {
		for (my $i = 1; $i <= 6; $i++) {
			if (mkdir("$g_file_lock", 0755)) {
				# ロック成功。次の処理へ。
				last;
			} elsif ($i == 1) {
				# 10分以上古いロックファイルは削除する。
				($mtime) = (stat($g_file_lock))[9];
				if ($mtime < time() - 600) {
					rmdir($g_file_lock);
				}
			} elsif ($i < 6) {
				# ロック失敗。1秒待って再トライ。
				sleep(1);
			} else {
				# 何度やってもロック失敗。あきらめる。
				exit(1);
			}
		}
	}

	# 途中で終了してもロックファイルが残らないようにする
	sub sigexit { rmdir($g_file_lock); exit(0); }
	$SIG{'PIPE'} = $SIG{'INT'} = $SIG{'HUP'} = $SIG{'QUIT'} = $SIG{'TERM'} = "sigexit";
}

#
# ロックを開放する
#
sub unlockLock {
	if ($g_lock_flag) {
		rmdir($g_file_lock);
	}
}

#
# CGIが使用できるかテストを行う。
#
sub test {
	print "Content-type: text/html\n";
	print "\n";
	print "<!doctype html>\n";
	print "<html>\n";
	print "<head>\n";
	print "<meta charset='utf-8'>\n";
    print "<title>Test</title>\n";
    print "</head>\n";
	print "<body>\n";
	print "<p>OK. CGIスクリプトは正常に動いています。</p>\n";
	if ($g_mailto ne "") {
		if (! -f $g_sendmail) {
			print "<p>ERROR: $g_sendmail が存在しません。</p>\n";
		}
	}
	if (!-d $g_lock_dir) {
		print "<p>ERROR: $g_lock_dir フォルダがありません。</p>\n";
	}
	if (-d $g_file_lock) {
		print "<p>ERROR: $g_file_lock が残っています。</p>\n";
	}
	if (! -r $g_file_count) {
		print "<p>ERROR: $g_file_count が存在しません。</p>\n";
	} elsif (! -w $g_file_count) {
		print "<p>ERROR: $g_file_count が書き込み可能ではありません。</p>\n";
	}
	if (! -r $g_file_date) {
		print "<p>ERROR: $g_file_date が存在しません。</p>\n";
	} elsif (! -w $g_file_date) {
		print "<p>ERROR: $g_file_date が書き込み可能ではありません。</p>\n";
	}
	if (! -r $g_file_access) {
		print "<p>ERROR: $g_file_access が存在しません。</p>\n";
	} elsif (! -w $g_file_access) {
		print "<p>ERROR: $g_file_access が書き込み可能ではありません。</p>\n";
	}
	if (($g_chdir ne "") && (! -d $g_chdir)) {
		print "<p>ERROR: $g_chdir が存在しません。</p>\n";
	}
	print "</body>\n";
	print "</html>\n";
}
