#!/usr/bin/perl -w
#------------------------------------------------------------------------------
# Group edit CGI
#------------------------------------------------------------------------------

use strict;
use utf8;
use CGI;
use Encode qw(encode);
use HTML::Template;
use LWP::UserAgent;
use HTTP::Request;
use URI::Escape qw(uri_escape_utf8);
use TF::CommonUtils;
use TF::GroupList;

binmode(STDOUT, ":utf8");

my $BRAND         = $ENV{'TF_BRANDNAME'} || '';
my $LOCATION      = $ENV{'TF_LOCATION'} || '';
my $USERTYPE      = $ENV{'TF_USERTYPE'} || 'normal';
my $REMOTE_USER   = $ENV{'REMOTE_USER'} || '';
my $PASSWD        = $ENV{'TF_USERPLAINPASSWORD'} || '';
my $TF_TOKEN      = $ENV{'TF_TOKEN'} || '';
my $LANGPARAM     = TF::CommonUtils::getLangPriority();
my $TEMPLATE_FILE = './tmpl/groupedit.tmpl.' . $LANGPARAM;
my $BACKURI       = $LOCATION . '/.cgi-bin/groupmembers';
my ($SCHEMA, $SERVER_PORT, $URI_PREFIX) = TF::CommonUtils::getCgiConnectInfo();
my $BASE_URL       = $URI_PREFIX . $LOCATION;
my $GROUP_MGMT_URL = $URI_PREFIX . $LOCATION . '/.management/GROUP/';

my $ERRORTABLE_ja = {
	CGIError => 'CGIエラーです',
	invalidToken => '画面を再読み込みしてからやり直してください',
	required => '対象グループが指定されていません',
	invalidGroupName => 'グループ名の形式が不正です',
	notFound => '対象グループが見つかりません',
	updateFailed => 'グループ更新に失敗しました',
	moveFailed => 'グループ名の変更に失敗しました',
	alreadyExists => '同じグループ名が既に存在します',
	forbidden => 'このグループは更新できません',
	deleteFailed => 'グループの削除に失敗しました',
};
my $ERRORTABLE_en = {
	CGIError => 'CGI error found.',
	invalidToken => 'Reload the page and try again.',
	required => 'The target group is missing.',
	invalidGroupName => 'The group name format is invalid.',
	notFound => 'The target group was not found.',
	updateFailed => 'Failed to update the group.',
	moveFailed => 'Failed to rename the group.',
	alreadyExists => 'The same group name already exists.',
	forbidden => 'This group cannot be updated.',
	deleteFailed => 'Failed to delete the group.',
};
my $ERRORTABLE = ($LANGPARAM eq 'ja') ? $ERRORTABLE_ja : $ERRORTABLE_en;

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

main();

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

	my $groupid = trim(TF::CommonUtils::decodeUTF8($cgi->param('groupid') || ''));
	if (!$groupid) {
		TF::CommonUtils::showErrorPage($ERRORTABLE->{required}, $LANGPARAM);
		return;
	}

	my ($values_ref, $load_error) = load_group_values($groupid);
	if (!$values_ref) {
		TF::CommonUtils::showErrorPage($load_error || $ERRORTABLE->{notFound}, $LANGPARAM);
		return;
	}
	my %values = %{$values_ref};
	my $status = '';
	my $delete_command = $cgi->param('hidCmd') || '';
	if (($cgi->request_method || 'GET') eq 'POST' && ($delete_command eq 'delete' || $delete_command eq 'forceDelete')) {
		my $err = validate_csrf($cgi);
		my $force = $delete_command eq 'forceDelete' ? 1 : 0;
		if (!$err && $force) {
			my $confirmation = trim(TF::CommonUtils::decodeUTF8($cgi->param('confirm_groupname') || ''));
			$err = localize('deleteNameMismatch') if $confirmation ne $values{groupname};
		}
		if (!$err) {
			my ($ok, $errmsg, $code) = request_delete_group($values{managementuri}, get_session_id(), $force);
			if (!$ok && !$force && $code == 424) {
				$values{force_delete_required} = 1;
				$err = localize('deleteNotEmpty');
			}
			else {
				$err = $errmsg if !$ok;
			}
		}
		if (!$err) {
			TF::CommonUtils::redirectPage($BACKURI, $LANGPARAM);
			return;
		}
		$status = $err;
	}

	if (($cgi->request_method || 'GET') eq 'POST' && (($cgi->param('hidCmd') || '') eq 'set')) {
		%values = (%values, submitted_values($cgi));
		my $err = validate_csrf($cgi);
		if ($err) {
			$status = $err;
		}
		else {
			$err = validate_values(\%values);
			if ($err) {
				$status = $err;
			}
			else {
				my ($ok, $errmsg) = request_update_group(\%values, get_session_id());
				if ($ok) {
					TF::CommonUtils::redirectPage($BACKURI . '?groupid=' . uri_escape_utf8($groupid), $LANGPARAM);
					return;
				}
				$status = $errmsg;
			}
		}
	}

	show_response($cgi, \%values, $status);
}

