# UBB common I/O and thread data handling routines


sub CreateLastTimeFiles {
	my $forum = shift;
	unless($forum =~ m/^\d{1,}$/) {
		&StandardHTML("Not a forum number: '$forum'");
		exit(0);
	}
	my @topics;

	my @forumfacts = &GetForumRecord($forum);

	my $this_forum_topics = &GetForumTopics($forum);
	%forum_topics = %$this_forum_topics;

	&SetExactPath($forum);

	if ($forumfacts[15] eq 'abc') {
		@topics = sort { $forum_topics{$a} cmp $forum_topics{$b} } keys %forum_topics;
	} else {
		@topics = sort { $forum_topics{$b} <=> $forum_topics{$a} } keys %forum_topics;
	}

	my $lastnumber = shift @topics;
	unshift (@topics, $lastnumber);

	if (!$lastnumber) {

		#whoa, no threads?
		&WriteFileAsString("$vars_config{NonCGIPath}/$exact_path/lasttime.file",   "\n");
		&WriteFileAsString("$vars_config{NonCGIPath}/$exact_path/lastnumber.file", "\n");
		return;
	}

	my @tdata = &GetThreadData($forum, $lastnumber);

	my @lasttime = ($tdata[12], $tdata[13]);
	&WriteFileAsArray("$vars_config{NonCGIPath}/$exact_path/lasttime.file", @lasttime);

	my ($total_posts, $total_topics) = &GetTotalTopicsPosts($forum);
	$total_posts += $total_topics;

	my $pub_name = $tdata[15] || $tdata[11];
	my $icon     = $tdata[17]  || 1;

	my $reallast = (reverse(sort(@topics)))[0];

	my @lastnumb = ($reallast, $total_topics, $total_posts, $tdata[3], $pub_name, $icon, $lastnumber);
	&WriteFileAsArray("$vars_config{NonCGIPath}/$exact_path/lastnumber.file", @lastnumb);

}

sub UpdateForumTopics {
	my $forum     = shift;
	my $topicdata = shift;
	my $dontwrite = shift;
	my $called = "(called by " . (caller)[1] . ", " . (caller)[2] . ")";

	$forum_threads->{$forum} = $topicdata;

	&SetExactPath($forum);
	my $path = "$vars_config{NonCGIPath}/$exact_path/forum_$forum.threads";

	if(-e $path) { unlink($path) or die "$! deleting $path $called"; }
	&WriteHashToFile($path, "forum_topics", $topicdata) unless $dontwrite;
}

sub GetThreadData {

	my ($forum, $topic) = @_;

	my $called = "(called by " . (caller)[1] . ", " . (caller)[2] . ")";
	die "Not passed a valid topic: '$topic', '$forum'\n$called\n" unless $topic =~ m/^(\d{4})(\d{2})$/;
	my ($lefty, $righty) = ($1, $2);

	unless (exists $forum_thread_data{$forum}->{$lefty}) {
		&SetExactPath($forum);
		if (!-e "$vars_config{NonCGIPath}/$exact_path/forum_thread_data_$lefty.cgi") {
			if (!-e "$vars_config{NonCGIPath}/$exact_path/upgrade.txt") {
				&StandardHTML("$vars_wordlets_err{no_forum_thread_data_files} $forum");
				exit;
			}
			&StandardHTML("$vars_wordlets_err{no_forum_thread_data} $lefty, $righty, $forum");
			exit;
		}

		&RequireVars("$vars_config{NonCGIPath}/$exact_path/forum_thread_data_$lefty.cgi");    #locks
	}

	return @{$forum_thread_data{$forum}->{$lefty}->{$righty}};
}


