#!/usr/local/bin/perl

# このスクリプトを使用したいかなる損害に対して作者は一切の責任を負いません。
#
# *** 設置例 ***
#    public_html/                     ホームディレクトリ
#        cgi-bin/                     CGIディレクトリ
#            puretree/          [777] PureTreeのディレクトリ
#                puretree.cgi   [755] PureTreeのCGIファイル
#                puretree.css   [644] PureTreeのCSSファイル
#
# 設置例では http://www.***.com/cgi-bin/puretree/puretree.cgi で実行することを想定

### 必須設定 ###
$PASS = 'donkiti';	#管理者のパスワード(4文字以上推奨)

### システム系 ###
$CGIURL = './puretree.cgi';	#CGIのURL(例:'http://www.***.com/cgi-bin/puretree/puretree.cgi')
$CSSURL = './puretree.css';	#CSSのURL(例:'http://www.***.com/cgi-bin/puretree/puretree.css')
$DATADIR = './';	#データの保存ディレクトリ(例:'./data/')
$LOGMAX = 100;	#ログのファイルサイズの上限(KB) このサイズを超えると過去ログへ順次移動

### 表示系 ###
$TITLE = '実践！筋トレ相談室';	#ページのタイトル
$SUBJECTMAX = 60;	#タイトルの表示上限(バイト)
$POPUPMAX = 100;	#コメントのポップアップ上限(バイト) 0でポップアップ機能Off
$SUMMARYMAX = 200;	#検索結果概要の表示上限(バイト)
$PAGEMAX = 5;	#ページ選択の表示数(ページ)
$TREEOFFSET = 2;	#各記事のツリー間隔(em) 0で記事のツリー表示Off
$UPTHREAD = 1;	#更新されたスレッドの浮上(0:Off 1:On)
$DATE1 = 72;	#新着時間1(時間) 投稿日時を赤表示 (注:赤表示されたスレッドが過去ログへ流れないよう設定調整してください。投稿規制が発動します)
$DATE2 = 24;	#新着時間2(時間) 投稿日時を赤強調表示

### 制限系 ###
$COMMENTMAX = 1;	#投稿サイズ上限(バイト)
$COMMENTLINEMAX = 100;	#投稿行上限(行)
$AUTHORMAX = 1;	#名前サイズ上限(バイト)
$POSTINTERVAL = 15;	#連続投稿禁止時間(秒)
$EDITLIMIT = 60;	#修正可能な時間(分) -99で修正機能Off

### 掲示板上部のレイアウト ###
$TOPLAYOUT = '<div style="text-align:center;margin:10px;"><span style="font-size:150%;font-weight:bold;"><img src="soudanshitu.gif"></span><br></div>'."\n";

####################
# メイン
####################
%FORM = ();
%COOKIE = ();
{
	#フォームの読み込み
	if (my $error = &get_form(\%FORM)) {&message($error); exit(0);}
	if (!defined($FORM{'mode'}) || ($FORM{'mode'} !~ /^\w+$/)) {$FORM{'mode'} = 'list';}
	if (!defined($FORM{'page'}) || ($FORM{'page'} !~ /^\d{4}$/)) {$FORM{'page'} = '9999';}
	if (!defined($FORM{'id'}) || ($FORM{'id'} !~ /^\d{10}$/)) {$FORM{'id'} = '';}
	if (!defined($FORM{'parent'}) || ($FORM{'parent'} !~ /^\d{10}$/)) {$FORM{'parent'} = '';}
	if (!defined($FORM{'root'}) || ($FORM{'root'} !~ /^\d{10}$/)) {$FORM{'root'} = '';}
	if (!defined($FORM{'sid'}) || ($FORM{'sid'} =~ /\W/)) {$FORM{'sid'} = '';}
	if (!defined($FORM{'query'})) {$FORM{'query'} = '';}
	else {
		&trim(\$FORM{'query'}); &z2h(\$FORM{'query'}); &tolower(\$FORM{'query'});
		my @keyword = ();
		foreach (split(/ /,$FORM{'query'})) {
			if (@keyword >= 3) {last;}
			push(@keyword,$_);
		}
		$FORM{'query'} = join(' ',@keyword);
	}
	if (!defined($FORM{&sessionid()})) {delete $FORM{'comment'}}
	else {
		$FORM{'comment'} = $FORM{&sessionid()};
		&z2h(\$FORM{'comment'});
		$FORM{'comment'} =~ s/ +$//mg;
		$FORM{'comment'} =~ s/\n+$//;
	}
	if (!defined($FORM{'author'})) {$FORM{'author'} = '';} else {&trim(\$FORM{'author'}); &z2h(\$FORM{'author'});}

	#クッキーの読み込み
	&get_cookie(\%COOKIE);
	if (!defined($COOKIE{'key'}) || ($COOKIE{'key'} !~ /^\w{10}$/)) {$COOKIE{'key'} = '';}
	if (!defined($COOKIE{'author'})) {$COOKIE{'author'} = '';} else {&trim(\$FORM{'author'}); &z2h(\$FORM{'author'});}

	#管理者チェック
	if (defined($COOKIE{'admin'})) {
		if ($COOKIE{'admin'} ne &sessionid_admin()) {$FORM{'admin'} = ''; &admin();}
		if (time - (stat($DATADIR.'pass.lock'))[9] > 3600) {$FORM{'admin'} = ''; &admin();}
	}

	#セッションチェックと連続投稿チェック
	if (($FORM{'mode'} eq 'insert') || ($FORM{'mode'} eq 'update')) {
		my $timestamp = 0; if (-e $DATADIR.'9999.cgi') {$timestamp = (stat($DATADIR.'9999.cgi'))[9];}
		if (($FORM{'mode'} eq 'insert') && ((my $wait = $timestamp - time + $POSTINTERVAL) > 0)) {&message('あと '.$wait.' 秒お待ちください。'); exit(0);}
		if (($FORM{'mode'} eq 'update') && ((my $wait = $timestamp - time + 5) > 0)) {&message('あと '.$wait.' 秒お待ちください。'); exit(0);}
		if (!defined($FORM{'comment'})) {&message('セッションが一致しません。トップページから投稿しなおしてください。'); exit(0);}
	}

	#各モードの呼び出し
	if ($FORM{'mode'} eq 'view') {&view();}
	elsif ($FORM{'mode'} eq 'input') {&input();}
	elsif (($FORM{'mode'} eq 'insert') && ($ENV{'REQUEST_METHOD'} eq 'POST')) {&insert();}
	elsif (($FORM{'mode'} eq 'update') && ($ENV{'REQUEST_METHOD'} eq 'POST')) {&update();}
	elsif ($FORM{'mode'} eq 'remove') {&remove();}
	elsif ($FORM{'mode'} eq 'search') {&search();}
	elsif ($FORM{'mode'} eq 'admin') {&admin();}
	$FORM{'mode'} = 'list'; &list();
	exit(0);
}