sub load_group_values {
	my $groupid = shift || '';
	my $glist = TF::GroupList->new();
	my $code = $glist->queryGroupList($BASE_URL, $REMOTE_USER, $PASSWD, $groupid);
	if ($code < 200 || $code >= 300) {
		print STDERR "groupedit: group detail SEARCH failed ($code) groupid=$groupid\n";
		return (undef, $ERRORTABLE->{notFound});
	}
	if (!$glist->isExist($groupid)) {
		return (undef, $ERRORTABLE->{notFound});
	}
	return ({
		groupid => $groupid,
		groupname => $glist->getName($groupid) || '',
		old_groupname => $glist->getName($groupid) || '',
		managementuri => $glist->getManagementUri($groupid) || '',
		comment => $glist->getComment($groupid) || '',
		active => $glist->isActive($groupid) ? 'on' : 'off',
		restrict_write => $glist->hasWriteConstraints($groupid) ? 'on' : 'off',
		restrict_oplog => $glist->hasOplogConstraints($groupid) ? 'on' : 'off',
		restrict_showprop => $glist->hasShowpropConstraints($groupid) ? 'on' : 'off',
		box => $glist->isBox($groupid) ? 'on' : 'off',
		created => format_datetime($glist->getGroupCreationDt($groupid)),
		updated => format_datetime($glist->getGroupUpdateDt($groupid)),
	}, '');
}

sub show_response {
	my ($cgi, $values_ref, $status) = @_;
	my %values = %{$values_ref || {}};

	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,
			title => localize('title'),
			subtitle => localize('subtitle'),
			backUri => $BACKURI . '?groupid=' . uri_escape_utf8($values{groupid} || ''),
			backLabel => localize('backLabel'),
			saveLabel => localize('saveLabel'),
			deleteLabel => localize('deleteLabel'),
			deleteConfirmTitle => localize('deleteConfirmTitle'),
			deleteConfirmMessage => localize('deleteConfirmMessage'),
			deleteConfirmYes => localize('deleteConfirmYes'),
			deleteConfirmNo => localize('deleteConfirmNo'),
			forceDeleteRequired => $values{force_delete_required} ? 1 : 0,
			forceDeleteTitle => localize('forceDeleteTitle'),
			forceDeleteMessage => localize('forceDeleteMessage'),
			forceDeleteInputLabel => localize('forceDeleteInputLabel'),
			forceDeleteLabel => localize('forceDeleteLabel'),
			groupNameRaw => esc($values{groupname}),
			status => $status,
			hasStatus => $status ? 1 : 0,
			token => $TF_TOKEN,
			groupIdValue => esc($values{groupid}),
			sectionState => localize('sectionState'),
			sectionBasic => localize('sectionBasic'),
			sectionMeta => localize('sectionMeta'),
			groupNameLabel => localize('groupNameLabel'),
			groupNameDesc => localize('groupNameDesc'),
			groupNameValue => esc($values{groupname}),
			commentLabel => localize('commentLabel'),
			commentDesc => localize('commentDesc'),
			commentValue => esc($values{comment}),
			activeLabel => localize('activeLabel'),
			activeDesc => localize('activeDesc'),
			activeChecked => $values{active} eq 'on' ? 1 : 0,
			sectionAttributes => localize('sectionAttributes'),
			restrictWriteLabel => localize('restrictWriteLabel'),
			restrictWriteDesc => localize('restrictWriteDesc'),
			restrictWriteChecked => $values{restrict_write} eq 'on' ? 1 : 0,
			restrictOplogLabel => localize('restrictOplogLabel'),
			restrictOplogDesc => localize('restrictOplogDesc'),
			restrictOplogChecked => $values{restrict_oplog} eq 'on' ? 1 : 0,
			restrictShowpropLabel => localize('restrictShowpropLabel'),
			restrictShowpropDesc => localize('restrictShowpropDesc'),
			restrictShowpropChecked => $values{restrict_showprop} eq 'on' ? 1 : 0,
			boxLabel => localize('boxLabel'),
			boxDesc => localize('boxDesc'),
			boxChecked => $values{box} eq 'on' ? 1 : 0,
			boxConfirmTitle => localize('boxConfirmTitle'),
			boxConfirmMessage => localize('boxConfirmMessage'),
			boxConfirmYes => localize('boxConfirmYes'),
			boxConfirmNo => localize('boxConfirmNo'),
			toggleOnLabel => localize('toggleOnLabel'),
			toggleOffLabel => localize('toggleOffLabel'),
			createdLabel => localize('createdLabel'),
			updatedLabel => localize('updatedLabel'),
			createdValue => esc($values{created} || '-'),
			updatedValue => esc($values{updated} || '-'),
		);
		print $cgi->header(-charset => 'UTF-8');
		print $htmltmpl->output;
		close($tmplfh);
		return;
	}
	TF::CommonUtils::showTemplateOpenErr($LANGPARAM);
}

