#!/usr/bin/perl -w
#------------------------------------------------------------------------------
# Location general configuration CGI
#
# Copyright (c) BAYBITS LLC. All rights reserved.
#------------------------------------------------------------------------------

use strict;
use utf8;
use CGI;
use HTML::Template;
use TF::CommonUtils;
use TF::SingleLock;
use File::Copy;

binmode(STDOUT, ":utf8");

my $LOCATION        = $ENV{'TF_LOCATION'} || '';
my $BRAND           = $ENV{'TF_BRANDNAME'} || '';
my $USERTYPE        = $ENV{'TF_USERTYPE'} || 'normal';
my $LANGPARAM       = TF::CommonUtils::getLangPriority();
my $TF_TOKEN        = $ENV{'TF_TOKEN'} || undef;
my $TEMPLATE_NAME   = 'general.tmpl.' . $LANGPARAM;
my $BACKURI         = $LOCATION . "/.cgi-bin/admin/settings";
my $CONFIG_ROOT     = $ENV{'TF_CONFIG_ROOT'} || '/usr/local/teamfile/www/conf/conf.d';
my $LOCATION_NAME   = $LOCATION;
$LOCATION_NAME =~ s/\///g;

my $OVERRIDE_DIR    = $CONFIG_ROOT . "/location-overrides/" . $LOCATION_NAME;
my $OVERRIDE_FILE   = $OVERRIDE_DIR . "/general.conf";
my $BASE_FILE       = $CONFIG_ROOT . "/new_" . $LOCATION_NAME . ".conf";
my $LOCK_FILE       = $OVERRIDE_DIR . "/.general.lock";
my $DEFAULT_LOGO    = '/icons/teamfile/tf_logo.png';
my $CUSTOM_IMG_ROOT = readCustomImageRoot();
my $SERVER_NAME     = resolveServerName();
my $CUSTOM_IMG_DIR  = $CUSTOM_IMG_ROOT . "/" . $SERVER_NAME . $LOCATION;
my $CUSTOM_IMG_FILE = $CUSTOM_IMG_DIR . "/logo.png";
my $CUSTOM_IMG_URL  = "/icons/teamfile/custom/" . $SERVER_NAME . $LOCATION . "/logo.png";
my $LOGO_MAX_SIZE   = 4 * 1024 * 1024;

my %LABELS = (
	TfNotifyServerName => {
		label_ja => '通知URL ホスト名',
		label_en => 'Notification host name',
		desc_ja  => '通知メールや公開URLで使用するホスト名。未設定時は自動判定します。 (TfNotifyServerName)',
		desc_en  => 'Host name used in notification mail and public URLs. Auto-detected when empty. (TfNotifyServerName)',
	},
	TfNotifyServerPort => {
		label_ja => '通知URL ポート番号',
		label_en => 'Notification port',
		desc_ja  => '通知メールや公開URLで使用するポート番号。auto または数値。 (TfNotifyServerPort)',
		desc_en  => 'Port used in notification mail and public URLs. Use auto or a numeric value. (TfNotifyServerPort)',
	},
	TfNotifyServerSchema => {
		label_ja => '通知URL スキーマ',
		label_en => 'Notification scheme',
		desc_ja  => '通知メールや公開URLで使用するスキーマ。auto / http / https。 (TfNotifyServerSchema)',
		desc_en  => 'Scheme used in notification mail and public URLs. auto / http / https. (TfNotifyServerSchema)',
	},
	logo => {
		label_ja => 'ロゴ画像',
		label_en => 'Logo image',
		desc_ja  => '左上ロゴに表示する PNG 画像。未設定時は既定ロゴを使用します。',
		desc_en  => 'PNG image shown as the top-left logo. The default logo is used when unset.',
	},
);