####################
# モード "list"
####################
sub list {
	#データの読み込み
	my @data = ();
	if (my $error = &load_data(\@data,$FORM{'page'})) {&message($error); exit(0);}

	#ページの取得
	my $page = '';
	if ($FORM{'page'} ne '9999') {$page = '&amp;page='.$FORM{'page'};}

	###出力開始###
	print 'Content-type: text/html; charset=Shift-JIS'."\n\n";
	print &header();

	#コントローラーの表示
	print &controller();

	#ツリーの表示
	push(@data,'0000000000	0000000000	0000000000	0	0.0.0.0	AAAAAAAAAA		');	#番犬
	my ($thread,$root_old,$depth_old,$branch) = ('',0,0,1);
	foreach my $index (0..$#data) {
		if ($data[$index] !~ /^(\d{10})\t(\d{10})\t(\d{10})\t(\d+)\t(\d+\.\d+\.\d+\.\d+)\t(\w{10})\t(.*?)\t(.*?)$/) {
			$thread .= '<div style="color:#aaaaaa;">Data error line '.($index + 1).'.</div>'."\n"; next;
		}
		my ($id,$parent,$root,$depth,$ip,$key,$comment,$author) = ($1,$2,$3,$4,$5,$6,$7,$8);

		#スレッドの表示
		if (!$root_old) {$root_old = $root;} elsif ($root != $root_old) {
			print "<div class=\"list_thread\">\n$thread</div>\n\n";
			$thread = ''; $root_old = $root; $depth_old = $depth; $branch = 1;
		}

		#タイトルとポップアップの取得
		my ($subject,$popup) = ('','');
		if ($comment =~ /<br>/) {$subject = $`; $popup = $';} else {$subject = $comment;}
		&html_decode(\$subject); &trim(\$subject); &fold(\$subject,$SUBJECTMAX); &html_encode(\$subject);
		if ($subject eq '') {$subject = '無題';}
		if ($POPUPMAX) {
			&html_decode(\$popup); $popup =~ s/^>.*?$//mg; &trim(\$popup); &fold(\$popup,$POPUPMAX); &html_encode(\$popup);
			if (!$popup) {$popup = '(なし)';}
			$popup = ' title="'.$popup.'"';
		} else {$popup = '';}

		#日時の取得
		my $date = '';
		{
			my ($sec,$min,$hour,$day,$mon,$year) = (localtime($id))[0..5];
			$date = sprintf("%04d/%02d/%02d",$year+1900,$mon+1,$day);
			if (time - $id < 3600 * $DATE2) {$date = ' <span class="list_date2">'.$date.'</span>';}
			elsif (time - $id < 3600 * $DATE1) {$date = ' <span class="list_date1">'.$date.'</span>';}
			else {$date = ' <span class="list_date0">'.$date.'</span>';}
		}

		#投稿者の取得
		if ($author) {$author = ' <span class="list_author">'.$author.'</span>';}

		#ブランクの取得
		my $blank = '';
		foreach (1..$depth) {$blank .= '&nbsp;&nbsp;&nbsp;&nbsp;';}

		#子行のトグル
		if ($depth <= $depth_old) {$branch = (!$branch);}

		#1行追加
		$depth_old = $depth;
		if ($depth == 0) {
			$thread .= '<div class="list_root"><a href="'.$CGIURL.'?mode=view'.$page.'&amp;root='.$root.'"'.$popup.'>'.$blank.'<span class="list_rootmark">●</span><span class="list_subject">'.$subject.'</span>'.$date.$author."</a></div>\n";
		} else {
			$thread .= '<div class="list_branch'.($branch + 1).'"><a href="'.$CGIURL.'?mode=view'.$page.'&amp;root='.$root.'#'.$id.'"'.$popup.'>'.$blank.'<span class="list_branchmark'.($branch + 1).'">●</span><span class="list_sunject">'.$subject.'</span>'.$date.$author."</a></div>\n";
		}
	}

	#文末の表示
	print '<hr><a href="'.$CGIURL.'?mode=admin" title="管理者モードです" class="admin_link" rel="nofollow">管理</a>';

	print &footer();
	exit(0);
}

####################
# モード "view"
####################
sub view {
	#データの読み込み
	my @data = ();
	if (my $error = &load_data(\@data,$FORM{'page'})) {&message($error); exit(0);}

	#スレッドの取得
	my @thread = ();
	foreach (@data) {
		if ($_ !~ /^(\d{10})\t(\d{10})\t(\d{10})\t(\d+)\t(\d+\.\d+\.\d+\.\d+)\t(\w{10})\t(.*?)\t(.*?)$/) {next;}
		if ($3 == $FORM{'root'}) {push(@thread,$_);}
	}
	if (@thread == 0) {&message('記事 '.$FORM{'root'}.' が見つかりません。'); exit(0);}

	###出力開始###
	print 'Content-type: text/html; charset=Shift-JIS'."\n\n";
	print &header();
	print '<table class="topic_table"><tr><td>'."\n";
	foreach (@thread) {print &layout(split(/\t/,$_));}
	print '</td></tr></table>'."\n";
	print '<div style="height:1200px"></div>'."\n";
	print &footer();
	exit(0);
}

####################
# モード "input"
####################
sub input {
	#入力フォームの取得
	my ($action,$guide,$comment_value,$author_value) = ('','','',$COOKIE{'author'});
	&html_encode(\$author_value);
	if ($FORM{'parent'}) {
		#返信の場合
		my $parent_data = '';
		{
			my @data = (); if (my $error = &load_data(\@data,'9999')) {&message($error); exit(0);}
			foreach (@data) {
				if ($_ !~ /^(\d{10})\t(\d{10})\t(\d{10})\t(\d+)\t(\d+\.\d+\.\d+\.\d+)\t(\w{10})\t(.*?)\t(.*?)$/) {next;}
				if ($1 == $FORM{'parent'}) {$parent_data = '<table class="topic_table"><tr><td>'."\n".&layout($1,$2,$3,0,$5,$6,$7,$8).'</td></tr></table>'."\n"; last;}
			}
		}
		if ($parent_data eq '') {&message('親記事 '.$FORM{'parent'}.' が見つかりません。'); exit(0);}
		$guide .= $parent_data;
		$guide .= '<br>上記へ返信します。<a href="'.$CGIURL.'?'.time.'" class="list_link">新着一覧へ</a>'."<br>\n";
		$action .= '<form action="'.$CGIURL.'" name="input" method="POST" class="input_form">'."\n";
		$action .= '<input type="hidden" name="mode" value="insert">'."\n";
		$action .= '<input type="hidden" name="parent" value="'.$FORM{'parent'}.'">'."\n";
	} elsif($FORM{'id'}) {
		#修正の場合
		{
			my @data = (); if (my $error = &load_data(\@data,'9999')) {&message($error); exit(0);}
			foreach (@data) {
				if ($_ !~ /^(\d{10})\t(\d{10})\t(\d{10})\t(\d+)\t(\d+\.\d+\.\d+\.\d+)\t(\w{10})\t(.*?)\t(.*?)$/) {next;}
				if ($1 == $FORM{'id'}) {
					$comment_value = $7; $author_value = $8;
					$comment_value =~ s/&nbsp;/ /g;
					$comment_value =~ s/<br>/\n/g;
					$comment_value =~ s/<.*?>//g;
					if ($CGIURL =~ /^http:/i) {$comment_value =~ s/http:\/\/CGIURL/$CGIURL/g;}
					else {my $cgiurl = ("http://$ENV{'HTTP_HOST'}$ENV{'REQUEST_URI'}" =~ /^(.+?\.cgi)/i)[0]; $comment_value =~ s/http:\/\/CGIURL/$cgiurl/g;}
					last;
				}
			}
		}
		if ($comment_value eq '') {&message('元記事 '.$FORM{'id'}.' が見つかりません。'); exit(0);}
		$guide .= '<br>修正します。<a href="'.$CGIURL.'?'.time.'" class="list_link">新着一覧へ</a>'."<br>\n";
		$action .= '<form action="'.$CGIURL.'" name="input" method="POST" class="input_form">'."\n";
		$action .= '<input type="hidden" name="mode" value="update">'."\n";
		$action .= '<input type="hidden" name="id" value="'.$FORM{'id'}.'">'."\n";
	} else {
		#新規投稿の場合
		$guide .= '<br>新規の質問文を作成します。<a href="'.$CGIURL.'?'.time.'" class="list_link">新着一覧へ</a>'."<br>\n";
		$action .= '<form action="'.$CGIURL.'" name="input" method="POST" class="input_form">'."\n";
		$action .= '<input type="hidden" name="mode" value="insert">'."\n";
	}
	$action .= '<textarea name="'.$FORM{'sid'}.'" cols="60" rows="6" class="input_comment">'."\n".$comment_value.'</textarea><br>'."\n";
	$action .= '名前: <input type="text" name="author" value="'.$author_value.'" class="input_author">'."\n";
	$action .= '<input type="submit" value="投稿する" class="input_post">'."\n";
	$action .= '</form>'."\n";

	#編集ガイド
	my $edit_guide = -1;
	if ($EDITLIMIT && $FORM{'id'}) {$edit_guide = $FORM{'id'} - time + $EDITLIMIT * 60;}
	elsif ($EDITLIMIT) {$edit_guide = $EDITLIMIT * 60;}
	if ($edit_guide >= 0) {
		if ($edit_guide >= 86400) {$edit_guide = int($edit_guide / 86400).'日';}
		elsif ($edit_guide >= 3600) {$edit_guide = int($edit_guide / 3600).'時';}
		elsif ($edit_guide > 0) {$edit_guide = int($edit_guide / 60).'分';}
		else {$edit_guide = '0分';}
		$edit_guide = '投稿内容は <b>'.$edit_guide.'間以内</b> であれば修正できます。(あなたのPCからのみ)'."<br>\n";
	} else {
		$edit_guide = '';
	}

	###出力開始###
	print 'Content-type: text/html; charset=Shift-JIS'."\n\n";
	print &header();
	print $guide;
	print '<table class="input_table"><tr><td>'.$action.'</td></tr></table>'."<br>\n";
	print '先頭行はタイトルとしても扱われます。'."<br>\n";
	print '全角の英数字は、投稿時に半角へ自動変換されます。'."<br>\n";
	print $edit_guide;
	print '<script type="text/javascript">'."\n<!--\n".'document.input.'.$FORM{'sid'}.'.focus();'."\n// -->\n".'</script>'."\n";
	print &footer();
	exit(0);
}

####################
# モード "insert"
####################
sub insert {
	#入力チェック
	{
		my $error = '';
		if ($FORM{'comment'} eq '') {$error .= 'コメントが空です。<br>';}
		if ((my $over = (length($FORM{'comment'}) - $COMMENTMAX)) > 0) {$error .= 'コメントが '.$over.' バイト多すぎます。<br>';}
		{
			my $over = 1;
			while ($FORM{'comment'} =~ /\n/g) {$over++;}
			$over -= $COMMENTLINEMAX;
			if ($over > 0) {$error .= 'コメントが '.$over.' 行多すぎます。<br>';}
		}
		if ((my $over = (length($FORM{'author'}) - $AUTHORMAX)) > 0) {$error .= '名前が '.$over.' バイト多すぎます。<br>';}
		if ($error) {&message($error); exit(0);}
	}

	#クッキーの準備
	if ($COOKIE{'key'} eq '') {
		my @code = ('0'..'9','A'..'Z','a'..'z');
		for (my $i = 10;$i > 0;$i--) {$COOKIE{'key'} .= $code[int(rand(62))];}
	}
	$COOKIE{'author'} = $FORM{'author'};

	#サニタイズ
	&html_encodex(\$FORM{'comment'});
	&html_encode(\$FORM{'author'});

	#URLの自動リンク
	{
		my $cgiurl = $CGIURL;
		if ($CGIURL !~ /^http:/i) {$cgiurl = ("http://$ENV{'HTTP_HOST'}$ENV{'REQUEST_URI'}" =~ /^(.+?\.cgi)/i)[0];}
		$FORM{'comment'} =~ s/$cgiurl/http:\/\/CGIURL/ig;
		$FORM{'comment'} =~ s/(http:\/\/[\w\#\%\&\+\-\.\/\:\;\=\?\@\~]+)/<a href="$1" target="_blank" rel="nofollow">$1<\/a>/ig;
	}

	### Lock ###
	if (my $error = &lock()) {&message($error); exit(0);}

	#データの読み込み
	my @data = ();
	if (my $error = &load_data(\@data,'9999')) {&message($error); &unlock(); exit(0);}

	#$id,$parent,$root,$depthの取得
	my ($id,$parent,$root,$depth) = (time,0,0,0);
	if ($FORM{'parent'}) {
		foreach (@data) {
			if ($_ !~ /^(\d{10})\t(\d{10})\t(\d{10})\t(\d+)\t(\d+\.\d+\.\d+\.\d+)\t(\w{10})\t(.*?)\t(.*?)$/) {next;}
			if ($1 == $FORM{'parent'}) {($parent,$root,$depth) = ($1,$3,$4); ++$depth; last;}
		}
	}
	if (!$depth) {$parent = $root = $id;}

	#データの追加
	my $line = sprintf("%10d\t%10d\t%10d\t%d",$id,$parent,$root,$depth)."\t".$ENV{'REMOTE_ADDR'}."\t".$COOKIE{'key'}."\t".$FORM{'comment'}."\t".$FORM{'author'};
	if (!$depth) {
		unshift(@data,$line);
	} else {
		#挿入位置の取得
		my ($top,$here,$last,$flag) = (-1,-1,-1,0);
		push(@data,'0000000000	0000000000	0000000000	0	0.0.0.0	AAAAAAAAAA		');	#番犬
		foreach (@data) {
			$last++;
			if ($_ !~ /^(\d{10})\t(\d{10})\t(\d{10})\t(\d+)\t(\d+\.\d+\.\d+\.\d+)\t(\w{10})\t(.*?)\t(.*?)$/) {
				if ($top < 0) {next;}
				else {&message('このスレッドはデータが壊れている可能性があるため返信できません。'); &unlock(); exit(0);}
			}
			if ($3 != $root) {if ($top < 0) {next;} last;}
			if ($top < 0) {$top = $last; next;}
			if ($here < 0) {if ($flag) {if ($4 < $depth) {$here = $last;}} elsif ($1 == $parent) {$flag++;} next;}
		}
		if ($here < 0) {$here = $last;}
		pop(@data);
		#1行挿入
		splice(@data,$here,0,$line);
		#スレッドの浮上
		if ($UPTHREAD) {
			my @thread = splice(@data,$top,$last-$top+1);
			unshift(@data,@thread);
		}
	}

	#データの書き込み
	if (my $error = &save_data(\@data)) {&message($error); &unlock(); exit(0);}

	###出力開始###
	print 'Content-type: text/html; charset=Shift-JIS'."\n";
	print &set_cookie(\%COOKIE)."\n";
	print &header();
	print '投稿ありがとうございました。<a href="'.$CGIURL.'?'.time.'" class="list_link">新着一覧へ</a>'."<br>\n";
	print '<table class="topic_table"><tr><td>'."\n".&layout($id,$parent,$root,0,$ENV{'REMOTE_ADDR'},$COOKIE{'key'},$FORM{'comment'},$FORM{'author'}).'</td></tr></table>'."\n";
	print &footer();

	&unlock();
	exit(0);
}

####################
# モード "update"
####################
sub update {
	#入力チェック
	if (!$COOKIE{'admin'} && ($FORM{'id'} + $EDITLIMIT * 60 - time < 0)) {&message('この記事は修正できません。'); exit(0);}
	{
		my $error = '';
		if ($FORM{'comment'} eq '') {$error .= 'コメントが空です。<br>';}
		if ((my $over = (length($FORM{'comment'}) - $COMMENTMAX)) > 0) {$error .= 'コメントが '.$over.' バイト多すぎます。<br>';}
		{
			my $over = 1;
			while ($FORM{'comment'} =~ /\n/g) {$over++;}
			$over -= $COMMENTLINEMAX;
			if ($over > 0) {$error .= 'コメントが '.$over.' 行多すぎます。<br>';}
		}
		if ((my $over = (length($FORM{'author'}) - $AUTHORMAX)) > 0) {$error .= '名前が '.$over.' バイト多すぎます。<br>';}
		if ($error) {&message($error); exit(0);}
	}

	#サニタイズ
	&html_encodex(\$FORM{'comment'});
	&html_encode(\$FORM{'author'});

	#URLの自動リンク
	{
		my $cgiurl = $CGIURL;
		if ($CGIURL !~ /^http:/i) {$cgiurl = ("http://$ENV{'HTTP_HOST'}$ENV{'REQUEST_URI'}" =~ /^(.+?\.cgi)/i)[0];}
		$FORM{'comment'} =~ s/$cgiurl/http:\/\/CGIURL/ig;
		$FORM{'comment'} =~ s/(http:\/\/[\w\#\%\&\+\-\.\/\:\;\=\?\@\~]+)/<a href="$1" target="_blank" rel="nofollow">$1<\/a>/ig;
	}

	### Lock ###
	if (my $error = &lock()) {&message($error); exit(0);}

	#データの読み込み
	my @data = ();
	if (my $error = &load_data(\@data,'9999')) {&message($error); &unlock(); exit(0);}

	#データの更新
	my ($parent,$root,$depth,$key) = (0,0,0,'');
	{
		my $offset = -1;
		foreach (@data) {
			++$offset;
			if ($_ !~ /^(\d{10})\t(\d{10})\t(\d{10})\t(\d+)\t(\d+\.\d+\.\d+\.\d+)\t(\w{10})\t(.*?)\t(.*?)$/) {next;}
			if ($1 == $FORM{'id'}) {($parent,$root,$depth,$key) = ($2,$3,$4,$6); last;}
		}
		if (!$parent || !$root) {&message('記事 '.$FORM{'id'}.' が見つかりません。'); &unlock(); exit(0);}
		if (!$COOKIE{'admin'} && ($key ne $COOKIE{'key'})) {&message('クッキーが一致しません。'); &unlock(); exit(0);}
		my $line = sprintf("%10d\t%10d\t%10d\t%d",$FORM{'id'},$parent,$root,$depth)."\t".$ENV{'REMOTE_ADDR'}."\t".$key."\t".$FORM{'comment'}."\t".$FORM{'author'};
		splice(@data,$offset,1,$line);
	}

	#データの書き込み
	if (my $error = &save_data(\@data)) {&message($error); &unlock(); exit(0);}

	###出力開始###
	print 'Content-type: text/html; charset=Shift-JIS'."\n\n";
	print &header();
	print '修正しました。<a href="'.$CGIURL.'?'.time.'" class="list_link">新着一覧へ</a>'."<br>\n";
	print '<table class="topic_table"><tr><td>'."\n".&layout($FORM{'id'},$parent,$root,0,$ENV{'REMOTE_ADDR'},$key,$FORM{'comment'},$FORM{'author'}).'</td></tr></table>'."\n";
	print &footer();

	&unlock();
	exit(0);
}

####################
# モード "remove"
####################
sub remove {
	if (!$COOKIE{'admin'}) {&message('削除できるのは管理者のみです。'); exit(0);}

	### Lock ###
	if (my $error = &lock()) {&message($error); exit(0);}

	#データの読み込み
	my @data = ();
	if (my $error = &load_data(\@data,'9999')) {&message($error); &unlock(); exit(0);}

	#データの削除
	{
		my $offset = -1;
		foreach (@data) {
			++$offset;
			if ($_ !~ /^(\d{10})\t(\d{10})\t(\d{10})\t(\d+)\t(\d+\.\d+\.\d+\.\d+)\t(\w{10})\t(.*?)\t(.*?)$/) {next;}
			if ($1 == $FORM{'id'}) {splice(@data,$offset,1); $offset = -1; last;}
		}
		if ($offset >= 0) {&message('記事 '.$FORM{'id'}.' が見つかりません。'); &unlock(); exit(0);}
	}

	#データの書き込み
	if (my $error = &save_data(\@data)) {&message($error); &unlock(); exit(0);}

	###出力開始###
	print 'Content-type: text/html; charset=Shift-JIS'."\n\n";
	print &header();
	print '削除しました。<a href="'.$CGIURL.'?'.time.'" class="list_link">新着一覧へ</a>'."<br>\n";
	print &footer();

	&unlock();
	exit(0);
}

####################
# モード "search"
####################
sub search {
	#キーワード
	my @keyword = split(/\s/,$FORM{'query'});
	if (@keyword == 0) {&message('キーワードを入力してください。'); exit(0);}

	#キーワード(HTMLエンコード版)
	my @keyword_html = @keyword;
	foreach (@keyword_html) {&html_encode(\$_);}

	#キーワード(URLエンコード版)
	my $keyword_url = $FORM{'query'};
	$keyword_url =~ s/(\W)/sprintf("%%%02X",ord($1))/eg; $keyword_url =~ s/%20/\+/g;

	#データの読み込み
	my @data = ();
	if (my $error = &load_data(\@data,$FORM{'page'})) {&message($error); exit(0);}

	#検索結果の取得
	my ($result,$buf,$subject,$root_old,$thread_count,$hit_count,$comment_count) = ('','','',0,0,0,0);
	push(@data,'0000000000	0000000000	0000000000	0	0.0.0.0	AAAAAAAAAA		');	#番犬
	foreach (@data) {
		my ($root,$comment,$author) = (0,'','');
		if ($_ =~ /^(\d{10})\t(\d{10})\t(\d{10})\t(\d+)\t(\d+\.\d+\.\d+\.\d+)\t(\w{10})\t(.*?)\t(.*?)$/) {($root,$comment,$author) = ($3,$7,$8);}

		if (!$root_old) {$root_old = $root;} elsif ($root != $root_old) {
			$thread_count++;

			#小文字化
			&tolower(\$buf);

			#ヒット判定
			HITCHECK:{
				#簡易ヒット判定
				foreach (@keyword_html) {
					if (index($buf,$_) < 0) {last HITCHECK;}
				}

				#タイトルの取得
				&html_decode(\$subject); &trim(\$subject); &fold(\$subject,$SUBJECTMAX); &html_encode(\$subject);
				if ($subject eq '') {$subject = '無題';}

				#日時の取得
				my $date = '';
				{
					my ($sec,$min,$hour,$day,$mon,$year) = (localtime($root_old))[0..5];
					$date = sprintf("%04d/%02d/%02d",$year+1900,$mon+1,$day);
				}

				#検索結果概要の取得
				my %hit = (); my $all_hit = 0;
				my $summary = '';
				SUMMARYLOOP:foreach my $line (split(/<br>/,$buf)) {
					&html_decode(\$line); &trim(\$line);
					my $hit_flag = 0;
					foreach (@keyword) {
						if (my $hit_count = &replace(\$line,$_,chr(0x0e).$_.chr(0x0f))) {
							$hit_flag += $hit_count;
							$hit{$_} += $hit_count;
						}
					}
					if (!$hit_flag) {next SUMMARYLOOP}
					if (length($summary) < $SUMMARYMAX) {$summary .= $line.' ';}
					$all_hit = 1; foreach (@keyword) {if (!defined($hit{$_})) {$all_hit = 0;}}
					if ($all_hit && length($summary) >= $SUMMARYMAX) {last SUMMARYLOOP;}
				}

				#全キーワードがヒットしたら表示
				if ($all_hit) {
					$hit_count++;
					&html_encode(\$summary);
					$summary =~ s/\x0e/<span class="search_highlight">/g;
					$summary =~ s/\x0f/<\/span>/g;
					$result .= '<div class="search_topic"><a href="'.$CGIURL.'?mode=view&amp;page='.$FORM{'page'}.'&amp;root='.$root_old.'&amp;query='.$keyword_url.'" title="検索結果'.$hit_count.'"><span class="search_subject">'.$subject.'</span><br><span class="search_date">'.$date.' 投稿数'.$comment_count.'</span><br><span class="search_summary">'.$summary."</span></a></div>\n";
				}
			}

			#次処理
			$root_old = $root; $buf = $subject = ''; $comment_count = 0;
		}
		$buf .= $author.' '.$comment.'<br>';
		if ($subject eq '') {if ($comment =~ /<br>/) {$subject = $`;} else {$subject = $comment;}}
		$comment_count++;
	}

	###出力開始###
	print 'Content-type: text/html; charset=Shift-JIS'."\n\n";
	print &header();

	#コントローラーの表示
	print &controller();

	#検索結果の表示
	print '指定したページに <b>'.join(' ',@keyword_html).'</b> を含むスレッドが <b>'.$hit_count.'件</b> 見つかりました。('.$thread_count.'件中)'."<br>\n";
	print $result;
	print '検索モードを解除するには"検索解除"ボタンをクリックしてください。'."<br>\n";
	print 'キーワードは3つまで指定できます。各キーワードはスペース(全角スペースも可)で区切ります。'."<br>\n";

	print &footer();
	exit(0);
}