sub submitted_values {
	my $cgi = shift;
	return (
		groupname => trim(TF::CommonUtils::decodeUTF8($cgi->param('groupname') || '')),
		comment => TF::CommonUtils::decodeUTF8($cgi->param('comment') || ''),
		active => (($cgi->param('active') || '') eq 'on') ? 'on' : 'off',
		restrict_write => (($cgi->param('restrict_write') || '') eq 'on') ? 'on' : 'off',
		restrict_oplog => (($cgi->param('restrict_oplog') || '') eq 'on') ? 'on' : 'off',
		restrict_showprop => (($cgi->param('restrict_showprop') || '') eq 'on') ? 'on' : 'off',
		box => (($cgi->param('box') || '') eq 'on') ? 'on' : 'off',
	);
}

sub validate_csrf {
	my $cgi = shift;
	return $ERRORTABLE->{invalidToken} if !$TF_TOKEN || (($cgi->param('hidcsrftoken') || '') ne $TF_TOKEN);
	return '';
}

sub validate_values {
	my $values = shift || {};
	return $ERRORTABLE->{notFound} if !$values->{managementuri};
	return $ERRORTABLE->{required} if !$values->{groupname};
	return $ERRORTABLE->{invalidGroupName} if !is_valid_groupname($values->{groupname});
	return '';
}