my $MSGTABLE_ja = {
	invalidPrm     => '値が不正です',
	writeError     => '設定の書き込みに失敗しました',
	dirNotWritable => '設定ディレクトリに書き込みできません',
	serverBusy     => 'サーバがビジーです。しばらく経ってからやり直してください',
	invalidLogo    => 'ロゴ画像は PNG ファイルを指定してください',
	logoTooLarge   => 'ロゴ画像のサイズが大きすぎます',
	logoWriteError => 'ロゴ画像の保存に失敗しました',
	invalidPort    => 'ポート番号は auto または数値で指定してください',
	invalidSchema  => 'スキーマは auto / http / https を指定してください',
};
my $MSGTABLE_en = {
	invalidPrm     => 'Invalid parameter.',
	writeError     => 'Failed to write settings.',
	dirNotWritable => 'The settings directory is not writable.',
	serverBusy     => 'The server is busy. Please retry this operation after a while.',
	invalidLogo    => 'Please choose a PNG file for the logo image.',
	logoTooLarge   => 'The logo image is too large.',
	logoWriteError => 'Failed to save the logo image.',
	invalidPort    => 'Specify auto or a numeric value for the port.',
	invalidSchema  => 'Specify auto, http, or https for the scheme.',
};
my $MSGTABLE = ($LANGPARAM eq 'ja') ? $MSGTABLE_ja : $MSGTABLE_en;

if (!$LOCATION || !$USERTYPE || $USERTYPE ne 'admin') {
	TF::CommonUtils::showAccessDeny();
	die '';
}

main();

sub main {
	my $cgi = CGI->new;
	if (!$cgi || $cgi->cgi_error) {
		TF::CommonUtils::showErrorPage($MSGTABLE->{invalidPrm}, $LANGPARAM);
		return;
	}

	my $cmd = $cgi->param('hidCmd') || '';
	if ($cmd eq 'set') {
		storeSettings($cgi);
		return;
	}

	showResponse($cgi, '');
}

sub storeSettings {
	my $cgi = shift;

	if (!defined $TF_TOKEN || (($cgi->param('hidcsrftoken') || '') ne $TF_TOKEN)) {
		TF::CommonUtils::showErrorPage($MSGTABLE->{invalidPrm}, $LANGPARAM);
		return;
	}

	my $notify_name   = normalizeOptionalText(TF::CommonUtils::decodeUTF8($cgi->param('TfNotifyServerName') || ''));
	my $notify_port   = normalizeNotifyPort(TF::CommonUtils::decodeUTF8($cgi->param('TfNotifyServerPort') || ''));
	my $notify_schema = normalizeNotifySchema(TF::CommonUtils::decodeUTF8($cgi->param('TfNotifyServerSchema') || ''));
	if (!validateOptionalSingleLine($notify_name)) {
		showResponse($cgi, $MSGTABLE->{invalidPrm} . ' (TfNotifyServerName)');
		return;
	}
	if (!defined $notify_port) {
		showResponse($cgi, $MSGTABLE->{invalidPort});
		return;
	}
	if (!defined $notify_schema) {
		showResponse($cgi, $MSGTABLE->{invalidSchema});
		return;
	}

	my $delete_logo = ($cgi->param('deleteLogo') || '') eq '1' ? 1 : 0;
	my $upload_fh = $cgi->upload('logoFile');
	if ($upload_fh && !validateLogoUpload($cgi, 'logoFile')) {
		showResponse($cgi, $MSGTABLE->{invalidLogo});
		return;
	}

	if (!-d $OVERRIDE_DIR && !mkdirParents($OVERRIDE_DIR)) {
		TF::CommonUtils::showErrorPage($MSGTABLE->{writeError}, $LANGPARAM);
		return;
	}
	if (!-w $OVERRIDE_DIR) {
		TF::CommonUtils::showErrorPage($MSGTABLE->{dirNotWritable}, $LANGPARAM);
		return;
	}

	my $lock = TF::SingleLock->new($LOCK_FILE);
	if (!$lock->waitForLocking(5)) {
		TF::CommonUtils::showErrorPage($MSGTABLE->{serverBusy}, $LANGPARAM);
		return;
	}

	my $tmp = $OVERRIDE_FILE . ".$$.tmp";
	my $fh;
	if (!open($fh, ">", $tmp)) {
		$lock->release();
		TF::CommonUtils::showErrorPage($MSGTABLE->{writeError}, $LANGPARAM);
		return;
	}
	binmode($fh, ":utf8");
	print $fh "# This file is managed by TeamFile CGI.\n";
	print $fh "# Location: $LOCATION_NAME\n\n";
	printDirective($fh, 'TfNotifyServerName', $notify_name) if $notify_name ne '';
	printDirective($fh, 'TfNotifyServerPort', $notify_port);
	printDirective($fh, 'TfNotifyServerSchema', $notify_schema);
	my $legacy_failed_login = readLegacyFailedLoginDirective();
	print $fh sprintf("%-24s %s\n", 'TfFailedLoginControl', $legacy_failed_login)
		if defined $legacy_failed_login;
	close($fh);

	if (!rename($tmp, $OVERRIDE_FILE)) {
		unlink($tmp);
		$lock->release();
		TF::CommonUtils::showErrorPage($MSGTABLE->{writeError}, $LANGPARAM);
		return;
	}

	if ($delete_logo && -f $CUSTOM_IMG_FILE) {
		if (!unlink($CUSTOM_IMG_FILE)) {
			logError("failed to delete logo file: $CUSTOM_IMG_FILE: $!");
		}
	}
	if ($upload_fh) {
		if (!-d $CUSTOM_IMG_DIR && !mkdirParents($CUSTOM_IMG_DIR, 0755)) {
			$lock->release();
			TF::CommonUtils::showErrorPage($MSGTABLE->{logoWriteError}, $LANGPARAM);
			return;
		}
		if (!saveUploadedLogo($upload_fh, $CUSTOM_IMG_FILE)) {
			$lock->release();
			TF::CommonUtils::showErrorPage($MSGTABLE->{logoWriteError}, $LANGPARAM);
			return;
		}
	}

	$lock->release();
	TF::CommonUtils::redirectPage($BACKURI, $LANGPARAM);
}