####################
# モード "admin"
####################
sub admin {
	my ($logcount,$logsize) = (0,0);
	if (!opendir(DIR,$DATADIR)) {return($!.' '.$DATADIR);}
	foreach (readdir(DIR)) {if ($_ =~ /^\d{4}\.cgi$/) {++$logcount; $logsize += (stat($DATADIR.$_))[7];}}
	closedir(DIR);
	if (!defined($FORM{'admin'})) {
		print 'Content-type: text/html; charset=Shift-JIS'."\n\n";
		print &header();
		print '<noscript><div style="color:#ff0000;font-weight:bold;">ブラウザのJavaScriptをOnにしてください。管理者モードが正常に機能しません。</div></noscript>'."\n";
		print '管理者パスワードを入力してください。<a href="'.$CGIURL.'?'.time.'" class="list_link">新着一覧へ</a>'."<br>\n";
		print '<table class="admin_table"><tr><td><form action="'.$CGIURL.'" name="password" method="POST" class="admin_form"><input type="hidden" name="mode" value="admin"><input type="password" name="admin" class="admin_input"><input type="submit" value="送信" class="admin_post"></form></td></tr></table>'."\n";
		print 'ログファイル数: '.$logcount."<br>\n";
		print 'ログサイズ: '.int($logsize / 1024)."KB<br><br>\n";
		print '<a href="http://www2s.biglobe.ne.jp/~memo/" class="version_link">PureTree 1.08</a>'."<br>\n";
		print '<script type="text/javascript">'."\n<!--\n".'document.password.admin.focus();'."\n// -->\n".'</script>'."\n";
		print &footer();
	} elsif ($FORM{'admin'} eq $PASS) {
		if (my $error = &passlock()) {&message($error); exit(0);}
		$COOKIE{'admin'} = &sessionid_admin();
		print 'Content-type: text/html; charset=Shift-JIS'."\n";
		print &set_cookie(\%COOKIE)."\n";
		print &header();
		print 'あなたのPCは管理者モードに設定されました。記事の修正と削除が可能です。<a href="'.$CGIURL.'?'.time.'" class="list_link">新着一覧へ</a>'."<br><br>\n";
		print '管理者モードは <b>1時間後</b> に自動解除されます。'."<br>\n";
		print '空パスワードの送信で管理者モードを手動で解除することもできます。'."<br>\n";
		print &footer();
	} else {
		if (my $error = &passlock()) {&message($error); exit(0);}
		my $message = 'パスワードが違います。';
		if (defined($COOKIE{'admin'})) {$message = '管理者モードは解除されました。';}
		delete($COOKIE{'admin'});
		print 'Content-type: text/html; charset=Shift-JIS'."\n";
		print &set_cookie(\%COOKIE)."\n";
		print &header();
		print $message.'<a href="'.$CGIURL.'?'.time.'" class="list_link">新着一覧へ</a>'."<br>\n";
		print &footer();
	}
	exit(0);
}