sub SafeGetThreadData {

	#like GetThreadData, only just loads the file into memory
	#doesn't return any data
	#won't die if there's no file
	my ($forum, $topic) = @_;

	my $called = "(called by " . (caller)[1] . ", " . (caller)[2] . ")";
	die "Not passed a valid topic: '$topic', '$forum'\n$called\n" unless $topic =~ m/^(\d{4})(\d{2})$/;
	my ($lefty, $righty) = ($1, $2);

	&SetExactPath($forum);
	unless (exists $forum_thread_data{$forum}->{$lefty}) {
		if (!-e "$vars_config{NonCGIPath}/$exact_path/forum_thread_data_$lefty.cgi") {
			return;
		}

		&RequireVars("$vars_config{NonCGIPath}/$exact_path/forum_thread_data_$lefty.cgi");    #locks
	}

	return;
}

sub GetTotalTopicsPosts {
	my $forum = shift;

	my ($totalposts, $totaltopics);

	&SetExactPath($forum);

	opendir(DIR, "$vars_config{NonCGIPath}/$exact_path") or die $!;
	my @datafiles = grep(/^forum_thread_data_\d{4}\.cgi$/, readdir(DIR));
	closedir(DIR);

	foreach (@datafiles) {
		&RequireVars("$vars_config{NonCGIPath}/$exact_path/$_");    #locks
	}

	foreach my $thisentry (sort keys %{$forum_thread_data{$forum}}) {
		foreach my $entry (sort keys %{$forum_thread_data{$forum}->{$thisentry}}) {
			$forum_thread_data{$forum}->{$thisentry}->{$entry}[5] = 0
				unless $forum_thread_data{$forum}->{$thisentry}->{$entry}[5];
			$totaltopics++;
			$totalposts += $forum_thread_data{$forum}->{$thisentry}->{$entry}[5];
		}    #endforeach
	}    #endforeach

	return ($totalposts, $totaltopics);
}    #endsub


sub ClearMetaData {
	my $forum = shift;
	my $called = "(called by " . (caller)[1] . ", " . (caller)[2] . ")";

	$exact_path = &SetExactPath($forum);

	opendir(DIR, "$vars_config{NonCGIPath}/$exact_path") or die "$! $called";
	my @data = grep(/forum_thread_data/, readdir(DIR));
	closedir(DIR);

	foreach(@data) {
		unlink "$vars_config{NonCGIPath}/$exact_path/$_" or die "Can't unlink $_: $!\n$called";
	}

	return;
}


sub NiceForumThreadsRebuild {
	my $forum = shift;

	my (%thistopics);

	&SetExactPath($forum);

	opendir(DIR, "$vars_config{NonCGIPath}/$exact_path") or die "$! opening $vars_config{NonCGIPath}/$exact_path";
	my @thisforum = readdir(DIR);
	my @datafiles = grep(/^forum_thread_data_\d{4}\.cgi$/, @thisforum);
	my @cgifiles  = grep(/^\d{6}\.cgi$/, @thisforum);
	closedir(DIR);

	if ((!-e "$vars_config{NonCGIPath}/$exact_path/upgrade.txt") && ($cgifiles[0] =~ m/\d{6}\.cgi/) && ($in{ubb} !~ m/stats/)) {

		# no upgrade.txt -> no rebuild yet!
		&StandardHTML("$vars_wordlets_err{no_forum_thread_data_files} $forum");
		exit;
	}

	my $excounter = 0;
	foreach (@datafiles) {
		$excounter++;

		&RequireVars("$vars_config{NonCGIPath}/$exact_path/$_");    #locks!
	}

	if (($excounter < 1) && ($cgifiles[0])) {

		# .cgi files but no forum_thread data -> no rebuild yet!
		&StandardHTML("$vars_wordlets_err{no_forum_thread_data_files} $forum");
		exit;
	}

	my @forumfacts = &GetForumRecord($forum);

	foreach my $firstfour (keys %{$forum_thread_data{$forum}}) {
		foreach my $lasttwo (keys %{$forum_thread_data{$forum}->{$firstfour}}) {
			my @threaddata = @{$forum_thread_data{$forum}->{$firstfour}->{$lasttwo}};

			if(($forumfacts[15]) && ($forumfacts[15] eq 'abc')) {
				$thistopics{"$firstfour$lasttwo"} = $threaddata[3];
			} else {
				$thistopics{"$firstfour$lasttwo"} = $threaddata[14];
			}

		}    #endforeach
	}    #endforeach

	my $refer = \%thistopics;

	return ($refer);

}    #endsub