sub showResponse {
	my $cgi = shift;
	my $status = shift || '';
	my %params = readGeneralSettings();
	my $current_logo = (-f $CUSTOM_IMG_FILE) ? $CUSTOM_IMG_URL : $DEFAULT_LOGO;
	my $notify_host_value = defined $params{TfNotifyServerName} ? normalizeOptionalText($params{TfNotifyServerName}) : '';
	my $notify_port_value = defined $params{TfNotifyServerPort} ? normalizeNotifyPort($params{TfNotifyServerPort}) : 'auto';
	my $notify_schema_value = lc($params{TfNotifyServerSchema} || 'auto');
	my $escaped_notify_host_value = ($notify_host_value ne '')
		? TF::CommonUtils::escapeHTML($notify_host_value, 0)
		: '';
	my $escaped_notify_port_value = (defined $notify_port_value && $notify_port_value ne '')
		? TF::CommonUtils::escapeHTML($notify_port_value, 0)
		: 'auto';

	my $template_file = findTemplateFile($TEMPLATE_NAME);
	if (open(my $tmplfh, "<", $template_file)) {
		binmode($tmplfh, ":utf8");
		my $htmltmpl = HTML::Template->new(
			filehandle => $tmplfh,
			case_sensitive => 1,
			die_on_bad_params => 0,
		);
		$htmltmpl->param(
			brandname => $BRAND,
			backUri => $BACKURI,
			status => $status,
			token => $TF_TOKEN,
			logoLabel => localized('logo', 'label'),
			logoDesc => localized('logo', 'desc'),
			logoSrc => $current_logo,
			hasCustomLogo => (-f $CUSTOM_IMG_FILE) ? 1 : 0,
			notifyHostLabel => localized('TfNotifyServerName', 'label'),
			notifyHostDesc => localized('TfNotifyServerName', 'desc'),
			notifyHostValue => $escaped_notify_host_value,
			notifyPortLabel => localized('TfNotifyServerPort', 'label'),
			notifyPortDesc => localized('TfNotifyServerPort', 'desc'),
			notifyPortValue => $escaped_notify_port_value,
			notifySchemaLabel => localized('TfNotifyServerSchema', 'label'),
			notifySchemaDesc => localized('TfNotifyServerSchema', 'desc'),
			notifySchemaValue => $notify_schema_value,
			notifySchemaAuto => ($notify_schema_value eq 'auto') ? 1 : 0,
			notifySchemaHttp => ($notify_schema_value eq 'http') ? 1 : 0,
			notifySchemaHttps => ($notify_schema_value eq 'https') ? 1 : 0,
			deleteLogoText => ($LANGPARAM eq 'ja') ? '既定ロゴへ戻す' : 'Use default logo',
		);
		print $cgi->header(-charset => 'utf-8');
		print $htmltmpl->output;
		close($tmplfh);
	}
	else {
		TF::CommonUtils::showTemplateOpenErr($LANGPARAM);
	}
}