####################
# 操作部のレイアウト(検索ボタン、新規投稿ボタンなど)
# In:	none
# Out:	$str
####################
sub controller {
	#ページリストの取得
	my $pagelist = '';
	{
		my @filelist = ();
		if (!opendir(DIR,$DATADIR)) {return($!.' '.$DATADIR);}
		foreach (reverse sort readdir(DIR)) {if ($_ =~ /^(\d{4})\.cgi$/) {push(@filelist,$1);}}
		closedir(DIR);
		if (@filelist > 1) {
			my $query = '';
			if ($FORM{'query'} ne '') {
				$query = $FORM{'query'};
				$query =~ s/(\W)/sprintf("%%%02X",ord($1))/eg; $query =~ s/%20/\+/g;
				$query = '&amp;query='.$query;
			}
			my $position = 0;
			foreach (@filelist) {if ($_ eq $FORM{'page'}) {last;} ++$position;}
			my $position_s = int($position / $PAGEMAX) * $PAGEMAX;
			my $position_e = $position_s + $PAGEMAX - 1; if ($position_e > @filelist - 1) {$position_e = @filelist - 1;}
			if ($position_s > 0) {$pagelist .= ' <a href="'.$CGIURL.'?mode='.$FORM{'mode'}.'&amp;page='.$filelist[$position_s - 1].$query.'" title="Prev">&lt;&lt;</a>';}
			foreach ($position_s .. $position_e) {
				my $disp = $filelist[$_]; if ($disp eq '9999') {$disp = '新着';}
				if ($filelist[$_] eq $FORM{'page'}) {$disp = '<span class="controller_currentpage">'.$disp.'</span>';}
				$pagelist .= ' <a href="'.$CGIURL.'?mode='.$FORM{'mode'}.'&amp;page='.$filelist[$_].$query.'">'.$disp.'</a>';
			}
			if ($position_e < @filelist - 1) {$pagelist .= ' <a href="'.$CGIURL.'?mode='.$FORM{'mode'}.'&amp;page='.$filelist[$position_e + 1].$query.'" title="Next">&gt;&gt;</a>';}
		}
	}

	#キーワードの取得
	my $query_html = $FORM{'query'};
	&html_encode(\$query_html);

	#操作部のレイアウト
	my $str = '';
	$str .= '<table class="controller_table"><tr>'."\n";
	if ($pagelist) {$str .= '<td>'.$pagelist.'</td>'."\n";}
	
	if ($FORM{'mode'} eq 'search') {
		$str .= '<td><form action="'.$CGIURL.'" method="GET" class="controller_form"><input type="hidden" name="mode" value="list"><input type="hidden" name="page" value="'.$FORM{'page'}.'"><input type="hidden" name="query" value="'.$query_html.'"><input type="submit" value="検索解除" class="controller_cancel"></form></td>'."\n";
	} else {
		$str .= '<td><form action="'.$CGIURL.'" method="GET" class="controller_form"><input type="hidden" name="mode" value="input"><input type="hidden" name="sid" value="'.&sessionid().'"><input type="submit" value="ご質問・ご相談" class="controller_post"></form></td>'."\n";
	}
	$str .= '</tr></table>'."\n\n";

	return($str);
}