sub ForumSanityCheck {
	my $forum = shift;

	my (%thistopics, $returnval);

	$returnval = 0;

	&SetExactPath($forum);

	opendir(DIR, "$vars_config{NonCGIPath}/$exact_path") or die "$! opening $vars_config{NonCGIPath}/$exact_path";
	my @thisforum = readdir(DIR);
	my @datafiles = grep(/^forum_thread_data_\d{4}\.cgi$/, @thisforum);
	my @cgifiles  = grep(/^\d{6}\.cgi$/, @thisforum);
	closedir(DIR);

	if (((scalar(@datafiles) < 1) && (scalar(@cgifiles) > 0)) || (!-e "$vars_config{NonCGIPath}/$exact_path/upgrade.txt")) {

		# .cgi files but no forum_thread data || no upgrade.txt -> no rebuild yet!
		$returnval = 1;
	}

	return $returnval;
}    #endsub


sub GetForumTopics {
	my $forum = shift;
	if ($forum_threads->{$forum}) { return $forum_threads->{$forum}; }
	local (%forum_topics);    #use our own copy rather than make the required file global

	&SetExactPath($forum);
	my $full_path = "$vars_config{NonCGIPath}/$exact_path/forum_$forum.threads";

	unless (-e $full_path) {
		&GenerateDotThreadsFile($forum);
	} else {

		# string terminators will never bother us again!  BUAHAHAHA!
		my $result = eval {
			local $SIG{'__DIE__'} = sub { return; };
			local $SIG{'__WARN__'} = sub { return; };
			my $token = &OpenAndLock($full_path);
			do $full_path;
			&CloseAndUnlock($token);
		};

		if($@ || !$result || !%forum_topics) {	#if the eval erred
			&BackupFile($forum, "forum_$forum.threads", &OpenFileAsVar($full_path));
			my $this_topics = &NiceForumThreadsRebuild($forum);
			&UpdateForumTopics($forum, $this_topics);	# aka $forum_threads->{$forum} = $this_topics
		} else {
			$forum_threads->{$forum} = \%forum_topics;
		}

	}

	return ($forum_threads->{$forum});
}

sub GenerateDotThreadsFile {
	my ($forum) = shift;
	my $this_topics = &NiceForumThreadsRebuild($forum);
	&UpdateForumTopics($forum, $this_topics);
}


sub WriteForumThreadData {
	my ($forum, $number) = @_;
	my $called = "(called by " . (caller)[1] . ", " . (caller)[2] . ")";

	&SetExactPath($forum);

	my $string = qq!\n\$forum_thread_data{"$forum"}->{"$number"} = {\n!;

	THIS: foreach my $entry (sort keys %{$forum_thread_data{$forum}->{$number}}) {
		next THIS unless @{$forum_thread_data{$forum}->{$number}->{$entry}}[0] eq "A";
		$string .= qq!\t'$entry' => [!;
		my $thiscount = 0;
		foreach my $item (@{$forum_thread_data{$forum}->{$number}->{$entry}}) {
			chomp($item);
			$string .= qq!q~! . &SmallClean2($item) . q!~, !;
			$string .= qq!\n\t\t! if ($thiscount == 3);
			$thiscount == 3 ? $thiscount = 0 : $thiscount++;
		}    #endforeach
		$string .= qq!],\n!;
	}    #endforeach

	$string .= qq!}\;\n1\;\n\n\n!;

	#unlink("$vars_config{NonCGIPath}/$exact_path/forum_thread_data_$number.cgi") or die "$! $called";
	&WriteFileAsString("$vars_config{NonCGIPath}/$exact_path/forum_thread_data_$number.cgi", $string);

	return;
}

sub WriteAllForumThreadData {
	my $forum = shift;

	&SetExactPath($forum);

	foreach my $entry (sort keys %{$forum_thread_data{$forum}}) {
		next unless $entry =~ m/^\d{4}$/;    #jic
		&WriteForumThreadData($forum, $entry);
	}    #endforeach
}