sub readGeneralSettings {
	my %params = (
		TfNotifyServerName => '',
		TfNotifyServerPort => 'auto',
		TfNotifyServerSchema => 'auto',
	);
	readDirectiveFile($BASE_FILE, \%params);
	readDirectiveFile($OVERRIDE_FILE, \%params);
	return %params;
}

sub readDirectiveFile {
	my $path = shift || '';
	my $params_ref = shift;
	return if !$path || !-f $path;

	if (open(my $fh, "<", $path)) {
		while (my $line = <$fh>) {
			chomp($line);
			$line =~ s/\r$//;
			$line =~ s/^\s+//;
			next if $line eq '' || $line =~ /^#/;
			if ($line =~ /^(TfNotifyServerName|TfNotifyServerPort|TfNotifyServerSchema)\s+(.+?)\s*$/) {
				my ($key, $value) = ($1, $2);
				$value =~ s/\s+#.*$//;
				$value =~ s/^"(.*)"$/$1/;
				$value =~ s/^'(.*)'$/$1/;
				$params_ref->{$key} = $value;
			}
		}
		close($fh);
	}
}

sub normalizeOptionalText {
	my $value = shift || '';
	$value =~ s/^[\s\x{3000}]+//;
	$value =~ s/[\s\x{3000}]+$//;
	return $value;
}

sub validateOptionalSingleLine {
	my $value = shift || '';
	return 0 if $value =~ /[\r\n]/;
	return 1;
}

sub normalizeNotifyPort {
	my $value = normalizeOptionalText(shift || '');
	return 'auto' if $value eq '';
	return lc($value) if lc($value) eq 'auto';
	return $value if $value =~ /^\d+$/;
	return undef;
}

sub normalizeNotifySchema {
	my $value = normalizeOptionalText(shift || '');
	return 'auto' if $value eq '';
	$value = lc($value);
	return $value if $value eq 'auto' || $value eq 'http' || $value eq 'https';
	return undef;
}

sub validateLogoUpload {
	my $cgi = shift;
	my $field = shift || '';
	my $upload = $cgi->upload($field);
	return 1 if !$upload;

	my $filename = $cgi->param($field) || '';
	return 0 if $filename !~ /\.png$/i;

	my $size = 0;
	my $header = '';
	seek($upload, 0, 0);
	read($upload, $header, 8);
	seek($upload, 0, 0);
	return 0 if $header ne "\x89PNG\x0d\x0a\x1a\x0a";

	while (read($upload, my $buf, 8192)) {
		$size += length($buf);
		if ($size > $LOGO_MAX_SIZE) {
			seek($upload, 0, 0);
			return 0;
		}
	}
	seek($upload, 0, 0);
	return 1;
}