####################
# 記事のレイアウト
# In:	$id,$parent,$root,$depth,$ip,$key,$comment,$author
# Out:	$str
####################
sub layout {
	my ($id,$parent,$root,$depth,$ip,$key,$comment,$author) = @_;
	if (!defined($comment)) {$comment = '';}
	if (!defined($author)) {$author = '';}
	if ($CGIURL =~ /^http:/i) {$comment =~ s/http:\/\/CGIURL/$CGIURL/g;}
	else {my $cgiurl = ("http://$ENV{'HTTP_HOST'}$ENV{'REQUEST_URI'}" =~ /^(.+?\.cgi)/i)[0]; $comment =~ s/http:\/\/CGIURL/$cgiurl/g;}

	#キーワードのハイライト
	if ($FORM{'query'} ne '') {
		my @tag = (); $comment =~ s/(<.*?>)/push(@tag,$1);"\a";/eg;	#タグの退避
		&html_decode(\$comment); &html_decode(\$author);
		{
			#キーワードを0Eと0Fで囲む
			my ($comment_pure,$author_pure) = ($comment,$author);
			&tolower(\$comment_pure); &tolower(\$author_pure);
			my @comment_buf = split(/\a/,$comment_pure);
			foreach my $keyword (split(/\s/,$FORM{'query'})) {
				foreach (@comment_buf) {&replace(\$_,$keyword,chr(0x0e).$keyword.chr(0x0f));}
				&replace(\$author_pure,$keyword,chr(0x0e).$keyword.chr(0x0f));
			}
			$comment_pure = join("\a",@comment_buf);
			my ($offset,$length) = (0,0);
			$comment_pure =~ s/(.*?)(\x0e|\x0f|$)/$length = length($1); $offset += length($1); substr($comment,$offset - $length,$length).$2;/eg;
			($offset,$length) = (0,0);
			$author_pure =~ s/(.*?)(\x0e|\x0f|$)/$length = length($1); $offset += length($1); substr($author,$offset - $length,$length).$2;/eg;
			$comment = $comment_pure;
			$author = $author_pure;
		}
		&html_encodex(\$comment); &html_encode(\$author);
		$comment =~ s/\x0e/<span class="topic_highlight">/g; $comment =~ s/\x0f/<\/span>/g;
		$author =~ s/\x0e/<span class="topic_highlight">/g; $author =~ s/\x0f/<\/span>/g;
		$comment =~ s/\a/shift(@tag);/eg;	#タグの復活
	}

	#日時の取得
	my $date = '';
	{
		my ($sec,$min,$hour,$day,$mon,$year) = (localtime($id))[0..5];
		$date = sprintf("%04d/%02d/%02d %02d:%02d",$year+1900,$mon+1,$day,$hour,$min);
		if (time - $id < 3600 * $DATE2) {$date = '<span class="topic_date2">'.$date.'</span>';}
		elsif (time - $id < 3600 * $DATE1) {$date = '<span class="topic_date1">'.$date.'</span>';}
		else {$date = '<span class="topic_date0">'.$date.'</span>';}
	}

	#投稿者の取得
	if ($author) {$author = ' <span class="topic_author">'.$author.'</span>';}

	#先頭行(操作リンク)の作成
	my $control = '';
	if ($FORM{'page'} eq '9999') {
		#日時＋投稿者＋削除修正返信
		$control .= '<div class="topic_control">';
		if ($COOKIE{'admin'}) {
			$control .= '<a href="javascript:if (confirm(\'削除しますか？\')) {location.href=\''.$CGIURL.'?mode=remove&amp;id='.$id.'\'} else {void(0)}">削除</a> ';
		}
		if (($COOKIE{'admin'}) || (($key eq $COOKIE{'key'}) && ($id + $EDITLIMIT * 60 - time > 0))) {
			$control .= '<a href="'.$CGIURL.'?mode=input&amp;id='.$id.'&amp;sid='.&sessionid().'">修正</a> ';
		}
		$control .= '<a href="'.$CGIURL.'?mode=input&amp;parent='.$id.'&amp;sid='.&sessionid().'">返信</a>';
		$control .= '</div>';
		$control .= $date.$author.'<br>';
	}
	else {
		#日時＋投稿者
		$control .= $date.$author.'<br>';
	}

	#レイアウト
	my $str = '';
	$str .= '<a name="'.$id.'"></a>';
	$str .= '<div style="margin-left:'.($depth * $TREEOFFSET).'em;" class="topic_layout">';
	$str .= $control;
	$str .= '<div class="topic_comment">'.$comment.'</div>';
	$str .= '</div>';
	$str .= "\n";

	return($str);
}