sub UpdateForumThreadDataForSingleThreadAndWrite {
	my ($forum, $thread, $dataref) = @_;
	my $called = "(called by " . (caller)[1] . ", " . (caller)[2] . ")";

	die "What in the world is '$thread'?\n$called" unless $thread =~ m/^(\d{4})(\d{2})$/;
	my ($lefty, $righty) = ($1, $2);

	&SetExactPath($forum);

	my $fullpath = "$vars_config{NonCGIPath}/$exact_path";

	@data2 = @$dataref;

	&SafeGetThreadData($forum, $thread);           #load it again, if it hasn't been retrieved
	&UpdateForumThreadDataForSingleThread($forum, $lefty, $righty, @data2);
	&WriteForumThreadData($forum, $lefty);

}    #endsub

sub UpdateForumThreadDataForSingleThread {
	my ($forum, $leftnum, $rightnum, @thread) = @_;
	my $called = "(called by " . (caller)[1] . ", " . (caller)[2] . ")";

	die "Wasn't passed a valid A line:\n$thread[0]\n$called" unless $thread[0] =~ m/^A/;

	@Aline      = split (/\|\|/, shift (@thread));
	@firstZline = split (/\|\|/, shift (@thread));
	if ($thread[0]) {
		@lastZline = split (/\|\|/, pop (@thread));
	} else {
		@lastZline = @firstZline;
	}
	@thread = undef;

	foreach (0 .. 15) { $Aline[$_]      = "" unless $Aline[$_];      chomp $Aline[$_]; }
	foreach (0 .. 15) { $firstZline[$_] = "" unless $firstZline[$_]; chomp $firstZline[$_]; }
	foreach (0 .. 15) { $lastZline[$_]  = "" unless $lastZline[$_];  chomp $lastZline[$_]; }

	my $firstjul = &ConvertPostTimetoJulian($firstZline[3], $firstZline[4]);
	my $lastjul  = &ConvertPostTimetoJulian($lastZline[3],  $lastZline[4]);

	my $goodarray = [$Aline[0], $Aline[1], $Aline[3], $Aline[4],
	($firstZline[9]||1), $Aline[2], $firstZline[3], $firstZline[4],
	$firstjul, $Aline[8], $Aline[9], $lastZline[2],
	$lastZline[3], $lastZline[4], $lastjul, $lastZline[10],
	$lastZline[11], ($lastZline[9]||1)];

	$forum_thread_data{$forum}->{$leftnum}->{$rightnum} = $goodarray;

	return;
}    #3ndsub


sub WriteHashToFile {    #writes the data from a hash to a file
	local (*FILE);
	my ($path, $hashname, $hashref) = @_;
	my %hash = %$hashref;

	my $called = "(called by " . (caller)[1] . ", " . (caller)[2] . ")";

	open(FILE, ">$path") or die "$! opening $path $called";
	&lock;
	print FILE "%" . $hashname . " = (\n";

	foreach my $key (sort keys %hash) {
		print FILE "q~$key~ => q~" . &SmallClean2($hash{$key}) . "~,\n";
	}

	print FILE ")\;\n1\;\n";
	&unlock;
	close FILE;
	chmod(0666, "$path") if $path !~ m/\.cgi$/;
	chmod(0777, "$path") if $path =~ m/\.cgi$/;

	&DieIfZeroSize("$path", "$path $called");

	return;
}    #endsub

sub WriteHashesToFile {    #writes hasheS to a single file
	local (*FILE);
	my ($path, $nameref, $hashrefs) = @_;

	my $called = "(called by " . (caller)[1] . ", " . (caller)[2] . ")";

	open(FILE, ">$path") or die "$! opening $path $called";
	&lock;

	my $outcount = 0;
	foreach my $name (@{$nameref}) {
		my %hash = %{$hashrefs->[$outcount]};

		print FILE "%" . $name . " = (\n";
		foreach my $key (sort keys %hash) {
			print FILE "q~$key~ => q~" . &SmallClean2($hash{$key}) . "~,\n";
		}
		print FILE ")\;\n\n";

		$outcount++;
	}    #endforeach

	print FILE "\n1\;\n";
	&unlock;
	close FILE;

	chmod(0666, "$path") if $path !~ m/\.cgi$/;
	chmod(0777, "$path") if $path =~ m/\.cgi$/;

	&DieIfZeroSize("$path", "$path $called");

	return;
}    #endsub