sub request_update_group {
	my ($values, $sid) = @_;
	my $ua  = LWP::UserAgent->new(ssl_opts => { verify_hostname => 0 });
	$ua->agent(TF::CommonUtils::getCgiUserAgent());

	my $target_url = management_url($values->{managementuri});
	if (($values->{old_groupname} || '') ne ($values->{groupname} || '')) {
		my ($dest_path, $dest_url) = move_destination($values->{managementuri}, $values->{groupname});
		return (0, $ERRORTABLE->{moveFailed}) if !$dest_path || !$dest_url;

		my $move_req = HTTP::Request->new;
		$move_req->uri($target_url);
		$move_req->method('MOVE');
		$move_req->protocol('HTTP/1.1');
		$move_req->authorization_basic($REMOTE_USER, $PASSWD);
		$move_req->header('x-tf-session' => $sid) if $sid;
		$move_req->header('TF-Client-Info' => cgi_client_info());
		$move_req->header('Destination' => $dest_url);
		$move_req->header('Overwrite' => 'F');

		my $move_res = $ua->request($move_req);
		if (!$move_res->is_success) {
			my $code = $move_res->code || 500;
			my $status = $move_res->status_line || '';
			print STDERR "groupedit: MOVE failed ($code) $status\n";
			return (0, $ERRORTABLE->{forbidden}) if $code == 403;
			return (0, $ERRORTABLE->{notFound}) if $code == 404;
			return (0, $ERRORTABLE->{alreadyExists}) if $code == 409 || $code == 412;
			return (0, $ERRORTABLE->{invalidGroupName}) if $code == 400;
			return (0, $ERRORTABLE->{moveFailed} . " ($code)");
		}
		$values->{managementuri} = $dest_path;
		$target_url = $dest_url;
	}

	my $req = HTTP::Request->new;
	$req->uri($target_url);
	$req->method('PROPPATCH');
	$req->protocol('HTTP/1.1');
	$req->authorization_basic($REMOTE_USER, $PASSWD);
	$req->header('x-tf-session' => $sid) if $sid;
	$req->header('TF-Client-Info' => cgi_client_info());
	$req->header('Content-Type' => 'text/xml; charset=UTF-8');
	$req->content(encode('UTF-8', build_group_update_xml($values)));

	my $res = $ua->request($req);
	if ($res->is_success && (($res->code || 0) == 207)) {
		my $code = multistatus_code($res->content());
		return (1, '') if !$code || $code == 200;
		print STDERR "groupedit: PROPPATCH propstat failed ($code)\n";
		return (0, $ERRORTABLE->{updateFailed} . " ($code)");
	}
	return (1, '') if $res->is_success;

	my $code = $res->code || 500;
	my $status = $res->status_line || '';
	print STDERR "groupedit: PROPPATCH failed ($code) $status\n";
	return (0, $ERRORTABLE->{forbidden}) if $code == 403;
	return (0, $ERRORTABLE->{notFound}) if $code == 404;
	return (0, $ERRORTABLE->{updateFailed} . " ($code)");
}

sub request_delete_group {
	my ($managementuri, $sid, $force) = @_;
	return (0, $ERRORTABLE->{notFound}) if !$managementuri;
	my $ua = LWP::UserAgent->new(ssl_opts => { verify_hostname => 0 });
	my $req = HTTP::Request->new;
	$req->uri(management_url($managementuri));
	$req->method('DELETE');
	$req->protocol('HTTP/1.1');
	$req->authorization_basic($REMOTE_USER, $PASSWD);
	$req->header('x-tf-session' => $sid) if $sid;
	$req->header('TF-Client-Info' => cgi_client_info());
	$req->header('TF-Force-Delete' => 'T') if $force;
	$ua->agent(TF::CommonUtils::getCgiUserAgent());
	my $res = $ua->request($req);
	return (1, '', $res->code || 204) if $res->is_success;
	my $code = $res->code || 500;
	print STDERR "groupedit: DELETE failed ($code) " . ($res->status_line || '') . "\n";
	return (0, $ERRORTABLE->{forbidden}, $code) if $code == 403;
	return (0, $ERRORTABLE->{notFound}, $code) if $code == 404;
	return (0, $ERRORTABLE->{deleteFailed} . " ($code)", $code);
}