##################################################
#################### 文字列系 ####################
##################################################

####################
# Header Contents
# In:	$subject
# Out:	$header
####################
sub header {
	my $str = '';
	$str .= '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">'."\n";
	$str .= '<html lang="ja">'."\n";
	$str .= '<head>'."\n";
	$str .= '	<meta http-equiv="Content-Language" content="ja">'."\n";
	$str .= '	<meta http-equiv="Content-type" content="text/html; charset=Shift_JIS">'."\n";
	$str .= '	<meta http-equiv="Content-Style-Type" content="text/css">'."\n";
	$str .= '	<meta http-equiv="Content-Script-Type" content="text/javascript">'."\n";
	$str .= '	<link rel="stylesheet" type="text/css" href="'.$CSSURL.'">'."\n";
	$str .= '	<title>'.$TITLE.'</title>'."\n";
	$str .= '</head>'."\n";
	$str .= '<body>'."\n";
	$str .= $TOPLAYOUT."\n";
	return($str);
}

####################
# Footer Contents
# In:	none
# Out:	$footer
####################
sub footer {
	my $str = '';
	$str .= '</body>'."\n";
	$str .= '</html>'."\n";
	return($str);
}

####################
# Encode HTML
# In:	\$str
# Out:	none
####################
sub html_encode {
	my ($str) = @_;
	if (!$$str) {return(0);}
	$$str =~ s/&/&amp;/g;
	$$str =~ s/</&lt;/g;
	$$str =~ s/>/&gt;/g;
	$$str =~ s/"/&quot;/g;
	return(0);
}