sub WriteFileAsHashKV {    #writes to a file using a key|value method
	local (*FILE);
	my ($path, $delimiter, $hashref) = @_;
	my %hash = %$hashref;

	my $called = "(called by " . (caller)[1] . ", " . (caller)[2] . ")";

	my $countish;

	open(FILE, ">$path") or die "$! opening $path $called";
	&lock;
	foreach my $key (sort keys %hash) {
		$countish++;
		print FILE "$key$delimiter$hash{$key}\n";
	}
	&unlock;
	close FILE;
	chmod(0666, "$path") if $path !~ m/\.cgi$/;
	chmod(0777, "$path") if $path =~ m/\.cgi$/;


	&DieIfZeroSize("$path", "$path $called") unless $countish  < 1;

	return;
}    #endsub

sub WriteFileAsHashVK {    #writes to a file using a value|key method
	local (*FILE);
	my ($path, $delimiter, $hashref) = @_;
	my %hash = %$hashref;

	my $called = "(called by " . (caller)[1] . ", " . (caller)[2] . ")";

	my $countish;
	open(FILE, ">$path") or die "$! opening $path $called";
	&lock;
	foreach my $key (sort keys %hash) {
		$countish++;
		print FILE "$hash{key}$delimiter$key\n";
	}
	&unlock;
	close FILE;
	chmod(0666, "$path") if $path !~ m/\.cgi$/;
	chmod(0777, "$path") if $path =~ m/\.cgi$/;

	&DieIfZeroSize("$path", "$path $called") if $countish > 0;

	return;
}    #endsub

sub WriteFileAsArray {    #writes an array to a file
	local (*FILE);
	my ($path, @array) = @_;

	my $called = "(called by " . (caller)[1] . ", " . (caller)[2] . ")";

	open(FILE, ">$path") or die "$! opening $path $called";
	&lock;
	foreach (@array) {
		chomp;
		print FILE "$_\n";
	}
	&unlock;
	close FILE;
	chmod(0666, "$path") if $path !~ m/\.cgi$/;
	chmod(0777, "$path") if $path =~ m/\.cgi$/;

	&DieIfZeroSize("$path", "$path $called") if(($array[0]) && ($array[0] ne ""));

	return;
}    #endsub

sub WriteFileAsString {    #writes a string to a file
	local (*FILE);
	my ($path, $string) = @_;

	my $called = "(called by " . (caller)[1] . ", " . (caller)[2] . ")";

	open(FILE, ">$path") or die "$! writing $path $called";
	&lock;
	print FILE $string;
	&unlock;
	close FILE;
	chmod(0666, "$path") if $path !~ m/\.cgi$/;
	chmod(0777, "$path") if $path =~ m/\.cgi$/;

	&DieIfZeroSize("$path", "$path $called") if ($string);

	return;
}    #endsub

sub AppendFileAsString {    #appends a string to a file
	local (*FILE);
	my ($path, $string) = @_;

	my $called = "(called by " . (caller)[1] . ", " . (caller)[2] . ")";

	open(FILE, ">>$path") or die "$! opening $path $called";
	&lock;
	print FILE $string;
	&unlock;
	close FILE;
	chmod(0666, "$path") if $path !~ m/\.cgi$/;
	chmod(0777, "$path") if $path =~ m/\.cgi$/;

	return;
}    #endsub

sub AppendFileAsArray {    #appends an array to a file
	local (*FILE);
	my ($path, @string) = @_;

	my $called = "(called by " . (caller)[1] . ", " . (caller)[2] . ")";

	open(FILE, ">>$path") or die "$! opening $path $called";
	&lock;
	foreach (@string) {
		chomp;
		print FILE "$_\n";
	}
	&unlock;
	close FILE;
	chmod(0666, "$path") if $path !~ m/\.cgi$/;
	chmod(0777, "$path") if $path =~ m/\.cgi$/;

	return;
}    #endsub

