#!/usr/bin/perl

################################################################################
#
# Gästebuch
#
# (c) 2005 by Stefan Bion
# https://www.stefanbion.de/
#
################################################################################

use strict;

use Time::Local;
use POSIX qw(strftime);
use Time::Piece;
use CGI::Carp qw(fatalsToBrowser);
use Digest::MD5 qw(md5_hex);

#############
# Variablen #
#############

# Template- und DB-Dateien
my $gBaseName = (split(/\./, (split(/\//, $ENV{'SCRIPT_NAME'}))[-1]))[0];
my $gFileNameTemplateHtml = "../$gBaseName.html";
my $gFileNameDatabase = "$gBaseName.db";
my $gFileNameDatabaseTrash = "$gBaseName-trash.db";
my $gFileNameId = "$gBaseName-id.db";

# Interne globale Variablen
my $gPage = 1;
my $gbLoggedIn = 0;
my $gUserName = '';
my $gScriptName = $ENV{'SCRIPT_NAME'};
my @gMessages;	# Puffer für Meldungen

#################
# Hauptprogramm #
#################

#### DB-Dateien erzeugen, falls nicht vorhanden ####

if (! -f $gFileNameDatabase)
{
	open(DB, ">>$gFileNameDatabase");
	close(DB);
}

#### HTML-Template einlesen ####

open(TPL, "$gFileNameTemplateHtml") or die "File not found: $gFileNameTemplateHtml";
my @gTemplateLines = <TPL>;
close(TPL);

# Meldungstexte
my %gMessageStrings;
ReadTemplateStrings('template-message-strings', \%gMessageStrings);

# Benutzer und Passwörter der Administratoren
my %gUsers;
ReadTemplateStrings('template-users', \%gUsers);

# Unerwünschte IP-Adressen
my @gForbiddenIPs;
ReadTemplateStrings('template-forbidden-ips', \@gForbiddenIPs);

# Verbotene Strings in Eingabefeldern (Regular Expressions)
my @gForbiddenStrings;
ReadTemplateStrings('template-forbidden-strings', \@gForbiddenStrings);

# Verbotene (Teil-)Strings im Feld "Name"
my @gForbiddenNames;
ReadTemplateStrings('template-forbidden-names', \@gForbiddenNames);

# einzubindende Stylesheets
my @gStyleSheets;
ReadTemplateStrings('template-stylesheets', \@gStyleSheets);

# CSS-Klassen für Info-, Fehler- und Warnmeldungen
my @gMessageClasses;
ReadTemplateStrings('template-message-classes', \@gMessageClasses);

#### Benutzerdefinierte globale Variablen ####
my %gConstants;
ReadTemplateStrings('template-constants', \%gConstants);
my $gbModerated = exists $gConstants{'Moderated'} ? $gConstants{'Moderated'} : 1; # 0 = unmoderiert, 1 = moderiert (default)
my $gPathNameSendmail = $gConstants{'PathNameSendmail'} || '/usr/lib/sendmail'; # Pfad zum sendmail-Programm
my $gLinesPerPage = $gConstants{'LinesPerPage'} || 10; # Anzahl Einträge pro Seite
my $gDateFormat = $gConstants{'DateFormat'} || '%d.%m.%Y'; # Datumsformat gemäß strftime
my $gTimeFormat = $gConstants{'TimeFormat'} || '%H:%M:%S'; # Uhrzeitformat gemäß strftime
my $gbMakeLinksClickable = exists $gConstants{'MakeLinksClickable'} ? $gConstants{'MakeLinksClickable'} : 0; # URLs im Nachrichtentext in Hyperlinks umwandeln: 0 = nein, 1 = ja

# Template for the Notification Mail
my $gTemplateNotificationMail = ExtractAndReplaceLines(\@gTemplateLines, '<template-notification-email>', '</template-notification-email>', '');

# Abschnitte für die Ausgabe -> @OUTPUT@
my $gTemplateSectionMain = ExtractAndReplaceLines(\@gTemplateLines, '<template-section-main>', '</template-section-main>', '@OUTPUT@');
my $gTemplateSectionEdit = ExtractAndReplaceLines(\@gTemplateLines, '<template-section-edit>', '</template-section-edit>', '');
my $gTemplateSectionPreview = ExtractAndReplaceLines(\@gTemplateLines, '<template-section-preview>', '</template-section-preview>', '');
my $gTemplateSectionConfirm = ExtractAndReplaceLines(\@gTemplateLines, '<template-section-confirm>', '</template-section-confirm>', '');

# Textbausteine für Meldungen -> @MESSAGES@
my $gTemplateMessageLine = ExtractAndReplaceLines(\@gTemplateLines, '<template-message-line>', '</template-message-line>', '@MESSAGE_LINES@');
my $gTemplateMessages = ExtractAndReplaceLines(\@gTemplateLines, '<template-messages>', '</template-messages>', '');

# Textbaustein für Steuerelemente der Hauptübersicht (Blättern, Neuer Eintrag, ...) -> @MAIN_CONTROLS@
my $gTemplateMainControls = ExtractAndReplaceLines(\@gTemplateLines, '<template-main-controls>', '</template-main-controls>', '');

# Textbausteine für die Eintrags-Anzeige -> @SHOW_ENTRIES@, @SHOW_ENTRY@
my $gTemplateEntryHeader = ExtractAndReplaceLines(\@gTemplateLines, '<template-entry-header>', '</template-entry-header>', '@ENTRY_HEADER@');
my $gTemplateEntryHeaderWebsite = ExtractAndReplaceLines(\@gTemplateLines, '<template-entry-header-website>', '</template-entry-header-website>', '');
my $gTemplateEntryLine = ExtractAndReplaceLines(\@gTemplateLines, '<template-entry-line>', '</template-entry-line>', '@ENTRY_LINES@');
my $gTemplateEntryEmail = ExtractAndReplaceLines(\@gTemplateLines, '<template-entry-email>', '</template-entry-email>', '@ENTRY_EMAIL@');
my $gTemplateEntryWebsite = ExtractAndReplaceLines(\@gTemplateLines, '<template-entry-website>', '</template-entry-website>', '@ENTRY_WEBSITE@');
my $gTemplateEntryHeaderComment = ExtractAndReplaceLines(\@gTemplateLines, '<template-entry-header-comment>', '</template-entry-header-comment>', '@ENTRY_HEADER_COMMENT@');
my $gTemplateEntryLineComment = ExtractAndReplaceLines(\@gTemplateLines, '<template-entry-line-comment>', '</template-entry-line-comment>', '@ENTRY_LINES_COMMENT@');
my $gTemplateEntryComment = ExtractAndReplaceLines(\@gTemplateLines, '<template-entry-comment>', '</template-entry-comment>', '@ENTRY_COMMENT@');
my $gTemplateEntryHintApprove = ExtractAndReplaceLines(\@gTemplateLines, '<template-entry-hint-approve>', '</template-entry-hint-approve>', '@ENTRY_HINT_APPROVE@');
my $gTemplateEntryApproveButton = ExtractAndReplaceLines(\@gTemplateLines, '<template-entry-approve-button>', '</template-entry-approve-button>', '@ENTRY_APPROVE_BUTTON@');
my $gTemplateEntryEditButtons = ExtractAndReplaceLines(\@gTemplateLines, '<template-entry-edit-buttons>', '</template-entry-edit-buttons>', '@ENTRY_EDIT_BUTTONS@');
my $gTemplateEntryPreviewButtons = ExtractAndReplaceLines(\@gTemplateLines, '<template-entry-preview-buttons>', '</template-entry-preview-buttons>', '@ENTRY_PREVIEW_BUTTONS@');
my $gTemplateEntryConfirmButton = ExtractAndReplaceLines(\@gTemplateLines, '<template-entry-confirm-button>', '</template-entry-confirm-button>', '@ENTRY_CONFIRM_BUTTON@');
my $gTemplateShowEntry = ExtractAndReplaceLines(\@gTemplateLines, '<template-show-entry>', '</template-show-entry>', '');

# Textbausteine für das Admin-Login -> @ADMIN_LOGIN@
my $gTemplateAdminLogin = ExtractAndReplaceLines(\@gTemplateLines, '<template-admin-login>', '</template-admin-login>', '');
my $gTemplateAdminLogout = ExtractAndReplaceLines(\@gTemplateLines, '<template-admin-logout>', '</template-admin-logout>', '');

# Textbausteine für den Eintrags-Editor -> @EDIT_ENTRY@
my $gTemplateEditHintModerated = ExtractAndReplaceLines(\@gTemplateLines, '<template-edit-hint-moderated>', '</template-edit-hint-moderated>', '@HINT_MODERATED@');
my $gTemplateEditAdminControls1 = ExtractAndReplaceLines(\@gTemplateLines, '<template-edit-admin-controls-1>', '</template-edit-admin-controls-1>', '@ADMIN_CONTROLS_1@');
my $gTemplateEditHintAdminMessage = ExtractAndReplaceLines(\@gTemplateLines, '<template-edit-hint-admin-message>', '</template-edit-hint-admin-message>', '@HINT_ADMIN_EDIT@');
my $gTemplateEditAdminControls2 = ExtractAndReplaceLines(\@gTemplateLines, '<template-edit-admin-controls-2>', '</template-edit-admin-controls-2>', '@ADMIN_CONTROLS_2@');
my $gTemplateEditAdminEntryInfo = ExtractAndReplaceLines(\@gTemplateLines, '<template-edit-admin-entry-info>', '</template-edit-admin-entry-info>', '@ADMIN_ENTRY_INFO@');
my $gTemplateEditEntry = ExtractAndReplaceLines(\@gTemplateLines, '<template-edit-entry>', '</template-edit-entry>', '');

#### Stylesheets einlesen und Inhalt in das HTML-Template einfügen ####

InsertStylesheets();

#### Formulardaten ermitteln ####

my %FORM;
ParseFormData(\%FORM);

#### Cookies ermitteln ####

my %gCookies;
ParseCookies(\%gCookies);

#### Initialisierung ####

if($FORM{'reload2'})
{
	$gPage = ('' ne $FORM{'page2'}) ? $FORM{'page2'} : 1;
}
else
{
	$gPage = ('' ne $FORM{'page'}) ? $FORM{'page'} : 1;
}

#### Falls IP-Adresse des Besuchers in der Liste, diesen zur Startseite umleiten ####

if(CheckIp())
{
	# Benutzer abweisen
	GoHome();
}

#### HTTP-Header ausgeben bzw. Batch-Modus initialisieren ####

PrintHttpHeader();

#### Submit-Buttons und Batch-Kommandos auswerten und entsprechende Aktionen ausführen ####

if($FORM{'add'})
{
	# Eintrag hinzufügen (Werte als CGI-Parameter übergeben)
	AddEntry();
}
elsif($FORM{'new'})
{
	# Neuen Eintrag hinzufügen
	NewEntry();
}
elsif($FORM{'preview'})
{
	# Vorschau des Eintrages
	PreviewEntry();
}
elsif($FORM{'correct'})
{
	# Eintrag korrigieren
	CorrectEntry();
}
elsif($FORM{'prev'})
{
	# Zur vorherigen Seite springen
	$gPage--;
}
elsif($FORM{'next'})
{
	# Zur nächsten Seite springen
	$gPage++;
}
elsif($FORM{'first'})
{
	# Zur vorherigen Seite springen
	$gPage = 1;
}
elsif($FORM{'last'})
{
	# Zur vorherigen Seite springen
	$gPage = 999999999;	# (wird später auf die Anzahl der Seiten gesetzt)
}

if($gbLoggedIn)
{
	my $id = '';

	# Vor Aufruf der entsprechenden Funktionen erst die ID aus dem Element-Namen extrahieren:

	$id = GetIdFromParameter('approve');
	if('' ne $id)
	{
		# Eintrag ändern
		ApproveEntry($id);
	}

	$id = GetIdFromParameter('edit');
	if('' ne $id)
	{
		# Eintrag ändern
		EditEntry($id);
	}

	$id = GetIdFromParameter('delete');
	if('' ne $id)
	{
		# Eintrag löschen
		DeleteEntry($id);
	}
}

DisplayEntries();

exit 0;

##############
# Funktionen #
##############

#
# Überträgt die mittels "POST" an das Formular übergebenen Daten
# in den Hash, dessen Referenz der Funktion übergeben wird. Der
# Zugriff auf die Daten erfolgt dann mit $Hashname{'Feldname'}.
#
sub ParseFormData
{
	my $FormHashRef = shift;

	my $formdata;

	read(STDIN, $formdata, $ENV{'CONTENT_LENGTH'});

	my @pairs = split(/&/, $formdata);

	for my $pair (@pairs)
	{
		my ($name, $value) = split(/=/, $pair);
		$value =~ tr/+/ /;
		$value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/meg;
		${$FormHashRef}{$name} = $value;
	}
}

#
# Überträgt die vom Webserver an das Script übergebenen Cookies
# in den Hash, dessen Referenz der Funktion übergeben wird. Der
# Zugriff auf die Daten erfolgt dann mit $Hashname{'Cookiename'}.
#
sub ParseCookies
{
	my $CookiesHashRef = shift;

	my @cookies = split(/[;,]\s*/, $ENV{'HTTP_COOKIE'});

	for my $pair (@cookies)
	{
		my ($name, $value) = split(/=/, $pair);
		$value =~ tr/+/ /;
		$value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
		${$CookiesHashRef}{$name} = $value;
	}
}

#
# Ausgabe des HTTP-Headers mit Cookies incl. Authentifizierung des Logins mit Benutzername und Paßwort
#
sub PrintHttpHeader
{
	#### Beginn HTTP-Header ####

	# Cache-Control: Der Proxy soll doch bitteschön neue Seiten holen!

	print "Cache-Control: no-cache\n";
	print "Pragma: no-cache\n";

	# HTML-Entsprechung für Cache-Control:
	#
	# <meta http-equiv="cache-control" content="no-cache">
	# <meta http-equiv="pragma" content="no-cache">

	#### Authentifizierung ####

	my $LoginAblaufdatum = MakeCookieExpireDatum(3600);	# Das Login soll nach 1 Stunde Inaktivität ablaufen.

	# Der Benutzer meldet sich ab:
	if($FORM{'logout'})
	{
		DeleteCookie('GbUsername');
		DeleteCookie('GbPassword');
		AddMessage(0, $gMessageStrings{'logged-out'});
	}
	# Der Benutzer meldet sich an:
	elsif($FORM{'login'})
	{
		my $username = $FORM{'user'};
		my $password = $FORM{'password'};

		if(VerifyLogin($username, $password))
		{
			SetCookie('GbUsername', $username, $LoginAblaufdatum);
			SetCookie('GbPassword', md5_hex($password), $LoginAblaufdatum);

			my $text = $gMessageStrings{'logged-in'};
			$text = ReplaceLine($text, '@USER_NAME@', $gUserName);
			AddMessage(0, $text);
		}
		else
		{
			AddMessage(1, $gMessageStrings{'login-failed'});
		}
	}
	# Der Benutzer hatte sich zuvor bereits angemeldet:
	elsif($FORM{'loggedin'})
	{
		my $username = $gCookies{'GbUsername'};
		my $password = $gCookies{'GbPassword'};

		if('' ne $username && '' ne $password)
		{
			if(VerifyLogin($username, $password, 1))
			{
				# Login-Cookies "auffrischen"
				SetCookie('GbUsername', $username, $LoginAblaufdatum);
				SetCookie('GbPassword', $password, $LoginAblaufdatum);
			}
			else
			{
				DeleteCookie('GbUsername');
				DeleteCookie('GbPassword');
				AddMessage(1, $gMessageStrings{'auto-logout'});
			}
		}
		else
		{
			AddMessage(1, $gMessageStrings{'not-logged-in'});
		}
	}

	print "Content-type: text/html; charset=utf-8\n\n";

	#### Ende HTTP-Header ####

#DEBUG:
#print "<p>FORM: ";
#for my $name (keys %FORM) { printf("%s = \"%s\", ", $name, $FORM{$name}); }
#print "<p>$LoginAblaufdatum";

}

#
# Gibt den HTTP-Header-Code zum Setzen eines Cookies aus.
# Parameter 1: Name des Cookies, Parameter 2: Wert des Cookies,
# Parameter 3 (optional): Ablaufdatum des Cookies in Unix-Schreibweise
#
sub SetCookie
{
	my $CookieName = shift;
	my $CookieValue = shift;
	my $CookieExpireDate = shift;

	$CookieValue =~ s/([^A-Za-z0-9])/sprintf("%%%02X", ord($1))/seg;
	$CookieExpireDate = 'Fri, 01-Jan-2038 00:00:00 GMT' if('' eq $CookieExpireDate);

	print "Set-Cookie: $CookieName=$CookieValue; Expires=$CookieExpireDate\n";
}

#
# Gibt den HTTP-Header-Code zum Löschen eines Cookies aus.
# Parameter 1: Name des Cookies
#
sub DeleteCookie
{
	my $CookieName = shift;

	print "Set-Cookie: $CookieName=; Expires=Thu, 01-Jan-1970 00:00:01 GMT\n";
}

#
# Erzeugt ein Expire-Datum für Cookies, das um die Anzahl von Sekunden in der Zukunft
# liegt, die im 1. Parameter angegeben sind.
#
sub MakeCookieExpireDatum
{
	my $Sekunden = shift;

	my @Wochentage = ('Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat');
	my @Monate = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec');
	my ($Sekunde, $Minute, $Stunde, $Tag, $Monat, $Jahr, $Wochentag) = gmtime(time + $Sekunden);
	my $ExpireDate = sprintf("%s, %02d-%s-%04d %02d:%02d:%02d GMT", $Wochentage[$Wochentag], $Tag, $Monate[$Monat], $Jahr + 1900, $Stunde, $Minute, $Sekunde);

	$ExpireDate;
}

#
# Überprüfung des Benutzers (Parameter 1) und des Paßwortes (Parameter 2) auf Gültigkeit.
# Falls das übergebene Paßwort crypted ist, muß als 3. Parameter ein true-Wert übergeben werden.
# Wenn die Überprüfung erfolgreich war, wird $gbLoggedIn = 1 gesetzt und der Benutzername in
# $gUserName gespeichert; der Rückgabewert ist dann true (andernfalls false).
#
sub VerifyLogin
{
	my $User = shift;
	my $Password = shift;
	my $bCrypted = shift;

	my $CryptedPassword = ($bCrypted) ? $Password : md5_hex($Password);

	$gbLoggedIn = 0;
	$gUserName = '';

	if('' ne $User && '' ne $Password && $gUsers{$User} eq $CryptedPassword)
	{
		$gbLoggedIn = 1;
		$gUserName = $User;
	}
	else
	{
		sleep(5); # zur Erschwerung von Brute-Force-Atacken
	}

	$gbLoggedIn;
}

#
# Gibt die Liste der Einträge aus. Wenn ein Benutzer angemeldet ist,
# werden zusätzliche Steuerelemente (Buttons) ausgegeben.
#
sub DisplayEntries
{
	#### Inhalt der DB-Dateien einlesen ####

	open(HANDLE_DB_DISPLAY_READ, "$gFileNameDatabase");
	my @Zeilen = <HANDLE_DB_DISPLAY_READ>;
	close(HANDLE_DB_DISPLAY_READ);

	my $AnzahlZeilen = @Zeilen;
	my $AnzahlSeiten = int(($AnzahlZeilen + $gLinesPerPage - 1) / $gLinesPerPage);
	
	$gPage = $AnzahlSeiten if($gPage > $AnzahlSeiten);
	$gPage = 1 if($gPage < 1);

	#### Überschrift und Einleitungstext ####

	my $htmlOutput = $gTemplateSectionMain;
	$htmlOutput = ReplaceLine($htmlOutput, '@NUM_ENTRIES@', $AnzahlZeilen);
	$htmlOutput = ReplaceLine($htmlOutput, '@PAGE@', $gPage);
	$htmlOutput = ReplaceLine($htmlOutput, '@NUM_PAGES@', $AnzahlSeiten);
	$htmlOutput = ReplaceLine($htmlOutput, '@MESSAGES@', GetHtmlMessages());

	#### Steuerelemente erzeugen ####

	my $PrevPageNo = ($gPage <= 1) ? $gMessageStrings{'btn-prevnext-none'} : ($gPage - 1);
	my $NextPageNo = ($gPage >= $AnzahlSeiten) ? $gMessageStrings{'btn-prevnext-none'} : ($gPage + 1);

	my $htmlDisabledPrev = ($gPage <= 1) ? 'disabled' : '';
	my $htmlDisabledNext = ($gPage >= $AnzahlSeiten) ? 'disabled' : '';

	my $htmlControls = $gTemplateMainControls;
	$htmlControls = ReplaceLine($htmlControls, '@DISABLED_FIRST@', $htmlDisabledPrev);
	$htmlControls = ReplaceLine($htmlControls, '@DISABLED_PREV@', $htmlDisabledPrev);
	$htmlControls = ReplaceLine($htmlControls, '@DISABLED_NEXT@', $htmlDisabledNext);
	$htmlControls = ReplaceLine($htmlControls, '@DISABLED_LAST@', $htmlDisabledNext);

	$htmlControls = ReplaceLine($htmlControls, '@PREV_PAGE@', $PrevPageNo);
	$htmlControls = ReplaceLine($htmlControls, '@NEXT_PAGE@', $NextPageNo);
	$htmlControls = ReplaceLine($htmlControls, '@NUM_PAGES@', $AnzahlSeiten);
	$htmlControls = ReplaceLine($htmlControls, '@PAGE@', $gPage);

	#### Steuerelemente ausgeben ####

	$htmlOutput = ReplaceLineOnce($htmlOutput, '@MAIN_CONTROLS@', ReplaceLine($htmlControls, '@REPEAT@', ''));
	$htmlOutput = ReplaceLineOnce($htmlOutput, '@MAIN_CONTROLS@', ReplaceLine($htmlControls, '@REPEAT@', '2'));

	#### Einträge sortieren ####

	@Zeilen = sort { SortZeilen($a, $b) } @Zeilen;

	#### Einträge zusammenstellen uns ausgeben ####

	my $htmlEntries = '';

	my $NrAktuelleZeile = 0;
	my $NrKleinsteZeile = ($gPage - 1) * $gLinesPerPage + 1;
	my $NrGroessteZeile = ($gPage - 1) * $gLinesPerPage + $gLinesPerPage;

	for my $Zeile (@Zeilen)
	{
		# Wenn Gästebuch moderiert und kein User eingeloggt, dann nur die freigeschalteten Einträge ausgeben:
		if(!$gbLoggedIn)
		{
			my $bApproved = (split(/\|/, $Zeile))[1];
			next if(!$bApproved);
		}

		# Nur die Einträge der aktuellen Seite ausgeben
		$NrAktuelleZeile++;
		next if($NrAktuelleZeile < $NrKleinsteZeile);
		last if($NrAktuelleZeile > $NrGroessteZeile);

		chomp $Zeile;
		$htmlEntries .= GetHtmlEntry(split(/\|/, $Zeile));
	}

	$htmlOutput = ReplaceLineOnce($htmlOutput, '@SHOW_ENTRIES@', $htmlEntries);

	#### Login-Formular am Ende der Liste ausgeben ####

	if(!$gbLoggedIn)
	{
		# Login-Formular
		$htmlOutput = ReplaceLine($htmlOutput, '@ADMIN_LOGIN@', $gTemplateAdminLogin);
	}
	else
	{
		# Logout-Formular
		my $htmlAdminLogout = $gTemplateAdminLogout;
		$htmlAdminLogout = ReplaceLine($htmlAdminLogout, '@USER_NAME@', $gUserName);
		$htmlOutput = ReplaceLine($htmlOutput, '@ADMIN_LOGIN@', $htmlAdminLogout);
	}

	#### Ende des Formulars und der Webseite ####

	my $htmlHiddenData = '';
	if($gbLoggedIn)
	{
		$htmlHiddenData = MakeHiddenData('loggedin', 'true');
	}

	@gTemplateLines = ReplaceLines(\@gTemplateLines, '@SCRIPT_NAME@', $gScriptName);
	@gTemplateLines = ReplaceLines(\@gTemplateLines, '@HIDDEN_DATA@', $htmlHiddenData);
	@gTemplateLines = ReplaceLines(\@gTemplateLines, '@OUTPUT@', $htmlOutput);
	print @gTemplateLines;
}

#
# Durchsucht die Formulardaten nach dem Feld mit dem Prefix, das der Funktion
# als Parameter übergeben wird, und gibt das Postfix zurück. Beide Teile sind
# durch "-" voneinander getrennt. Beispiel: In den Formulardaten ist u.a. das
# Feld mit dem Namen "edit-123" enthalten. Mit dem Parameter "edit" findet die
# Funktion dieses Formularfeld und gibt "123" zurück. Ist kein Feld mit dem
# Prefix "edit" vorhanden, dann wird ein Leerstring zurückgegeben.
#
sub GetIdFromParameter
{
	my $prefix = shift;
	my $result = '';

	for my $par_name (keys %FORM)
	{
		my @par_pieces = split(/\-/, $par_name);

		if($par_pieces[0] eq $prefix)
		{
			$result = $par_pieces[1];
			last;
		}
	}

	$result;
}

#
# Vorschau des Eintrages
#
sub PreviewEntry
{
	AddEntry(1);	# 1 = Nur Plausi-Prüfungen und Datenkorrekturen durchführen

	# übergebene Werte aus CGI-Parametern holen
	my $approved = TextToHtml($FORM{'approved'});
	my $datum = TextToHtml($FORM{'datum'});
	my $zeit = TextToHtml($FORM{'zeit'});
	my $name = TextToHtml($FORM{'name'});
	my $email = TextToHtml($FORM{'email'});
	my $link = TextToHtml($FORM{'link'});
	my $inhalt = TextToHtml($FORM{'inhalt'}, 2);	# 2 = Zeilenumbrüche nicht nach HTML konvertieren
	my $comment = TextToHtml($FORM{'comment'}, 2);	# 2 = Zeilenumbrüche nicht nach HTML konvertieren

	# Spezielle Konvertierungen zum Anzeigen in der Vorschau:
	my $datum_display = TextToHtml(($gbLoggedIn) ? NormalizeDateTime($FORM{'datum'}, $gDateFormat) : GetDatumHeute());
	my $zeit_display = TextToHtml(($gbLoggedIn) ? NormalizeDateTime($FORM{'zeit'}, $gTimeFormat) : GetZeitJetzt());
	my $inhalt_display = TextToHtml(BereinigeText($FORM{'inhalt'}), ($gbLoggedIn) ? ($FORM{'htmlinhalt'}) ? 1 : 0 : 0);	# 1 = HTML erlauben
	my $comment_display = TextToHtml(BereinigeText($FORM{'comment'}), ($gbLoggedIn) ? ($FORM{'htmlcomment'}) ? 1 : 0 : 0);	# 1 = HTML erlauben

	# Vorschau erzeugen
	my $htmlEntry = GetHtmlEntry('0', '1', $datum_display, $zeit_display, $name, $email, $link, $inhalt_display, $comment_display);

	# Vorschau ins Template einfügen
	my $htmlOutput = $gTemplateSectionPreview;
	$htmlOutput = ReplaceLine($htmlOutput, '@SHOW_ENTRY@', $htmlEntry);

	# Template vervollständigen und ausgeben
	my $htmlHiddenData = MakeHiddenData
	(
		'loggedin', $FORM{'loggedin'},
		'page', $gPage,

		'id', $FORM{'id'},
		'approved', $approved,
		'datum', $datum,
		'zeit', $zeit,
		'name', $name,
		'email', $email,
		'link', $link,
		'inhalt', $inhalt,
		'comment', $comment,
		'ip', $FORM{'ip'},
		'browser', $FORM{'browser'},
		'history', $FORM{'history'},
		'htmlinhalt', $FORM{'htmlinhalt'},
		'htmlcomment', $FORM{'htmlcomment'}
	);

	@gTemplateLines = ReplaceLines(\@gTemplateLines, '@SCRIPT_NAME@', $gScriptName);
	@gTemplateLines = ReplaceLines(\@gTemplateLines, '@HIDDEN_DATA@', $htmlHiddenData);
	@gTemplateLines = ReplaceLines(\@gTemplateLines, '@OUTPUT@', $htmlOutput);
	print @gTemplateLines;
	exit 0;
}

#
# Fügt der aktiven Datenbank einen Eintrag hinzu. Alle dazu notwendigen Daten
# existieren als Formulardaten.
#
sub AddEntry
{
	my $bCheckOnly = shift;		# Wenn 1, werden nur Plausi-Prüfungen und Korrekturen vorgenommen.

	# Returns aus den mehrzeiligen Textfeldern entfernen, sonst macht print aus "\r\n" -> "\r\r\n"
	$FORM{'inhalt'} =~ s/\r//g;
	$FORM{'comment'} =~ s/\r//g;

	# übergebene Werte aus CGI-Parametern holen
	my $id = $FORM{'id'};		# Wenn die ID -1 ist (neuer Eintrag), muss eine neue ID erzeugt werden.
	my $approved = $FORM{'approved'};
	my $datum = $FORM{'datum'};
	my $zeit = $FORM{'zeit'};
	my $name = $FORM{'name'};
	my $email = $FORM{'email'};
	my $link = $FORM{'link'};
	my $inhalt = $FORM{'inhalt'};
	my $comment = $FORM{'comment'};

	# Neu zu ermittelnde Werte:
	my $ip = $ENV{'REMOTE_ADDR'};
	my $browser = $ENV{'HTTP_ACCEPT_LANGUAGE'} . '###SEP###' . $ENV{'HTTP_USER_AGENT'};
	my $history = $gMessageStrings{'history-new-entry'} . ' ' . GetZeitstempel();

	# Einträge ändern dürfen nur eingeloggte Benutzer
	return if(-1 ne $id && !$gbLoggedIn);

	# Plausi-Prüfungen und Datenkorrektur
	my $bFehler = 0;

	# Gegen "Hackversuche" (jemand speichert sich z.B. das Eingabeformular
	# ab und editiert in den Hidden-Feldern herum) einige Maßnahmen:

	if(!$gbLoggedIn)
	{
		$datum = GetDatumHeute();
		$zeit = GetZeitJetzt();
		$comment = '';
	}
	else
	{
		$datum = NormalizeDateTime($datum, $gDateFormat, \$bFehler);
		$zeit = NormalizeDateTime($zeit, $gTimeFormat, \$bFehler);
	}

	# Content-Filter
	if(!$gbLoggedIn && ContainsForbiddenStrings($name, $email, $link, $inhalt))
	{
		DisplayEntries();
		exit 0;
	}

	# Name (Pflichtfeld; 1...50 Zeichen)

	if('' eq $name)
	{
		AddMessage(1, $gMessageStrings{'validate-name-missing'});
		$bFehler = 1;
	}
	elsif(!$gbLoggedIn && length($name) > 50)
	{
		AddMessage(1, $gMessageStrings{'validate-name-length'});
		$bFehler = 1;
	}
	elsif(!$gbLoggedIn && ContainsForbiddenNames($name))
	{
		AddMessage(1, $gMessageStrings{'validate-name-forbidden'});
		$bFehler = 1;
	}

	# E-Mail-Adresse (optional; muß User, '@', und mindestens 2 durch '.' voneinander getrennte
	# Host-Bestandteile enthalten und darf maximal 50 Zeichen lang sein)

	if('' ne $email)
	{
		my ($user, $host) = split(/\@/, $email);
		my @hostparts = split(/\./, $host);
		if('' eq $user || @hostparts < 2 || '' eq $hostparts[$#hostparts] || '' eq $hostparts[$#hostparts - 1])
		{
			AddMessage(1, $gMessageStrings{'validate-email-syntax'});
			$bFehler = 1;
		}
	}

	if(!$gbLoggedIn && length($email) > 50)
	{
		AddMessage(1, $gMessageStrings{'validate-email-length'});
		$bFehler = 1;
	}

	# Link (optional; für nicht Eingeloggte max. 200 Zeichen)

	if('' ne $link && 'http' ne substr($link, 0, 4))
	{
		$FORM{'link'} = 'https://'.$link;
	}

	if(!$gbLoggedIn && length($link) > 200)
	{
		AddMessage(1, $gMessageStrings{'validate-website-length'});
		$bFehler = 1;
	}

	# Text (Pflichtfeld; für nicht Eingeloggte max. 4 KB und Strings > 50 Zeichen nicht erlaubt)

	if('' eq $inhalt)
	{
		AddMessage(1, $gMessageStrings{'validate-message-missing'});
		$bFehler = 1;
	}
	elsif(!$gbLoggedIn && length($inhalt) > 4096)
	{
		AddMessage(1, $gMessageStrings{'validate-message-length'});
		$bFehler = 1;
	}
	else
	{
		my @Woerter = split(/[\ \r\n]/, $inhalt);
		my $maxlength = 0;
		for my $Wort (@Woerter)
		{
			chomp $Wort;
			$maxlength = length($Wort) if($maxlength < length($Wort));
		}
		if(!$gbLoggedIn && $maxlength > 50)
		{
			AddMessage(1, $gMessageStrings{'validate-message-longwords'});
			$bFehler = 1;
		}
		else
		{
			# Datenkorrekturen:
			$inhalt = BereinigeText($inhalt);
			$comment = BereinigeText($comment);
		}
	}

	# Wenn nur geprüft werden sollte (für Vorschau) und keine Fehler aufgetreten sind, dann zurückkehren:
	return if($bCheckOnly & !$bFehler);

	# Wenn Fehler aufgetreten sind, Eingabe korrigieren:
	CorrectEntry() if($bFehler);

	my $aktion = 'add';	# für Benachrichtigungs-Mail

	# Datensätze in Array einlesen
	open(HANDLE_DB_ADD, "+<$gFileNameDatabase");
	my @Zeilen = <HANDLE_DB_ADD>;

	# Wenn noch keine gültige ID vorhanden (neuer Eintrag), ...
	if(-1 eq $id || '' eq $id)
	{
		# ... dann eine neue erzeugen:

		# ID-Datei zum Lesen und Schreiben öffnen
		if(open(HANDLE_ID, "+<$gFileNameId"))
		{
			# ID-Datei vorhanden: Zuletzt erzeugte ID aus Datei lesen
			$id = <HANDLE_ID>;
			chomp $id;

			# Zurück zum Anfang der ID-Datei
			seek(HANDLE_ID, 0, 0);
			truncate(HANDLE_ID, 0);
		}
		else
		{
			# Noch keine ID-Datei vorhanden: Zuletzt verwendete ID ermitteln
			# (= höchste in der DB und im Archiv vorkommende ID)

			$id = 0;

			# IDs in der DB-Datei durchgehen:

			for my $Zeile (@Zeilen)
			{
				my $loop_id = (split(/\|/, $Zeile))[0];
				$id = $loop_id if($id < $loop_id);
			}

			# Neue ID-Datei erzeugen
			open(HANDLE_ID, ">$gFileNameId");
		}
		# und inkrementieren
		$id++;

		# Neue ID in die ID-Datei schreiben:
		print HANDLE_ID $id;
		close(HANDLE_ID);

		# Nach dem Hinzufügen eines *neuen* Eintrages soll wieder die 1. Seite angezeigt werden:
		$gPage = 1;
	}
	# sonst (gültige ID) ...
	else
	{
		# ... den Eintrag mit der vorhandenen ID aus dem Array löschen:

		my @Zeilen_neu = ();
		for my $Zeile (@Zeilen)
		{
			my $loop_id = (split(/\|/, $Zeile))[0];

			if($loop_id ne $id)
			{
				push @Zeilen_neu, $Zeile;
			}
			else
			{
				chomp $Zeile;

				# Gespeicherte IP-Adresse, Hostnamen und Bearbeitungshistorie merken:
				($ip, $browser, $history) = (split(/\|/, $Zeile))[9,10,11];

				# Bearbeitungshistorie erweitern:
				$history .= sprintf("%s%s %s", ('' ne $history) ? ',' : '', $gUserName, GetZeitstempel());
			}
		}
		@Zeilen = @Zeilen_neu;

		$aktion = 'mod';	# für Benachrichtigungs-Mail
	}

	# Daten DB-gerecht aufbereiten
	my $approved_db = $gbLoggedIn ? $approved ? 1 : 0 : $gbModerated ? 0 : 1;
	my $datum_db = TextToHtml($datum);
	my $zeit_db = TextToHtml($zeit);
	my $name_db = TextToHtml($name);
	my $email_db = TextToHtml($email);
	my $link_db = TextToHtml($link);
	my $inhalt_db = TextToHtml($inhalt, ($gbLoggedIn) ? ($FORM{'htmlinhalt'}) ? 1 : 0 : 0);	# 1 = HTML erlauben
	my $comment_db = TextToHtml($comment, ($gbLoggedIn) ? ($FORM{'htmlcomment'}) ? 1 : 0 : 0);	# 1 = HTML erlauben
	my $ip_db = TextToHtml($ip);
	my $browser_db = TextToHtml($browser);

	# Datensatz zum Array hinzufügen
	push @Zeilen, "$id|$approved_db|$datum_db|$zeit_db|$name_db|$email_db|$link_db|$inhalt_db|$comment_db|$ip_db|$browser_db|$history\n";

	# Daten zur Datei hinzufügen
	seek(HANDLE_DB_ADD, 0, 0);
	truncate(HANDLE_DB_ADD, 0);
	print HANDLE_DB_ADD @Zeilen;
	close(HANDLE_DB_ADD);

	# Benachrichtigungs-Mail senden
	SendMail($aktion, $id, $approved_db, $datum, $zeit, $name, $email, $link, $inhalt, $comment, $ip, $browser, $history);

	# Bestätigungsseite anzeigen (als "Reload-Verhinderer", damit durch Reloaden der Eintragsliste
	# kein wiederholter Neueintrag erfolgt):
	ConfirmEntry($id);
}

#
# Ruft das Erfassungs-Formular im Korrektur-Modus auf.
#
sub CorrectEntry
{
	EditEntry($FORM{'id'}, 1);
}

#
# Ruft das Erfassungs-Formular im Neuanlage-Modus (ID = -1) auf.
#
sub NewEntry
{
	EditEntry(-1);
}

#
# Gibt das Erfassungs-Formular aus. Im Änderungsmodus (ID ungleich -1) werden
# die Felder mit den Daten aus dem Datensatz mit dieser ID vorbelegt, ansonsten
# bleiben sie leer. Auch die Dropdown-Boxen werden mit Werten gefüllt. Die
# Formulardaten, die zwischen dem Anzeige-Formular in DisplayEntries() und diesem
# Erfassungs-Formular ausgetauscht werden, und die keine sichtbaren Eingabefelder
# sind, werden mittels "hidden"-Feldern weitergegeben.
#
sub EditEntry
{
	my $a_id = shift;
	my $bKorr = shift;

	my $approved = 'checked';
	my $datum = '';
	my $zeit = '';
	my $name = '';
	my $email = '';
	my $link = '';
	my $inhalt = '';
	my $comment = '';
	my $ip = '';
	my $browser = '';
	my $history = '';

	# Einträge ändern dürfen nur eingeloggte Benutzer
	return if(-1 ne $a_id && !$gbLoggedIn);

	my $untertitel = 'title-add';

	# Korrektur-Modus?
	if($bKorr)
	{
		# Daten aus dem Formular holen:

		# Returns aus den mehrzeiligen Textfeldern entfernen, sonst macht print aus "\r\n" -> "\r\r\n"
		$FORM{'inhalt'} =~ s/\r//g;
		$FORM{'comment'} =~ s/\r//g;

		$approved = $FORM{'approved'};
		$datum = (-1 ne $a_id || $gbLoggedIn) ? TextToHtml($FORM{'datum'}) : '';
		$zeit = (-1 ne $a_id || $gbLoggedIn) ? TextToHtml($FORM{'zeit'}) : '';
		$name = TextToHtml($FORM{'name'});
		$email = TextToHtml($FORM{'email'});
		$link = TextToHtml($FORM{'link'});
		$inhalt = TextToHtml($FORM{'inhalt'}, 2);	# 2 = Zeilenumbrüche nicht nach HTML konvertieren
		$comment = TextToHtml($FORM{'comment'}, 2);	# 2 = Zeilenumbrüche nicht nach HTML konvertieren
		$ip = $FORM{'ip'};
		$browser = $FORM{'browser'};
		$history = $FORM{'history'};

		$untertitel = 'title-correct';
	}
	elsif(-1 ne $a_id)
	{
		# Daten aus der Datenbank holen (falls keine Neuanlage):
		open(HANDLE_DB_EDIT_READ, "$gFileNameDatabase");
		my @Zeilen = <HANDLE_DB_EDIT_READ>;
		close(HANDLE_DB_EDIT_READ);

		for my $Zeile (@Zeilen)
		{
			chomp $Zeile;
			my $id = (split(/\|/, $Zeile))[0];

			if($id eq $a_id)
			{
				($approved, $datum, $zeit, $name, $email, $link, $inhalt, $comment, $ip, $browser, $history) = (split(/\|/, $Zeile))[1..11];

				$approved = ($approved ? 'checked' : '');
				$inhalt =~ s/<br>/\n/g;		# HTML-codierte Zeilenumbrüche wieder in "echte" Linefeeds umwandeln
				$comment =~ s/<br>/\n/g;	# HTML-codierte Zeilenumbrüche wieder in "echte" Linefeeds umwandeln

				$untertitel = 'title-edit';

				last;
			}
		}
	}

	# Checkboxen für "HTML zulassen" in den mehrzeiligen Textfeldern

	$FORM{'htmlinhalt'} = 'checked' if(-1 ne index($inhalt, '<'));
	$FORM{'htmlcomment'} = 'checked' if(-1 ne index($comment, '<'));

	# Administrative Daten zum Eintrag

	my ($lang, $ua) = split(/###SEP###/, $browser);
	my $htmlEditHistory = '';
	if($gbLoggedIn && -1 ne $a_id)
	{
		my @EditHistory = split(/\,/, $history);
		for my $Bearbeitungsvorgang (@EditHistory)
		{
			my ($user, $datum, $zeit) = split(/\ /, $Bearbeitungsvorgang);
			$htmlEditHistory .= "<br>$datum, $zeit: $user";
		}
	}

	# Eingabeformular

	my $htmlHintModerated = ($gbModerated && !$gbLoggedIn) ? $gTemplateEditHintModerated : '';
	my $htmlAdminControls1 = ($gbLoggedIn) ? $gTemplateEditAdminControls1 : '';
	my $htmlHintAdminMessage = ($gbLoggedIn && -1 ne $a_id) ? $gTemplateEditHintAdminMessage : '';
	my $htmlAdminControls2 = ($gbLoggedIn) ? $gTemplateEditAdminControls2 : '';
	my $htmlAdminEntryInfo = ($gbLoggedIn && -1 ne $a_id) ? $gTemplateEditAdminEntryInfo : '';

	my $htmlEditEntry = $gTemplateEditEntry;
	$htmlEditEntry = ReplaceLine($htmlEditEntry, '@HINT_MODERATED@', $htmlHintModerated);
	$htmlEditEntry = ReplaceLine($htmlEditEntry, '@ADMIN_CONTROLS_1@', $htmlAdminControls1);
	$htmlEditEntry = ReplaceLine($htmlEditEntry, '@ADMIN_CONTROLS_2@', $htmlAdminControls2);
	$htmlEditEntry = ReplaceLine($htmlEditEntry, '@HINT_ADMIN_EDIT@', $htmlHintAdminMessage);
	$htmlEditEntry = ReplaceLine($htmlEditEntry, '@ADMIN_ENTRY_INFO@', $htmlAdminEntryInfo);

	# Eingabefelder
	$htmlEditEntry = ReplaceLine($htmlEditEntry, '@DATE@', $datum);
	$htmlEditEntry = ReplaceLine($htmlEditEntry, '@TIME@', $zeit);
	$htmlEditEntry = ReplaceLine($htmlEditEntry, '@AUTHOR_NAME@', $name);
	$htmlEditEntry = ReplaceLine($htmlEditEntry, '@AUTHOR_EMAIL@', $email);
	$htmlEditEntry = ReplaceLine($htmlEditEntry, '@AUTHOR_WEBSITE@', $link);
	$htmlEditEntry = ReplaceLine($htmlEditEntry, '@ENTRY_MESSAGE@', $inhalt);
	$htmlEditEntry = ReplaceLine($htmlEditEntry, '@ENTRY_COMMENT@', $comment);
	$htmlEditEntry = ReplaceLine($htmlEditEntry, '@CHECKED_APPROVED@', $approved);
	$htmlEditEntry = ReplaceLine($htmlEditEntry, '@CHECKED_HTML_MESSAGE@', $FORM{'htmlinhalt'});
	$htmlEditEntry = ReplaceLine($htmlEditEntry, '@CHECKED_HTML_COMMENT@', $FORM{'htmlcomment'});

	# Administrative Daten zum Eintrag
	$htmlEditEntry = ReplaceLine($htmlEditEntry, '@ENTRY_ID@', $a_id);
	$htmlEditEntry = ReplaceLine($htmlEditEntry, '@REMOTE_ADDR@', $ip);
	$htmlEditEntry = ReplaceLine($htmlEditEntry, '@HTTP_ACCEPT_LANGUAGE@', $lang);
	$htmlEditEntry = ReplaceLine($htmlEditEntry, '@HTTP_USER_AGENT@', $ua);
	$htmlEditEntry = ReplaceLine($htmlEditEntry, '@HISTORY@', $htmlEditHistory);

	my $htmlOutput = $gTemplateSectionEdit;
	$htmlOutput = ReplaceLine($htmlOutput, '@TITLE@', $gMessageStrings{$untertitel});
	$htmlOutput = ReplaceLine($htmlOutput, '@MESSAGES@', GetHtmlMessages());
	$htmlOutput = ReplaceLine($htmlOutput, '@EDIT_ENTRY@', $htmlEditEntry);

	#### Ende des Formulars und der Webseite ####

	my $htmlHiddenData = MakeHiddenData
	(
		'loggedin', $FORM{'loggedin'},
		'page', $gPage,

		'id', $a_id,
		'ip', $ip,
		'browser', $browser,
		'history', $history
	);

	@gTemplateLines = ReplaceLines(\@gTemplateLines, '@SCRIPT_NAME@', $gScriptName);
	@gTemplateLines = ReplaceLines(\@gTemplateLines, '@HIDDEN_DATA@', $htmlHiddenData);
	@gTemplateLines = ReplaceLines(\@gTemplateLines, '@OUTPUT@', $htmlOutput);
	print @gTemplateLines;
	exit 0;
}

#
# Löscht den Datensatz mit der übergebenen ID.
#
sub DeleteEntry
{
	my $a_id = shift;

	# Datenbank öffnen und lesen
	open(HANDLE_DB_DELETE, "+<$gFileNameDatabase");
	my @Zeilen = <HANDLE_DB_DELETE>;

	my @Zeilen_neu;

	for my $Zeile (@Zeilen)
	{
		my $id = (split(/\|/, $Zeile))[0];

		if($id ne $a_id)
		{
			push @Zeilen_neu, $Zeile;
		}
		else
		{
			my $aktion = 'del';

			# Zeile in Trash-Datei schreiben:
			open(HANDLE_TSH_DELETE, ">>$gFileNameDatabaseTrash");
			print HANDLE_TSH_DELETE $Zeile;
			close(HANDLE_TSH_DELETE);

			chomp $Zeile;
			my ($id, $approved, $datum, $zeit, $name, $email, $link, $inhalt, $comment, $ip, $browser, $history) = split(/\|/, $Zeile);
			my $inhalt = HtmlToText($inhalt);
			my $comment = HtmlToText($comment);

			# Benachrichtigungs-Mail senden
			SendMail($aktion, $id, $approved, $datum, $zeit, $name, $email, $link, $inhalt, $comment, $ip, $browser, $history);
		}
	}

	# Die nicht gelöschten Sätze in die DB zurückschreiben
	seek(HANDLE_DB_DELETE, 0, 0);
	truncate(HANDLE_DB_DELETE, 0);
	print HANDLE_DB_DELETE @Zeilen_neu;
	close(HANDLE_DB_DELETE);
}

#
# Schaltet den Datensatz mit der übergebenen ID frei.
#
sub ApproveEntry
{
	my $a_id = shift;

	# Datenbank öffnen und lesen
	open(HANDLE_DB_APPROVE, "+<$gFileNameDatabase");
	my @Zeilen = <HANDLE_DB_APPROVE>;

	my @Zeilen_neu;

	for my $Zeile (@Zeilen)
	{
		chomp $Zeile;
		my ($id, $approved, $datum, $zeit, $name, $email, $link, $inhalt, $comment, $ip, $browser, $history) = split(/\|/, $Zeile);

		if($id eq $a_id)
		{
			$approved = 1;

			my $aktion = 'app';

			my $inhalt = HtmlToText($inhalt);
			my $comment = HtmlToText($comment);

			# Benachrichtigungs-Mail senden
			SendMail($aktion, $id, $approved, $datum, $zeit, $name, $email, $link, $inhalt, $comment, $ip, $browser, $history);
		}

		push @Zeilen_neu, "$id|$approved|$datum|$zeit|$name|$email|$link|$inhalt|$comment|$ip|$browser|$history\n";
	}

	# Die Sätze in die DB zurückschreiben
	seek(HANDLE_DB_APPROVE, 0, 0);
	truncate(HANDLE_DB_APPROVE, 0);
	print HANDLE_DB_APPROVE @Zeilen_neu;
	close(HANDLE_DB_APPROVE);
}

#
# Zwischenseite mit automatischem Reload der Eintragsliste, um doppelte Einträge
# durch manuellen Reload zu unterbinden.
#
sub ConfirmEntry
{
	my $a_id = shift;

	open(HANDLE_DB_CONFIRM_READ, "$gFileNameDatabase");
	my @Zeilen = <HANDLE_DB_CONFIRM_READ>;
	close(HANDLE_DB_CONFIRM_READ);

	# Eintrag ermitteln
	my $htmlEntry = '';
	for my $Zeile (@Zeilen)
	{
		chomp $Zeile;
		my ($id, $approved, $datum, $zeit, $name, $email, $link, $inhalt, $comment, $ip, $browser, $history) = split(/\|/, $Zeile);
		next if($id != $a_id);
		$htmlEntry = GetHtmlEntry($id, '1', $datum, $zeit, $name, $email, $link, $inhalt, $comment, $ip, $browser);
		last;
	}

	# Hinweismeldung
	my $htmlHintApprove = ($gbModerated && !$gbLoggedIn) ? $gMessageStrings{'hint-approve'} : '';

	# Eintrag ins Template einfügen
	my $htmlOutput = $gTemplateSectionConfirm;
	$htmlOutput = ReplaceLine($htmlOutput, '@SHOW_ENTRY@', $htmlEntry);
	$htmlOutput = ReplaceLine($htmlOutput, '@HINT_APPROVE@', $htmlHintApprove);

	# Template vervollständigen und ausgeben
	my $htmlHiddenData = MakeHiddenData
	(
		'loggedin', $FORM{'loggedin'},
		'page', $gPage,
	);

	@gTemplateLines = ReplaceLines(\@gTemplateLines, '@SCRIPT_NAME@', $gScriptName);
	@gTemplateLines = ReplaceLines(\@gTemplateLines, '@HIDDEN_DATA@', $htmlHiddenData);
	@gTemplateLines = ReplaceLines(\@gTemplateLines, '@OUTPUT@', $htmlOutput);
	print @gTemplateLines;
	exit 0;
}

#
# Einen Eintrag ausgeben
#
sub GetHtmlEntry
{
	# Datenfelder:

	my $id = shift;
	my $approved = shift;
	my $datum = shift;
	my $zeit = shift;
	my $name = shift;
	my $email = shift;
	my $link = shift;
	my $inhalt = ProcessText(shift);
	my $comment = ProcessText(shift);
	my $ip = shift;
	my $browser = shift;

	# Daten aufbereiten

	my $htmlHeader = ('' ne $link && 'http://' ne $link && 'https://' ne $link) ? $gTemplateEntryHeaderWebsite : $gTemplateEntryHeader;
	my $htmlEmail = ('' ne $email && $gbLoggedIn) ? $gTemplateEntryEmail : '';
	my $htmlHomepage = ('' ne $link && 'http://' ne $link && 'https://' ne $link) ? $gTemplateEntryWebsite : '';
	my $htmlComment = ('' ne $comment) ? $gTemplateEntryComment : '';
	my $htmlHeaderComment = ('' ne $comment) ? $gTemplateEntryHeaderComment : '';
	my $htmlHintApprove = (!$approved && $gbLoggedIn) ? $gTemplateEntryHintApprove : '';
	my $htmlClassNewEntry = (!$approved && $gbLoggedIn) ? 'new' : '';
	my $htmlApproveButton = (!$approved && $gbLoggedIn) ? $gTemplateEntryApproveButton : '';
	my $htmlEditButtons = (!$FORM{'preview'} && !$FORM{'add'} && $gbLoggedIn) ? $gTemplateEntryEditButtons : '';
	my $htmlPreviewButtons = ($FORM{'preview'}) ? $gTemplateEntryPreviewButtons : '';
	my $htmlConfirmButton = ($FORM{'add'}) ? $gTemplateEntryConfirmButton : '';

	# Eintrag ausgeben

	my $htmlEntry = $gTemplateShowEntry;

	$htmlEntry = ReplaceLine($htmlEntry, '@ENTRY_HEADER@', $htmlHeader);
	$htmlEntry = ReplaceLine($htmlEntry, '@ENTRY_EMAIL@', $htmlEmail);
	$htmlEntry = ReplaceLine($htmlEntry, '@ENTRY_WEBSITE@', $htmlHomepage);
	$htmlEntry = ReplaceLine($htmlEntry, '@ENTRY_COMMENT@', $htmlComment);
	$htmlEntry = ReplaceLine($htmlEntry, '@ENTRY_HEADER_COMMENT@', $htmlHeaderComment);
	$htmlEntry = ReplaceLine($htmlEntry, '@ENTRY_HINT_APPROVE@', $htmlHintApprove);
	$htmlEntry = ReplaceLine($htmlEntry, '@CLASS_ENTRY@', $htmlClassNewEntry);
	$htmlEntry = ReplaceLine($htmlEntry, '@ENTRY_APPROVE_BUTTON@', $htmlApproveButton);
	$htmlEntry = ReplaceLine($htmlEntry, '@ENTRY_EDIT_BUTTONS@', $htmlEditButtons);
	$htmlEntry = ReplaceLine($htmlEntry, '@ENTRY_PREVIEW_BUTTONS@', $htmlPreviewButtons);
	$htmlEntry = ReplaceLine($htmlEntry, '@ENTRY_CONFIRM_BUTTON@', $htmlConfirmButton);

	$htmlEntry = ReplaceLine($htmlEntry, '@ENTRY_ID@', $id);
	$htmlEntry = ReplaceLine($htmlEntry, '@DATE@', $datum);
	$htmlEntry = ReplaceLine($htmlEntry, '@TIME@', $zeit);
	$htmlEntry = ReplaceLine($htmlEntry, '@AUTHOR_NAME@', $name);
	$htmlEntry = ReplaceLine($htmlEntry, '@AUTHOR_EMAIL@', $email);
	$htmlEntry = ReplaceLine($htmlEntry, '@AUTHOR_WEBSITE@', $link);

	$htmlEntry = ReplaceLine($htmlEntry, '@ENTRY_LINES@', GetHtmlEntryLines($inhalt, $gTemplateEntryLine));
	$htmlEntry = ReplaceLine($htmlEntry, '@ENTRY_LINES_COMMENT@', GetHtmlEntryLines($comment, $gTemplateEntryLineComment));

	$htmlEntry;
}

#
# Bereingen des Eintragstextes vor dem Abspeichern in die DB
# - überflüssige Linefeeds entfernen
#
sub BereinigeText
{
	my $text = shift;

	# Aus 3 und mehr aufeinanderfolgende Linefeeds 2 machen:
#	$text =~ s/([^\n]*)[\n]{3,}([^\n]*)/$1\n\n$2/sg;	
	$text =~ s/[\n]{3,}/\n\n/sg;	# (eine etwas kürzere Version...)

	# Alle Linefeeds am Beginn entfernen:
	$text =~ s/^[\n]*([^\n])/$1/;

	# Alle Linefeeds am Schluß entfernen:
	$text =~ s/[\n]*$//;

	$text;
}

#
# Aufbereiten des Eintragstextes zur Anzeige:
# - URLs zu Links aufbereiten
#
sub ProcessText
{
	my $text = shift;

	if ($gbMakeLinksClickable)
	{
		# Macht aus einer URL einen anklickbaren Link, aber nur, wenn sie nicht bereits ein Link ist:
		$text =~ s#((?<!href=")(?<!src="))(http://[^\s"()<>]*)#$1<a href="$2" target="_blank" rel="noopener nofollow">$2</a>#g;
		$text =~ s#((?<!href=")(?<!src="))(https://[^\s"()<>]*)#$1<a href="$2" target="_blank" rel="noopener nofollow">$2</a>#g;
	}

	$text;
}

#
# Gibt Meldungen aus, falls welche vorhanden sind
#
sub GetHtmlMessages
{
	my $strMessages = '';

	if(@gMessages > 0)
	{
		my $strMessageLines = '';
		for my $message (@gMessages)
		{
			my ($severity, $text) = split(/\|/, $message);

			my $html = $gTemplateMessageLine;
			$html = ReplaceLine($html, '@CLASS_ERROR@', $gMessageClasses[$severity]);
			$html = ReplaceLine($html, '@MESSAGE_LINE@', $text);
			$strMessageLines .= $html;
		}

		$strMessages = $gTemplateMessages;
		$strMessages = ReplaceLine($strMessages, '@MESSAGE_LINES@', $strMessageLines);
	}

	$strMessages;
}

#
# Hilfsfunktion zum Maskieren aller HTML-(&, ", <, >) und DB-(Satztrenner, Feldtrenner)
# eigenen Zeichen, damit diese nicht vom Browser bzw. vom Script "interpretiert" werden,
# sondern als diese Zeichen dargestellt werden. Mit dieser Funktion sollten alle
# Textstrings, die in die DB geschrieben werden, zuvor in HTML konvertiert werden.
# Später nach dem Laden aus der DB können die Strings dann unmittelbar auf einer
# HTML-Seite angezeigt werden, ohne daß sie zuvor erneut konvertiert werden müssen.
#
sub TextToHtml
{
	my $text = shift;
	my $flag = shift;	# 1 = HTML erlauben, nur Zeilenumbrüche konvertieren
						# 2 = nur HTML konvertieren, keine Zeilenumbrüche

	if(1 ne $flag)
	{
		$text =~ s/\&/&amp;/g;
		$text =~ s/\"/&quot;/g;
		$text =~ s/\</&lt;/g;
		$text =~ s/\>/&gt;/g;
	}

	if(2 ne $flag)
	{
		$text =~ s/\r\n/<br>/g;
		$text =~ s/\n/<br>/g;
	}

	$text =~ s/\|/&#124;/g;		# Feldtrenner für DB-Dateien

	$text;
}

#
# Hilfsfunktion zum Konvertieren der in einem String enthaltenen HTML-Tags in normale
# Textzeichen und Zeilenumbrüche. Mit dieser Funktion sollten alle HTML-codierte Strings,
# die aus der DB geladen wurden, um in mehrzeiligen Formularfeldern (textarea) angezeigt
# zu werden, zuvor in Text konvertiert werden. HTML-codierte Strings zur Anzeige in
# einzeiligen Textfeldern dürfen damit nicht konvertiert werden, da die dort enthaltenen
# HTML-Zeichen bereits vom Browser "interpretiert" werden.
#
sub HtmlToText
{
	my $text = shift;

	$text =~ s/<br>/\n/g;

	$text =~ s/&quot;/\"/g;
	$text =~ s/&lt;/\</g;
	$text =~ s/&gt;/\>/g;
	$text =~ s/&#124;/\|/g;

	$text;
}

#
# "Normalisiert" einen Datums- oder Zeitstring. Ist der Eingabestring leer,
# wird das akutelle Datum bzw. die aktuelle Uhrzeit zurückgegeben.
#
sub NormalizeDateTime
{
	my $input = shift;
	my $format = shift;
	my $refError = shift; # Wird bei Fehler auf 1 gesetzt.

	if ('' eq $input)
	{
		return strftime($format, localtime());
	}

	eval
	{
		my $time = Time::Piece->strptime($input, $format);
		$input = $time->strftime($format);
	}
	or do
	{
		AddMessage(1, $format eq $gDateFormat ? $gMessageStrings{'validate-date-format'} : $gMessageStrings{'validate-time-format'});
		#AddMessage(1, $@);
		${$refError} = 1;
	};

	$input;
}

#
# Sendet eine E-Mail an Redakteure und ggf. an Abonnenten mit Angaben über
# hinzugefügte, geänderte, gelöschte, archivierte und wiederhergestellte
# Einträge. Abonnenten erhalten nur Mails über hinzugefügte und geänderte
# Einträge in der "Echt"-Datenbank (nicht im Archiv).
#
sub SendMail
{
	my $aktion = shift;
	my $id = shift;
	my $approved = shift;
	my $datum = shift;
	my $zeit = shift;
	my $name = shift;
	my $email = shift;
	my $link = shift;
	my $inhalt = shift;
	my $comment = shift;
	my $ip = shift;
	my $browser = shift;
	my $history = shift;

	# Infos aufbreiten

	my $historyLines = '';
	my @Lines = split(/\,/, $history);
	for my $line (@Lines)
	{
		my ($histUser, $histDate, $histTime) = split(/\ /, $line);
		my $historyLine = $gMessageStrings{'sendmail-history-line'};
		$historyLine = ReplaceLine($historyLine, '@DATE@', $histDate);
		$historyLine = ReplaceLine($historyLine, '@TIME@', $histTime);
		$historyLine = ReplaceLine($historyLine, '@TEXT@', $histUser);
		$historyLines .= "$historyLine\n";
	}

	$history =~ s/\,/\, /sg;

	my %ActionStrings = (
		'add' => $gMessageStrings{'sendmail-action-added'},
		'mod' => $gMessageStrings{'sendmail-action-modified'},
		'del' => $gMessageStrings{'sendmail-action-deleted'},
		'app' => $gMessageStrings{'sendmail-action-approved'}
	);

	my $status = ($approved) ? $gMessageStrings{'sendmail-state-approved'} : ('add' eq $aktion) ? $gMessageStrings{'sendmail-state-not-approved-new'} : $gMessageStrings{'sendmail-state-not-approved'};
	my $person = ('' ne $gUserName) ? $gMessageStrings{'sendmail-person-user'} : $gMessageStrings{'sendmail-person-visitor'};
	my ($lang, $ua) = split(/###SEP###/, $browser);

	my $subject = $gMessageStrings{'sendmail-subject'};
	$subject = ReplaceLine($subject, '@ENTRY_ID@', $id);
	$subject = ReplaceLine($subject, '@ACTION@', $ActionStrings{$aktion});

	# Mail-Text zusammenstellen

	my $mailtext = $gTemplateNotificationMail;
	$mailtext = ReplaceLine($mailtext, '@FROM@', EncodeHeaderText($gMessageStrings{'sendmail-from'}));
	$mailtext = ReplaceLine($mailtext, '@TO@', EncodeHeaderText($gMessageStrings{'sendmail-to'}));
	$mailtext = ReplaceLine($mailtext, '@SUBJECT@', EncodeHeaderText($subject));
	$mailtext = ReplaceLine($mailtext, '@X-HEADER-STRINGS@', "X-Comment: Mail automatically generated by $gScriptName\nX-Mailer: $gPathNameSendmail ;-)");
	$mailtext = ReplaceLine($mailtext, '@GB@', $gMessageStrings{'sendmail-gb'});
	$mailtext = ReplaceLine($mailtext, '@PERSON@', $person);
	$mailtext = ReplaceLine($mailtext, '@STATE@', $status);
	$mailtext = ReplaceLine($mailtext, '@DATE@', $datum);
	$mailtext = ReplaceLine($mailtext, '@TIME@', $zeit);
	$mailtext = ReplaceLine($mailtext, '@REMOTE_ADDR@', $ip);
	$mailtext = ReplaceLine($mailtext, '@HTTP_ACCEPT_LANGUAGE@', $lang);
	$mailtext = ReplaceLine($mailtext, '@HTTP_USER_AGENT@', $ua);
	$mailtext = ReplaceLine($mailtext, '@HISTORY@', $history);
	$mailtext = ReplaceLine($mailtext, '@HISTORY_LINES@', $historyLines);
	# Muss nach allen anderen Ersetzungen stehen
	$mailtext = ReplaceLine($mailtext, '@ENTRY_ID@', $id);
	$mailtext = ReplaceLine($mailtext, '@ACTION@', $ActionStrings{$aktion});
	$mailtext = ReplaceLine($mailtext, '@USER_NAME@', $gUserName);
	# Muss ganz am Ende stehen, damit keine Benutzereingaben ersetzt werden
	$mailtext = ReplaceLine($mailtext, '@AUTHOR_NAME@', $name);
	$mailtext = ReplaceLine($mailtext, '@AUTHOR_EMAIL@', $email);
	$mailtext = ReplaceLine($mailtext, '@AUTHOR_WEBSITE@', $link);
	$mailtext = ReplaceLine($mailtext, '@MESSAGE@', $inhalt);
	$mailtext = ReplaceLine($mailtext, '@MESSAGE_HTML@', TextToHtml($inhalt));
	$mailtext = ReplaceLine($mailtext, '@COMMENT@', $comment);
	$mailtext = ReplaceLine($mailtext, '@COMMENT_HTML@', TextToHtml($comment));

	# Mail senden

	if(open(MAIL, "|$gPathNameSendmail -t"))
	{
		print MAIL $mailtext;
		close (MAIL);
	}
}

#
# Codiert den Mail-Betreff standardkonform
#
sub EncodeHeaderText
{
	my $text = shift;

	# Nur codieren, wenn Zeichen mit Wert > 127 enthalten sind
	if ($text =~ /[\x80-\xFF]/)
	{
		# "=" codieren
		$text =~ s/(=)/sprintf("=%02X", ord($1))/seg;

		# "_" und "?" codieren
		$text =~ s/([_?])/sprintf("=%02X", ord($1))/seg;

		# " " durch "_" ersetzen
		$text =~ tr/ /_/;

		# alle Zeichen mit Wert > 127 codieren
		$text =~ s/([\x80-\xFF])/sprintf("=%02X", ord($1))/seg;

		# "=?utf-8?Q?" voranstellen und "?=" anhängen
		$text = "=?utf-8?Q?" . $text . "?=";
	}

	$text;
}

#
# Vergleichsfunktion zum Sortieren der in der DB-Datei enthaltenen Zeilen
# nach ID (absteigend, d.h. die Einträge mit den größeren IDs stehen oben).
#

sub SortZeilen
{
	my $a = shift;
	my $b = shift;

	my $id_a = (split(/\|/, $a))[0];
	my $id_b = (split(/\|/, $b))[0];

	$id_b - $id_a;
}

#
# Zeitstempel erzeugen (Format: "TT.MM.JJJJ HH:MM:SS")
#
sub GetZeitstempel
{
	strftime("$gDateFormat $gTimeFormat", localtime());
}

#
# Heutiges Datum zurückgeben (Format: "TT.MM.JJJJ")
#
sub GetDatumHeute
{
	strftime($gDateFormat, localtime());
}

#
# Aktuelle Uhrzeit zurückgeben (Format: "HH:MM:SS")
#
sub GetZeitJetzt
{
	strftime($gTimeFormat, localtime());
}

#
# Schreibt die übergebene Textmeldung (Parameter 1) in den Meldungspuffer.
# Parameter 2 gibt die Art der Meldung an: 0 = Info, 1 = Fehler, 2 = Warnung.
# Beim Schreiben der Webseite wird diese Meldung dann ausgegeben (entsprechend
# der Meldungsart in einer bestimmten Farbe).
#
sub AddMessage
{
	my $severity = shift;
	my $text = shift;

	push @gMessages, "$severity|$text";
}

#
# Stylesheets einlesen und Inhalt in das HTML-Template einfügen
#

sub InsertStylesheets
{
	my $css = '';

	for my $StyleSheet (@gStyleSheets)
	{
		open(CSS, "$StyleSheet") or die "File not found: $StyleSheet";
		my @Lines = <CSS>;
		close(CSS);

		$css .= join("", @Lines);
	}

	@gTemplateLines = ReplaceLines(\@gTemplateLines, '@STYLESHEETS@', $css);
}

#
# IP-Adressen-Filter
#
sub CheckIp
{
	my $bResult = 0;

	my $ip = $ENV{'REMOTE_ADDR'};
	for my $loop_ip (@gForbiddenIPs)
	{
#		print "<br>$ip = $loop_ip";
		if($ip eq $loop_ip)
		{
			$bResult = 1;
			last;
		}
	}
	$bResult;
}

#
# Inhaltsfilter
#
sub ContainsForbiddenStrings
{
	my $bResult = 0;

	for my $InputText (@_)
	{
		for my $ForbiddenString (@gForbiddenStrings)
		{
			if($InputText =~ m/$ForbiddenString/i)
			{
				$bResult = 1;
				last;
			}
		}
	}
	$bResult;
}

sub ContainsForbiddenNames
{
	my $bResult = 0;

	for my $InputText (@_)
	{
		for my $ForbiddenName (@gForbiddenNames)
		{
			if($InputText =~ m/$ForbiddenName/i)
			{
				$bResult = 1;
				last;
			}
		}
	}
	$bResult;
}

#
# Umleitung zur Homepage
# (Muss noch vor Ausgabe des HTTP-Headers aufgerufen werden!)
#
sub GoHome
{
	my $urlHome = $ENV{'SCRIPT_URI'};
	$urlHome =~ s/$ENV{'SCRIPT_NAME'}//;
	print "Location: $urlHome\n\n";
	exit 0;
}

#### ab hier neu ####

sub MakeHiddenData
{
	my $name = shift;
	my $value = shift;

	my $strResult = '';

	while ($name ne '')
	{
		$strResult .= '<input type="hidden" name="' . $name . '" value="' . $value . '">';
		$name = shift;
		$value = shift;
	}

	$strResult;
}

#
#
#
sub GetHtmlEntryLines
{
	my $dbText = shift;
	my $htmlTemplateEntryLine = shift;

	my $htmlLines = '';
	my @Lines = split(/<br><br>/, $dbText);

	for my $line (@Lines)
	{
		$htmlLines .= ReplaceLine($htmlTemplateEntryLine, '@ENTRY_LINE@', $line);
	}

	$htmlLines;
}

sub ReplaceLines
{
	my $refLines = shift;
	my $strSearch = shift;
	my $strReplace = shift;

	my @ReturnLines = @{$refLines};

	for my $Zeile (@ReturnLines)
	{
		$Zeile =~ s/$strSearch/$strReplace/g;
	}
	
	@ReturnLines;
}

sub ReplaceLine
{
	my $strZeile = shift;
	my $strSearch = shift;
	my $strReplace = shift;

	my $ReturnZeile = $strZeile;

	$ReturnZeile =~ s/$strSearch/$strReplace/g;
	
	$ReturnZeile;
}

sub ReplaceLineOnce
{
	my $strZeile = shift;
	my $strSearch = shift;
	my $strReplace = shift;

	my $ReturnZeile = $strZeile;

	$ReturnZeile =~ s/$strSearch/$strReplace/;
	
	$ReturnZeile;
}

sub ExtractAndReplaceLines
{
	my $refLines = shift;
	my $strStart = shift;
	my $strEnd = shift;
	my $strReplace = shift;

	my @LinesNew;
	my $strReturn;

	my $bFound = 0;
	my $bOccurrences = 0;

	for my $Zeile (@{$refLines})
	{
		if($bFound)
		{
			if($Zeile =~ /$strEnd/)
			{
				$bFound = 0;
			}
			else
			{
				$strReturn .= $Zeile if($bOccurrences == 1);
			}
		}
		else
		{
			if($Zeile =~ /$strStart/)
			{
				$bFound = 1;
				$bOccurrences++;
				push @LinesNew, $strReplace;
			}
			else
			{
				push @LinesNew, $Zeile;
			}
		}
	}

	@{$refLines} = @LinesNew;

	$strReturn;
}

#
# Die Zeilen aus dem HTML-Template-Tag $tag in den Hash $refHashOrArray einlesen.
# Whitespaces am Anfang und Ende jeder Zeile aus dem Template werden entfernt.
# Leerzeilen und Zeilen, deren erstes Nicht-Whitespace-Zeichen ein # ist, werden ignoriert.
# Wenn die Zeilen das Pipe-Symbol | enthalten, werden die Teile vor und nach dem | als Key und Value in den angegebenen Hash geschrieben.
# Wenn die Zeilen kein Pipe-Symbol enthalten, werden die gelesenen Strings in das angegebene Array geschrieben.
#
sub ReadTemplateStrings
{
	my $tag = shift;
	my $refHashOrArray = shift;

	my $lines = ExtractAndReplaceLines(\@gTemplateLines, "<$tag>", "</$tag>", '');
	my $bHasPipe = (-1 ne index($lines, '|'));
	my @Lines = split(/\n/, $lines);
	for my $line (@Lines)
	{
		# Alle Whitespaces an Beginn und Ende entfernen:
		$line = (split(/\#/, $line))[0]; # cut off remark
		$line =~ s/^[\s]*([^\s])/$1/;
		$line =~ s/[\s]*$//;
		next if ('' eq $line);
		if ($bHasPipe)
		{
			my ($key, $value) = split(/\|/, $line);
			${$refHashOrArray}{$key} = $value;
		}
		else
		{
			push @{$refHashOrArray}, $line;
		}
	}
}