sub management_url {
	my $managementuri = shift || '';
	return $managementuri if $managementuri =~ m{^https?://}i;
	return $URI_PREFIX . $managementuri if $managementuri =~ m{^/};
	return $GROUP_MGMT_URL . $managementuri;
}

sub move_destination {
	my ($managementuri, $groupname) = @_;
	return ('', '') if !$managementuri || !$groupname;

	my $path = $managementuri;
	$path =~ s{^https?://[^/]+}{}i;
	$path =~ s{[?#].*$}{};
	$path =~ s{/$}{};
	return ('', '') if $path !~ m{/};

	my $parent = $path;
	$parent =~ s{/[^/]*$}{};
	my $dest_path = $parent . '/' . uri_escape_utf8($groupname);
	return ($dest_path, management_url($dest_path));
}

sub build_group_update_xml {
	my $values = shift || {};
	my $state_tag = ($values->{active} eq 'on') ? '<TF:active/>' : '<TF:inactive/>';
	my $box_tag = ($values->{box} eq 'on') ? '<TF:box><TF:active/></TF:box>' : '<TF:box><TF:inactive/></TF:box>';
	my $constraints_tag = build_constraints_xml($values);
	return '<?xml version="1.0" encoding="UTF-8" standalone="no" ?>'
		. '<D:propertyupdate xmlns:D="DAV:" xmlns:TF="http://www.teamfile.com/DTD/TF/">'
		. '<D:set><D:prop><TF:groupdiscovery><TF:groupinfo>'
		. '<TF:comment>' . escape_xml($values->{comment} || '') . '</TF:comment>'
		. '<TF:state>' . $state_tag . '</TF:state>'
		. $box_tag
		. $constraints_tag
		. '</TF:groupinfo></TF:groupdiscovery></D:prop></D:set></D:propertyupdate>';
}

sub build_constraints_xml {
	my $values = shift || {};
	my $restrict_write = $values->{restrict_write} eq 'on';
	my $restrict_oplog = $values->{restrict_oplog} eq 'on';
	my $restrict_showprop = $values->{restrict_showprop} eq 'on';

	if ($values->{box} eq 'on') {
		$restrict_write = 0;
		$restrict_oplog = 1;
		$restrict_showprop = 1;
	}

	my $xml = '<TF:constraints>';
	$xml .= '<TF:write/>' if $restrict_write;
	$xml .= '<TF:operationlog/>' if $restrict_oplog;
	$xml .= '<TF:showproperty/>' if $restrict_showprop;
	$xml .= '</TF:constraints>';
	return $xml;
}

sub cgi_client_info {
	# product=m, client=c, groupconstraints=1, groupleader=1, groupquota=0, accesscontrol=0, box=1, uploadpolicy=0
	return 'mc110010';
}

sub multistatus_code {
	my $data = shift || '';
	return undef if $data !~ m{<D:status>\s*HTTP/\S+\s+(\d+)}s;
	return int($1);
}

sub get_session_id {
	my @pairs = split(/;\s*/, $ENV{'HTTP_COOKIE'} || '');
	foreach my $pair (@pairs) {
		my ($key, $val) = split(/=/, $pair, 2);
		return $val || '' if defined $key && $key eq 'tf-session';
	}
	return '';
}

sub format_datetime {
	my $epoch = shift || 0;
	return '' if !$epoch;
	my @tm = localtime($epoch);
	return sprintf('%04d-%02d-%02d %02d:%02d:%02d', $tm[5] + 1900, $tm[4] + 1, $tm[3], $tm[2], $tm[1], $tm[0]);
}

sub trim {
	my $str = shift;
	return '' if !defined $str;
	$str =~ s/^[\s\x{3000}]+//;
	$str =~ s/[\s\x{3000}]+$//;
	return $str;
}

sub is_valid_groupname {
	my $groupname = shift || '';
	return 0 if $groupname eq '';
	return 0 if $groupname =~ m{/};
	return 0 if $groupname =~ /^\./;
	return 0 if $groupname =~ /[\x00-\x1f]/;
	return 1;
}

sub escape_xml {
	my $str = shift;
	return '' if !defined $str;
	$str =~ s/&/&amp;/g;
	$str =~ s/</&lt;/g;
	$str =~ s/>/&gt;/g;
	$str =~ s/"/&quot;/g;
	$str =~ s/'/&apos;/g;
	return $str;
}

sub esc {
	my $str = shift;
	return '' if !defined $str || $str eq '';
	my $escaped = TF::CommonUtils::escapeHTML($str, 0);
	return defined $escaped ? $escaped : '';
}

sub localize {
	my $key = shift || '';
	my %ja = (
		title => 'グループ編集',
		subtitle => 'グループのコメントと有効状態を更新します。',
		backLabel => 'グループ管理へ戻る',
		saveLabel => '保存',
		deleteLabel => 'グループ削除',
		deleteConfirmTitle => 'グループ削除',
		deleteConfirmMessage => 'グループが空の場合に削除します。ファイルや下位グループが残っている場合は削除せず、改めて確認します。',
		deleteConfirmYes => '削除',
		deleteConfirmNo => 'キャンセル',
		deleteNotEmpty => 'ファイルまたは下位グループが残っているため、通常削除できません。',
		forceDeleteTitle => 'グループと全ファイルの完全削除',
		forceDeleteMessage => 'この操作は、下位グループとグループフォルダ内のすべてのファイルを完全に削除します。元に戻すことはできません。続行するにはグループ名を入力してください。',
		forceDeleteInputLabel => '確認のためグループ名を入力',
		forceDeleteLabel => 'すべて削除',
		deleteNameMismatch => '入力したグループ名が一致しません。',
		sectionState => '状態',
		sectionBasic => '基本設定',
		sectionAttributes => '属性',
		sectionMeta => '管理情報',
		groupNameLabel => 'グループ名',
		groupNameDesc => 'グループ名を変更すると管理URIもMOVEで変更されます',
		commentLabel => 'コメント',
		commentDesc => 'グループの説明として表示されます',
		activeLabel => '有効状態',
		activeDesc => 'このグループを利用可能にするか',
		restrictWriteLabel => '書き込みを禁止',
		restrictWriteDesc => 'このグループ配下へのアップロードや更新を禁止します',
		restrictOplogLabel => '操作ログを閲覧禁止',
		restrictOplogDesc => 'このグループの操作ログを一般ユーザから見えないようにします',
		restrictShowpropLabel => 'プロパティの表示を禁止',
		restrictShowpropDesc => 'このグループの詳細プロパティ表示を制限します',
		boxLabel => 'このグループを公開する',
		boxDesc => 'アカウントを持たない第三者へファイルを渡せるBOXグループにします',
		boxConfirmTitle => 'グループ公開の確認',
		boxConfirmMessage => 'このグループに所属したすべてのユーザがアカウントを持たない第三者でファイルを渡すことができるようになります。よろしいですか？',
		boxConfirmYes => 'はい',
		boxConfirmNo => 'いいえ',
		toggleOnLabel => 'ON',
		toggleOffLabel => 'OFF',
		createdLabel => '登録日時',
		updatedLabel => '更新日時',
	);
	my %en = (
		title => 'Edit Group',
		subtitle => 'Update the group comment and active state.',
		backLabel => 'Back to group management',
		saveLabel => 'Save',
		deleteLabel => 'Delete group',
		deleteConfirmTitle => 'Delete group',
		deleteConfirmMessage => 'Delete this group only if it is empty. If files or child groups remain, nothing will be deleted and another confirmation will be required.',
		deleteConfirmYes => 'Delete',
		deleteConfirmNo => 'Cancel',
		deleteNotEmpty => 'The group is not empty and cannot be deleted normally.',
		forceDeleteTitle => 'Permanently delete the group and all files',
		forceDeleteMessage => 'This permanently deletes all child groups and every file in their group folders. Enter the group name to continue.',
		forceDeleteInputLabel => 'Enter the group name to confirm',
		forceDeleteLabel => 'Delete everything',
		deleteNameMismatch => 'The entered group name does not match.',
		sectionState => 'State',
		sectionBasic => 'Basic Settings',
		sectionAttributes => 'Attributes',
		sectionMeta => 'Metadata',
		groupNameLabel => 'Group name',
		groupNameDesc => 'Changing the group name also moves the management URI.',
		commentLabel => 'Comment',
		commentDesc => 'Displayed as the group description',
		activeLabel => 'Active state',
		activeDesc => 'Whether this group is usable',
		restrictWriteLabel => 'Disable write/upload',
		restrictWriteDesc => 'Prevent uploads and updates under this group',
		restrictOplogLabel => 'Hide operation logs',
		restrictOplogDesc => 'Prevent regular users from viewing operation logs for this group',
		restrictShowpropLabel => 'Hide properties',
		restrictShowpropDesc => 'Restrict detailed property display for this group',
		boxLabel => 'Publish this group',
		boxDesc => 'Enable BOX sharing for users in this group',
		boxConfirmTitle => 'Confirm Group Publishing',
		boxConfirmMessage => 'All users who belong to this group will be able to send files to third parties who do not have accounts. Continue?',
		boxConfirmYes => 'Yes',
		boxConfirmNo => 'No',
		toggleOnLabel => 'ON',
		toggleOffLabel => 'OFF',
		createdLabel => 'Created at',
		updatedLabel => 'Updated at',
	);
	my $table = ($LANGPARAM eq 'ja') ? \%ja : \%en;
	return exists $table->{$key} ? $table->{$key} : $key;
}