sub sh_lock {
	flock(FILE, LOCK_SH) or die "Can't obtain shared lock: $!";
}    #end shared lock sr

sub lock {
	my $called = "(called by " . (caller)[1] . ", " . (caller)[2] . ")";
	flock(FILE, LOCK_EX) or die "Can't obtain exclusive lock: $!  $called";
}    #end lock sr

sub unlock {
	flock(FILE, LOCK_UN) or die "Can't release lock: $!";
}    # end unlock sr


sub OpenAndLock {    #used to keep a lock on required files
	my $file   = shift;
	my $called = "(called by " . (caller)[1] . ", " . (caller)[2] . ")";
	my $token  = *FHANDLE;

	open($token, "<$file") or die "Can't open $file ($!) $called";
	flock($token, LOCK_SH) or die "Can't lock $file ($!) $called";
	return $token;
}    #endsub


sub CloseAndUnlock {
	my $token  = shift;
	my $called = "(called by " . (caller)[1] . ", " . (caller)[2] . ")";
	flock($token, LOCK_UN) or die "Can't release lock ($!) $called";
	close $token;
}    #endsub

sub OpenFileToSTDOUT {
	local (*FILE);

	my $file   = shift;
	my $called = "(called by " . (caller)[1] . ", " . (caller)[2] . ")";

	my $item;

	open(FILE, "<$file") or die "$! opening $file $called";
	&sh_lock;
	while (<FILE>) { print; $item .= $_; }
	&unlock;
	close FILE;

	&DieIfZeroSize("$file", "$file $called") unless ($item);

	return;
}    #endsub

sub OpenFileAsVar {
	push (@openedfiles, $_[0]);
	local (*FILE);
	local ($str);

	my $called = "(called by " . (caller)[1] . ", " . (caller)[2] . ")";

	if (-e "$_[0]") {
		open(FILE, "$_[0]") or &StandardHTML("$! opening $_[0] for reading. $called");
		&sh_lock;
		while (<FILE>) { $str .= $_; }
		&unlock;
		close(FILE);
	} else {
		&CheckCachedFile($_[0]);
	}
	chomp($str);
	return ($str);
}

sub OpenFileAsString {    # :)
	return &OpenFileAsVar(@_);
}

sub OpenFileAsArray {
	push (@openedfiles, $_[0]);
	local (*FILE);
	local (@thisarray);

	my $called = "(called by " . (caller)[1] . ", " . (caller)[2] . ")";

	if (-e "$_[0]") {
		open(FILE, "<$_[0]") or die "$! reading $_[0] $called";
		&sh_lock;
		@thisarray = <FILE>;
		&unlock;
		close(FILE);
	} else {
		&CheckCachedFile($_[0], $called);
	}
	return (@thisarray);
}

sub OpenFileAsArray2 {
	push (@openedfiles, $_[0]);
	local (*FILE);

	my $called = "(called by " . (caller)[1] . ", " . (caller)[2] . ")";

	open(FILE, "$_[0]") or die "$! reading $_[0] $called";
	&sh_lock;
	my @thisarray = <FILE>;
	&unlock;
	close(FILE);

	return (@thisarray);
}

sub OpenFileAsHash {
	push (@openedfiles, $_[0]);
	local (*FILE);

	my ($path, $delim) = @_;
	my $delimiter = quotemeta($delim);

	my $called = "(called by " . (caller)[1] . ", " . (caller)[2] . ")";

	my %hash;

	open(FILE, "<$path") or die "$! opening $path $called";
	&sh_lock;
	while (<FILE>) {
		chomp;
		my ($left, $right) = split (/$delimiter/, $_);
		$hash{$left} = $right;
	}
	&unlock;
	close FILE;

	my $hashref = \%hash;

	return $hashref;
}    #endsub