sub saveUploadedLogo {
	my $src = shift;
	my $dst = shift || '';
	my $tmp = $dst . ".$$.tmp";

	if (open(my $out, ">", $tmp)) {
		binmode($out);
		binmode($src);
		if (!seek($src, 0, 0)) {
			logError("failed to seek logo upload stream: $!");
			close($out);
			unlink($tmp);
			return 0;
		}
		while (1) {
			my $buf = '';
			my $bytes = read($src, $buf, 8192);
			if (!defined $bytes) {
				logError("failed to read logo upload stream: $!");
				close($out);
				unlink($tmp);
				return 0;
			}
			last if $bytes == 0;
			if (!print $out $buf) {
				logError("failed to write logo temp file: $tmp: $!");
				close($out);
				unlink($tmp);
				return 0;
			}
		}
		if (!close($out)) {
			logError("failed to close logo temp file: $tmp: $!");
			unlink($tmp);
			return 0;
		}
		if (!chmod(0644, $tmp)) {
			logError("failed to chmod logo temp file: $tmp: $!");
			unlink($tmp);
			return 0;
		}
		if (!rename($tmp, $dst)) {
			logError("failed to rename logo file: $tmp -> $dst: $!");
			unlink($tmp);
			return 0;
		}
		return 1;
	}
	logError("failed to open logo temp file: $tmp: $!");
	return 0;
}

sub localized {
	my $key = shift || '';
	my $name = shift || '';
	my $entry = $LABELS{$key} || {};
	return $entry->{$name . '_' . $LANGPARAM} || $entry->{$name . '_en'} || '';
}

sub printDirective {
	my ($fh, $key, $value) = @_;
	return if !defined $value || $value eq '';
	print $fh sprintf("%-24s %s\n", $key, quoteApacheValue($value));
}

sub readLegacyFailedLoginDirective {
	return undef if !-f $OVERRIDE_FILE || !open(my $fh, '<', $OVERRIDE_FILE);
	while (my $line = <$fh>) {
		if ($line =~ /^\s*TfFailedLoginControl\s+(\d+(?:\s+\d+(?:\s+\d+)?)?)\s*(?:#.*)?$/) {
			my $value = $1;
			close($fh);
			return $value;
		}
	}
	close($fh);
	return undef;
}

sub quoteApacheValue {
	my $value = shift || '';
	if ($value =~ /[\s"#]/) {
		$value =~ s/\\/\\\\/g;
		$value =~ s/"/\\"/g;
		return '"' . $value . '"';
	}
	return $value;
}

sub mkdirParents {
	my $dir = shift || '';
	my $mode = shift || 0777;
	my @parts = split(/\//, $dir);
	my $path = '';

	foreach my $part (@parts) {
		next if $part eq '';
		$path .= '/' . $part;
		next if -d $path;
		if (!mkdir($path)) {
			logError("failed to create directory: $path: $!");
			return 0;
		}
		if (!chmod($mode, $path)) {
			logError("failed to chmod directory: $path: $!");
			return 0;
		}
	}
	return 1;
}

sub logError {
	my $message = shift || '';
	print STDERR "admin/general: $message\n";
}

sub findTemplateFile {
	my $name = shift || '';
	my @candidates = ("./tmpl/$name", "../tmpl/$name");

	foreach my $path (@candidates) {
		return $path if -f $path;
	}
	return $candidates[0];
}

sub readCustomImageRoot {
	my $path = $CONFIG_ROOT . '/mod_dav_tf.conf.default';
	if (open(my $fh, '<', $path)) {
		while (my $line = <$fh>) {
			next if $line =~ /^\s*#/;
			if ($line =~ /^\s*TfCustomImagePath\s+(.+?)\s*$/) {
				my $value = $1;
				$value =~ s/^"(.*)"$/$1/;
				close($fh);
				return $value;
			}
		}
		close($fh);
	}
	return '/usr/local/teamfile/www/icons/teamfile/custom';
}

sub resolveServerName {
	my $name = $ENV{'SERVER_NAME'} || $ENV{'HTTP_HOST'} || 'localhost';
	$name =~ s/:\d+$//;
	return $name;
}