####################
# Encode HTML for Textarea
# In:	\$str
# Out:	none
####################
sub html_encodex {
	my ($str) = @_;
	if (!$$str) {return(0);}
	$$str =~ s/&/&amp;/g;
	$$str =~ s/</&lt;/g;
	$$str =~ s/>/&gt;/g;
	$$str =~ s/"/&quot;/g;
	$$str =~ s/^ /&nbsp;/mg;
	$$str =~ s/  / &nbsp;/g;
	$$str =~ s/\n/<br>/g;
	return(0);
}

####################
# Decode HTML
# In:	\$str
# Out:	0
####################
sub html_decode {
	my ($str) = @_;
	if (!$$str) {return(0);}
	$$str =~ s/<br>/\n/g;
	$$str =~ s/<.*?>//g;
	$$str =~ s/&nbsp;/ /g;
	$$str =~ s/&quot;/"/g;
	$$str =~ s/&gt;/>/g;
	$$str =~ s/&lt;/</g;
	$$str =~ s/&amp;/&/g;
	return(0);
}

####################
# SessionID
# In:	none
# Out:	$sid
####################
sub sessionid {
	my $sid = 'AA';
	if ($ENV{'REMOTE_ADDR'} =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) {$sid = substr(crypt(sprintf("%02X%02X%02X%02X",$1,$2,$3,$4),$sid),2);}
	if (-e $DATADIR.'9999.cgi') {$sid = substr(crypt(sprintf("%04X",(stat($DATADIR.'9999.cgi'))[9] % 65536),$sid),2);}
	$sid = substr($sid,5);
	$sid =~ s/\W/_/g;
	return('ID'.$sid);
}

####################
# SessionID for Admin
# In:	none
# Out:	$sid
####################
sub sessionid_admin {
	my $sid = 'AA';
	if ($ENV{'REMOTE_ADDR'} =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) {$sid = substr(crypt(sprintf("%02X%02X%02X%02X",$1,$2,$3,$4),$sid),2);}
	if (-e $DATADIR.'pass.lock') {$sid = substr(crypt(sprintf("%04X",(stat($DATADIR.'pass.lock'))[9] % 65536),$sid),2);}
	$sid = substr(crypt($PASS,$sid),5);
	$sid =~ s/\W/_/g;
	return($sid);
}

####################
# 半角の大文字を小文字へ変換(Shift-JIS専用)
# In:	\$str
# Out:	0
####################
sub tolower {
	my ($str) = @_;
	$$str =~ s/(([\x81-\x9f\xe0-\xfc].)*)([\x00-\x7f\xa1-\xdf]*)/$1.lc($3)/eg;
	return(0);
}

####################
# 無駄空白/改行/タブの削除(Shift-JIS専用)
# In:	\$str
# Out:	0
####################
sub trim {
	my ($str) = @_;
	my $regex_limit = 32766; if (defined($ENV{'MOD_PERL'})) {$regex_limit = 800;}
	$$str =~ s/\G((?:[\x81-\x9f\xe0-\xfc].|[\x00-\x7f\xa1-\xdf]){0,$regex_limit}?)(?:　)/$1 /g;
	$$str =~ s/\s+/ /g;
	$$str =~ s/^ //;
	$$str =~ s/ $//;
	return(0);
}

####################
# 文字列を切り取る(Shift-JIS専用)
# In:	\$str,$length
# Out:	0
####################
sub fold {
	my ($str,$length) = @_;
	if (length($$str) <= $length) {return(0);}
	my $regex_limit = 32766; if (defined($ENV{'MOD_PERL'})) {$regex_limit = 800;}
	substr($$str,0,$length) =~ /(^([\x81-\x9f\xe0-\xfc].|[\x00-\x7f\xa1-\xdf]){0,$regex_limit})/;
	$$str = $1;
	return(0);
}

####################
# 文字列を置き換える(Shift-JIS専用)
# In:	\$str,$pattern,$replace
# Out:	ヒット数
####################
sub replace {
	my ($str,$pattern,$replace) = @_;
	$pattern = quotemeta($pattern);
	my $hit = 0;
	my $regex_limit = 32766; if (defined($ENV{'MOD_PERL'})) {$regex_limit = 800;}
	$hit += $$str =~ s/\G((?:[\x81-\x9f\xe0-\xfc].|[\x00-\x7f\xa1-\xdf]){0,$regex_limit}?)(?:$pattern)/$1$replace/g;
	if (!$hit) {return(0);} else {return($hit);}
}

####################
# 全角の英数字を半角へ変換(Shift-JIS専用)
# In:	\$str
# Out:	0
####################
sub z2h {
	my ($str) = @_;
	my ($result,$c1,$c2,$pos) = ('',0xff,0,0);
	while ($c1) {
		$c1 = ord(substr($$str,$pos,1)); $pos++;
		if (((0x81 <= $c1) && ($c1 <= 0x9f)) || ((0xe0 <= $c1) && ($c1 <= 0xfc))) {
			$c2 = ord(substr($$str,$pos,1)); $pos++;
			if ($c1 == 0x82) {
				if ((0x4f <= $c2) && ($c2 <= 0x58)) {$result .= chr($c2 - 0x1f); next;}
				if ((0x60 <= $c2) && ($c2 <= 0x79)) {$result .= chr($c2 - 0x1f); next;}
				if ((0x81 <= $c2) && ($c2 <= 0x9a)) {$result .= chr($c2 - 0x20); next;}
			}
			$result .= chr($c1).chr($c2); next;
		}
		if ($c1) {$result .= chr($c1);}
	}
	$$str = $result;
	return(0);
}

##################################################
#################### 入出力系 ####################
##################################################

####################
# メッセージ表示
# In:	@_
# Out:	none
####################
sub message {
	print 'Content-type: text/html; charset=Shift-JIS'."\n\n";
	print '<html lang="ja"><head><meta http-equiv="Content-type" content="text/html; charset=Shift_JIS"><link rel="stylesheet" type="text/css" href="'.$CSSURL.'"></head><body>';
	print @_;
	print '<br><a href="'.$CGIURL.'?'.time.'" class="list_link">新着一覧へ</a></body></html>';
	return(0);
}

####################
# ロックOn
# In:	none
# Out:	0 or Error Message
####################
sub lock {
	if (-e $DATADIR.'file.lock') {
		if ((my $wait = (stat($DATADIR.'file.lock'))[9] - time + 60) > 0) {return('ロック中です。あと '.$wait.' 秒お待ちください。');}
		if (!rmdir($DATADIR.'file.lock')) {return($!.' '.$DATADIR.'file.lock');}
	}
	if (!mkdir($DATADIR.'file.lock',0666)) {return($!.' '.$DATADIR.'file.lock');}
	return(0);
}