sub WriteMemberProfile {
	local (*FILE);
	my ($number, @profile) = @_;

	#$number is the profile number,
	#@profile is a chomped profile array

	my $called = "(called by " . (caller)[1] . ", " . (caller)[2] . ")";



	if(!@profile) {
		&StandardHTML("$vars_wordlets_err{wrote_out_zero_profile} $number $called");
	} elsif(!$profile[0]) {
		&StandardHTML("$vars_wordlets_err{wrote_out_bad_profile} $number $called");
	}

	open(FILE, ">$vars_config{MembersPath}/$number.cgi") or die "Cannot write file '$number' to Members directory: $! $called";
	&lock;
	foreach (@profile) {
		if ($_) {
			chomp if $_ =~ m/[\n\r]$/;
			print FILE "$_\n";
		} else {
			print FILE "\n";
		}

	}
	&unlock;
	close FILE;

	chmod(0777, "$vars_config{MembersPath}/$number.cgi");

	&DieIfZeroSize("$vars_config{MembersPath}/$number.cgi", "$vars_wordlets{user_number} $number $called");

}    #endsub

sub WriteTopic {
	local (*FILE);
	my $forum = shift;
	my $topic = shift;
	my @data = @_;

	&SetExactPath($forum);

	my $called = "(called by " . (caller)[1] . ", " . (caller)[2] . ")";

	if(!@data) {
		&StandardHTML("$vars_wordlets_err{wrote_out_zero_topic} (forum $forum, topic $topic) $called");
	} elsif(
		(!$data[0]) ||
		($data[0] !~ m/^A/) ||
		($data[1] !~ m/^Z/) ||
		($data[(scalar(@data) - 1)] !~ m/^Z/)
		) {
		&StandardHTML("$vars_wordlets_err{wrote_out_bad_topic} (forum $forum, topic $topic) $called");
	}

	unless ($topic =~ /^\d{6}$/) {
		&PostHackDetails("$vars_wordlets_err{hack_attempt_open_topic}", $called);
	}

	&WriteFileAsArray("$vars_config{NonCGIPath}/$exact_path/$topic.cgi", @data);

	&DieIfZeroSize("$vars_config{NonCGIPath}/$exact_path/$topic.cgi", "Thread $topic in forum $forum $called");

	return;
}


sub OpenTopic {

	# $_[0] : topic number
	# $_[1] : forum number
	local (*FILE);

	my $called = "(called by " . (caller)[1] . ", " . (caller)[2] . ")";

	&SetExactPath($_[1]);

	unless ($_[0] =~ /^\d{6}$/) {
		&PostHackDetails("$vars_wordlets_err{hack_attempt_open_topic} ($_[0])");
	}

	die ("Tried to open zero sized thread $_[0] in forum $_[1]") unless -s "$vars_config{NonCGIPath}/$exact_path/$_[0].cgi";

	my @topic_guts = sort(&OpenFileAsArray("$vars_config{NonCGIPath}/$exact_path/$_[0].cgi"));

	if ((($topic_guts[0] !~ m/^A/) || ($topic_guts[1] !~ m/^Z/)) && ($in{ubb} ne "delete_topic")) {

		#no A-line means the thread is dead, no Z-line means it's MORE than dead!
		print "<pre><br />$vars_wordlets_err{thread_file_corrupt}<br />", "  $vars_wordlets{forum_column} $_[1], $vars_wordlets{topic_header} $_[0]  (<a href='$vars_config{CGIURL}/ultimatebb.cgi?ubb=get_topic&f=$_[1]&t=$_[0]'>View</a>)", "<br /><a href='mailto:$vars_display{BBEmail}?Subject=", UBBCGI::escape("$vars_wordlets_err{thread_file_corrupt_email} ($vars_wordlets{forum_column} $_[1], $vars_wordlets{topic_header} $_[0])"), "&Body=", UBBCGI::escape("$vars_wordlets_err{thread_file_corrupt_body}\n$vars_config{CGIURL}/ultimatebb.cgi?ubb=get_topic&f=$_[1]&t=$_[0]"), "'>$vars_wordlets_err{mail_an_admin}</a><br>$called</pre>";
	}    #temporary troubleshooting code - if the thread is corrupt, offer to delete it

	#Some users managed to have living pre-Y2K UBBs with new threads, resulting
	#in malformated date strings.  This routine attempts to fix the string.
	#It's a kuldge, but it does the job for the time being.

	my @clean_topic_guts = ();

	foreach (@topic_guts) {

		my @cleanup = split (/\|\|/) if m/^Z/;    #only Z lines have dates
		unless (@cleanup) { push (@clean_topic_guts, $_); next; }    #so skip non-Z lines
		$cleanup[3] =~ m/^(\d\d)\-(\d\d)\-(\d\d)$/;                  #only catch two digit years
		if ($1 && ($3 > 90) && ($3 < 150)) {			     #y2k+50!
			my $three = $3 + 1900;
			$cleanup[3] = "$1-$2-$three";
		}
		if ($1 && ($3 < 90)) {
			my $three = $3 + 2000;
			$cleanup[3] = "$1-$2-$three";
		}
		my $line = join ("||", @cleanup);
		push (@clean_topic_guts, $line);
	}    # end foreach

	@topic_guts       = @clean_topic_guts;
	@clean_topic_guts = undef;

	#CC Cleanup Code END

	return (@topic_guts);
}    # end OpenTopic


sub OpenProfile {
	local (@got_profile);

	# $_[0] : member profile number
	chomp($_[0]);    # just in case :)

	my $called = "(called by " . (caller)[1] . ", " . (caller)[2] . ")";

	# thanks to Leshrac for the %member_profile hash idea!
	if ($member_profile{"$_[0]"}) {
		return @{$member_profile{"$_[0]"}};
	} else {
		# make sure no lf bumping is going on
		my $maxmemfields = 31;    #total fields permitted in member file

		# make sure input is a number
		if ($_[0] > 0) { @got_profile = &OpenFileAsArray("$vars_config{MembersPath}/$_[0].cgi"); }

		if ((!@got_profile) || ($got_profile[0] eq '')) {
			print header(
				-charset => "$masterCharset",
				-type    => "text/html",
			);

			if(!-e "$vars_config{MembersPath}/$_[0].cgi") {
				&StandardHTML("$vars_wordlets_err{no_member_number} '$_[0]' $called");
			} else {
				&StandardHTML("$vars_wordlets_err{no_member_data} '$_[0]' $called");
			}
		}


		if ($#got_profile > $maxmemfields) {
			&PostHackDetails("$vars_wordlets_err{hack_attempt_member_file} ('$_[0]')");
		}

		$member_profile{"$_[0]"} = [@got_profile];
		return (@got_profile);
	}

}    # end Open Profile



sub DieIfZeroSize {
	&StandardHTML("$vars_wordlets_err{zero_sized_file_written} $_[1]") if -z $_[0];
	return;
}


sub BackupFile {
	my($forum, $fn, @strings) = @_;
	my $string = join("\n", @strings);
	my $prepend = "$vars_config{NonCGIPath}/cache-$vars_config{cache_pw}/backups";
	my $path = "$prepend/Forum$forum/$fn." . &GeneratePasswordCore(7) . ".cgi";

	unless(-d $prepend) {
		mkdir("$vars_config{NonCGIPath}/cache-$vars_config{cache_pw}", 0777);
		mkdir($prepend, 0777) or die "$!: $prepend";
		mkdir("$prepend/Forum$forum", 0777);
	}

	unless(-d "$prepend/Forum$forum") {
		mkdir("$vars_config{NonCGIPath}/cache-$vars_config{cache_pw}", 0777);
		mkdir("$prepend", 0777);
		mkdir("$prepend/Forum$forum", 0777) or die "$!: Forum$forum";
	}

	unless(-d "$prepend/Forum$forum") {
		#die("Can't make $prepend/Forum$forum");
		# screw it - if it's still impossible to create, don't
		$path = $prepend . "forum.$forum." . $fn . "." . &GeneratePasswordCore(7) . ".cgi";
	}

	&WriteFileAsString($path, $string);
}


#Don't touch anything below.  :)

1;