####################
# ロックOff
# In:	none
# Out:	0 or Error Message
####################
sub unlock {
	if (!rmdir($DATADIR.'file.lock')) {return($!.' '.$DATADIR.'file.lock');}
	return(0);
}

####################
# ロック(パスワード用)
# In:	none
# Out:	0 or Error Message
####################
sub passlock {
	if (-e $DATADIR.'pass.lock') {
		if ((my $wait = (stat($DATADIR.'pass.lock'))[9] - time + 15) > 0) {return('パスワードロック中です。あと '.$wait.' 秒お待ちください。');}
		if (!rmdir($DATADIR.'pass.lock')) {return($!.' '.$DATADIR.'pass.lock');}
	}
	if (!mkdir($DATADIR.'pass.lock',0666)) {return($!.' '.$DATADIR.'pass.lock');}
	return(0);
}

####################
# Cookieを取得
# In:	\%cookie
# Out:	none
####################
sub get_cookie {
	my ($cookie) = @_;
	%$cookie = ();
	my $str = '';
	if (defined($ENV{'HTTP_COOKIE'})) {$ENV{'HTTP_COOKIE'} =~ /PureTree=([^;]*)/; $str = $1;}
	foreach (split(/:/,$str)) {
		s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
		s/[\x00-\x1f]//g;
		if (/^(\w+)=(.+)/) {$$cookie{$1} = $2;}
	}
	return(0);
}

####################
# Cookieヘッダを返す
# In:	\%cookie
# Out:	Set-Cookie
####################
sub set_cookie {
	my ($cookie) = @_;
	my $str = '';
	foreach (keys %$cookie) {
		my ($para,$value) = ($_,$$cookie{$_});
		if ($value eq '') {next;}
		$value =~ s/(\W)/sprintf("%%%02X",ord($1))/eg;
		if ($str) {$str .= ':';}
		$str .= $para.'='.$value;
	}
	my $expires = '';
	{
		my @month = ('Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec');
		my @weeks = ('Sun','Mon','Tue','Wed','Thu','Fri','Sat');
		my ($sec,$min,$hour,$day,$mon,$year,$week) = (gmtime(time+86400 * 30))[0..6];	#有効期限は30日間
		$expires = sprintf("%s, %02d-%s-%04d %02d:%02d:%02d GMT",$weeks[$week],$day,$month[$mon],$year+1900,$hour,$min,$sec);
	}
	return("Set-Cookie: PureTree=$str; expires=$expires;\n");
}

####################
# フォームを取得
# In:	\%form
# Out:	0 or Error Message
####################
sub get_form {
	my ($form) = @_;
	%$form = ();
	#Check CONTENT_LENGTH
	my $remain = 0;
	if (defined($ENV{'CONTENT_LENGTH'})) {$remain = $ENV{'CONTENT_LENGTH'};}
	if ($remain =~ /\D/) {return('Bad CONTENT_LENGTH');}
	if ($remain > 102400) {return('Over CONTENT_LENGTH');}
	#Load STDIN
	my $buf = '';
	while ($remain > 0) {
		my $data = ''; my $size = read(STDIN,$data,$remain);
		$buf .= $data; $remain -= $size;
		if (!$size) {return('No Receive');}
	}
	if ($remain < 0) {return('Over Receive');}
	#Set form
	if (defined($ENV{'QUERY_STRING'})) {$buf .= '&'.$ENV{'QUERY_STRING'};}
	foreach (split(/\&/,$buf)) {
		my ($para,$value) = ('','');
		if (/^(\w+?)=(.*)$/) {$para = $1; $value = $2;} else {next;}
		$value =~ tr/+/ /;
		$value =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
		$$form{$para} = $value;
	}
	#Sanitize
	foreach (keys %$form) {
		$$form{$_} =~ s/\x0d\x0a/\n/g;
		$$form{$_} =~ tr/\x0d\x0a/\n\n/;
		$$form{$_} =~ s/[\x00-\x09\x0b-\x1f]//g;
	}
	return(0);
}

####################
# データの読み込み
# In:	\@data,$page
# Out:	0 or Error Message
####################
sub load_data {
	my ($data,$page) = @_;
	if (!open(IN,'<'.$DATADIR.sprintf("%04d.cgi",$page))) {return(0);}
	@$data = <IN>;
	close(IN);
	chomp(@$data);
	if (defined($$data[0]) && ($$data[0] =~ /([\r|\n]+)$/)) {my $backup = $/; $/ = $1; chomp(@$data); $/ = $backup;}	#chomp漏れチェック
	return(0);
}

####################
# データの書き込み
# In:	\@data
# Out:	0 or Error Message
####################
sub save_data {
	my ($data) = @_;
	#過去ログ処理
	if ((-e $DATADIR.'9999.cgi') && ((stat($DATADIR.'9999.cgi'))[7] > $LOGMAX * 1024)) {
		#最終スレッドを抜き出す
		my @thread = ();
		{
			my ($offset,$flag) = (0,0);
			foreach (reverse @$data) {
				--$offset;
				if ($_ !~ /^(\d{10})\t(\d{10})\t(\d{10})\t(\d+)\t(\d+\.\d+\.\d+\.\d+)\t(\w{10})\t(.*?)\t(.*?)$/) {$offset = 0; last;}	#過去ログ機能一時停止
				my ($id,$root) = ($1,$3);
				if ($flag == 0) {$flag = $root;} elsif ($root != $flag) {++$offset; last;}
			}
			if ($offset < 0) {@thread = splice(@$data,$offset);}
		}
		#大量投稿チェック(最終スレッドが新着時間1を経過していない場合は過去ログに移動しない＆投稿規制)
		if ((my $past = 3600 * $DATE1 - time + ($thread[0] =~ /^(\d{10})/)[0]) > 0) {return('ただいま投稿規制中です。投稿量が制限値を超えました。<br>規制は '.sprintf("%.1f",$past/3600).' 時間後に解除されます。');}
		#過去ログへ書き込み
		if (@thread > 0) {
			#最終ログ番号の取得
			my $log_no = 0;
			{
				my @filelist = ();
				if (!opendir(DIR,$DATADIR)) {return($!.' '.$DATADIR);}
				foreach (sort readdir(DIR)) {if ($_ =~ /^(\d{4})\.cgi$/) {push(@filelist,$1);}}
				closedir(DIR);
				if ($#filelist > 0) {$log_no = $filelist[$#filelist - 1];}
			}
			#最終ログの読み込み
			my @log = ();
			if ((-e $DATADIR.sprintf("%04d.cgi",$log_no)) && ((stat($DATADIR.sprintf("%04d.cgi",$log_no)))[7] < $LOGMAX * 1024)) {
				if (my $error = &load_data(\@log,$log_no)) {return($error);}
			} else {$log_no++;}
			#最終ログへ書き込み
			unshift(@log,@thread);
			if (!open(OUT,'>'.$DATADIR.sprintf("%04d.cgi",$log_no))) {return($!.' '.$DATADIR.sprintf("%04d.cgi",$log_no));}
			foreach (@log) {print OUT $_."\n";}
			close(OUT);
		}
	}
	#データの書き込み
	if (!open(OUT,'>'.$DATADIR.'9999.cgi')) {return($!.' '.$DATADIR.'9999.cgi');}
	foreach (@$data) {print OUT $_."\n";}
	close(OUT);
	return(0);
}
