#!/usr/bin/perl -w
#------------------------------------------------------------------------------
# User 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::UserPermissions;
use TF::UserExpiration;
use TF::UserList;

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/useredit.tmpl.' . $LANGPARAM;
my $BACKURI       = $LOCATION . '/.cgi-bin/user';
my ($SCHEMA, $SERVER_PORT, $URI_PREFIX) = TF::CommonUtils::getCgiConnectInfo();
my $BASE_URL      = $URI_PREFIX . $LOCATION;
my $USER_MGMT_URL = $BASE_URL . '/.management/USER/';
my $ALLOWHOSTS_MAX_LENGTH = 255;

my $ERRORTABLE_ja = {
	CGIError => 'CGIエラーです',
	invalidToken => '画面を再読み込みしてからやり直してください',
	required => '未入力の項目があります',
	passwordMismatch => 'パスワード確認が一致しません',
	invalidUserType => '選択できないユーザ種別です',
		invalidUserId => 'ユーザIDの形式が不正です',
		invalidMail => 'メールアドレスの形式が不正です',
		invalidAllowhosts => '許可IPアドレスの形式が不正です',
		allowhostsTooLong => '許可IPアドレスは255文字以内で入力してください',
		invalidQuota => '利用上限の入力値が不正です',
		invalidExpiration => '有効期限の日時を正しく入力してください',
	notFound => '対象ユーザが見つかりません',
	updateFailed => 'ユーザ更新に失敗しました',
	deleteFailed => 'ユーザ削除に失敗しました',
	forbidden => 'このユーザは更新できません',
	selfDeleteForbidden => 'ログイン中のユーザは削除できません',
	selfTypeForbidden => 'ログイン中のユーザはユーザ種別を変更できません',
};
my $ERRORTABLE_en = {
	CGIError => 'CGI error found.',
	invalidToken => 'Reload the page and try again.',
	required => 'Some required fields are missing.',
	passwordMismatch => 'Password confirmation does not match.',
	invalidUserType => 'The selected user type is not allowed.',
		invalidUserId => 'The user ID format is invalid.',
		invalidMail => 'The mail address format is invalid.',
		invalidAllowhosts => 'The allowed IP address format is invalid.',
		allowhostsTooLong => 'Allowed IP addresses must be 255 characters or fewer.',
		invalidQuota => 'The quota value is invalid.',
		invalidExpiration => 'Enter a valid expiration date and time.',
	notFound => 'The target user was not found.',
	updateFailed => 'Failed to update the user.',
	deleteFailed => 'Failed to delete the user.',
	forbidden => 'This user cannot be updated.',
	selfDeleteForbidden => 'The signed-in user cannot be deleted.',
	selfTypeForbidden => 'The signed-in user type cannot be changed.',
};
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) {
		TF::CommonUtils::showErrorPage($ERRORTABLE->{CGIError}, $LANGPARAM);
		return;
	}
	if ($cgi->cgi_error) {
		TF::CommonUtils::showErrorPage($ERRORTABLE->{CGIError} . '(' . $cgi->cgi_error . ')', $LANGPARAM);
		return;
	}

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

	my ($loaded, $load_error) = load_user_values($target);
	if (!$loaded) {
		TF::CommonUtils::showErrorPage($load_error, $LANGPARAM);
		return;
	}

	my %values = %{$loaded};
	my $is_self = ($target eq $REMOTE_USER) ? 1 : 0;
	my $status = '';
	if (($cgi->request_method || 'GET') eq 'POST' && (($cgi->param('hidCmd') || '') eq 'delete')) {
		my $err = validate_csrf($cgi);
		if ($err) {
			$status = $err;
		}
		elsif ($is_self) {
			$status = $ERRORTABLE->{selfDeleteForbidden};
		}
		else {
			my ($ok, $errmsg) = request_delete_user($target, get_session_id());
			if ($ok) {
				TF::CommonUtils::redirectPage($BACKURI, $LANGPARAM);
				return;
			}
			$status = $errmsg;
		}
	}
	elsif (($cgi->request_method || 'GET') eq 'POST' && (($cgi->param('hidCmd') || '') eq 'set')) {
		%values = (%values, submitted_values($cgi));
		%values = (%values, TF::UserPermissions::submitted($cgi, $loaded, 0, $is_self,
			($ENV{TF_GROUPCONSTRAINTS} || '') eq 'on'));
		%values = (%values, TF::UserExpiration::submitted($cgi, $loaded, 0, $is_self));
		my $err = validate_csrf($cgi);
		if ($err) {
			$status = $err;
		}
		elsif ($is_self && (($values{usertype} || '') ne ($loaded->{usertype} || ''))) {
			$status = $ERRORTABLE->{selfTypeForbidden};
			$values{usertype} = $loaded->{usertype};
		}
		else {
			$err = validate_values(\%values, $is_self);
			if ($err) {
				$status = $err;
			}
			else {
				$values{password} = $values{new_password} ne '' ? $values{new_password} : $loaded->{password};
				my ($ok, $errmsg) = request_update_user($target, \%values, get_session_id());
				if ($ok) {
					TF::CommonUtils::redirectPage($BACKURI, $LANGPARAM);
					return;
				}
				$status = $errmsg;
			}
		}
		$values{new_password} = '';
		$values{password_confirm} = '';
	}

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

sub showResponse {
	my ($cgi, $values_ref, $status) = @_;
	my %values = %{$values_ref || {}};
	my $is_self = (($values{userid} || '') eq $REMOTE_USER) ? 1 : 0;

	my @type_rows = ();
	push @type_rows, radio_row('normal', localize('typeNormal'), ($values{usertype} || '') eq 'normal');
	if ($USERTYPE eq 'admin') {
		push @type_rows, radio_row('groupleader', localize('typeGroupLeader'), ($values{usertype} || '') eq 'groupleader');
		push @type_rows, radio_row('admin', localize('typeAdmin'), ($values{usertype} || '') eq 'admin');
	}
	push @type_rows, radio_row('limited', localize('typeLimited'), ($values{usertype} || '') eq 'limited');

	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(
			TF::UserPermissions::template_values(\%values, $is_self, ($ENV{TF_GROUPCONSTRAINTS} || '') eq 'on'),
			TF::UserExpiration::template_values(\%values, $is_self),
			brandname => $BRAND,
			title => localize('title'),
			subtitle => localize('subtitle'),
			backUri => $BACKURI,
			backLabel => localize('backLabel'),
			saveLabel => localize('saveLabel'),
			deleteLabel => localize('deleteLabel'),
				deleteDialogTitle => localize('deleteDialogTitle'),
				deleteConfirm => localize('deleteConfirm'),
				deleteCancelLabel => localize('deleteCancelLabel'),
				deleteConfirmLabel => localize('deleteConfirmLabel'),
				clientPasswordMismatch => localize('clientPasswordMismatch'),
				allowhostsTooLong => localize('allowhostsTooLong'),
				status => $status,
			hasStatus => $status ? 1 : 0,
			token => $TF_TOKEN,
			sectionState => localize('sectionState'),
			sectionBasic => localize('sectionBasic'),
			sectionQuota => localize('sectionQuota'),
			userIdLabel => localize('userIdLabel'),
			userIdDesc => localize('userIdDesc'),
			userIdValue => esc($values{userid}),
			nameLabel => localize('nameLabel'),
			nameDesc => localize('nameDesc'),
			nameValue => esc($values{fullname}),
			mailLabel => localize('mailLabel'),
			mailDesc => localize('mailDesc'),
			mailValue => esc($values{mailaddr}),
			mailPlaceholder => localize('mailPlaceholder'),
			allowhostsLabel => localize('allowhostsLabel'),
			allowhostsDesc => localize('allowhostsDesc'),
			allowhostsValue => esc($values{allowhosts}),
			allowhostsPlaceholder => localize('allowhostsPlaceholder'),
			removeAddressLabel => localize('removeAddressLabel'),
			passwordLabel => localize('passwordLabel'),
			passwordDesc => localize('passwordDesc'),
			passwordValue => '',
			passwordToggleLabel => localize('passwordToggleLabel'),
			passwordHideLabel => localize('passwordHideLabel'),
			passwordConfirmLabel => localize('passwordConfirmLabel'),
			passwordConfirmDesc => localize('passwordConfirmDesc'),
			passwordConfirmValue => '',
			commentLabel => localize('commentLabel'),
			commentDesc => localize('commentDesc'),
			commentValue => esc($values{comment}),
			maxStorageLabel => localize('maxStorageLabel'),
			maxStorageDesc => localize('maxStorageDesc'),
			maxStorageValue => esc($values{maxstorage_display}),
			unlimitedStorageLabel => localize('unlimitedStorageLabel'),
			maxStorageUnlimitedChecked => $values{maxstorage_unlimited} ? 1 : 0,
			maxResourceLabel => localize('maxResourceLabel'),
			maxResourceDesc => localize('maxResourceDesc'),
			maxResourceValue => esc($values{maxresource_display}),
			unlimitedResourceLabel => localize('unlimitedResourceLabel'),
			maxResourceUnlimitedChecked => $values{maxresource_unlimited} ? 1 : 0,
			activeLabel => localize('activeLabel'),
			activeDesc => localize('activeDesc'),
			activeChecked => $values{active} eq 'on' ? 1 : 0,
			expirationLabel => localize('expirationLabel'),
			expirationDesc => localize('expirationDesc'),
			expirationUnlimitedLabel => localize('expirationUnlimitedLabel'),
			toggleOnLabel => localize('toggleOnLabel'),
			toggleOffLabel => localize('toggleOffLabel'),
			typeLabel => localize('typeLabel'),
			typeDesc => localize('typeDesc'),
			typeRows => \@type_rows,
			canDelete => $is_self ? 0 : 1,
			canChangeUserType => $is_self ? 0 : 1,
			userTypeValue => esc($values{usertype}),
			userTypeStaticLabel => usertype_label($values{usertype}),
		);
		print $cgi->header(-charset => 'utf-8');
		print $htmltmpl->output;
		close($tmplfh);
	}
	else {
		TF::CommonUtils::showTemplateOpenErr($LANGPARAM);
	}
}

sub load_user_values {
	my $target = shift || '';
	my $ulist = TF::UserList->new();
	my $status = $ulist->queryUser($BASE_URL, $REMOTE_USER, $PASSWD, $target);
	return (undef, $ERRORTABLE->{notFound} . " ($status)") if $status != 207 || !$ulist->hasUserItem($target);

	my $maxstorage = $ulist->getMaxStorage($target) || '0';
	my $maxresource = $ulist->getMaxResource($target) || '0';
	return ({
		userid => $target,
		active => $ulist->isActive($target) ? 'on' : 'off',
		usertype => $ulist->getUserType($target) || 'normal',
		fullname => TF::CommonUtils::decodeUTF8($ulist->getName($target) || ''),
		mailaddr => normalize_mail_list($ulist->getMailAddr($target) || ''),
		allowhosts => normalize_allowhosts($ulist->getAllowHosts($target) || ''),
		password => $ulist->getPassword($target) || '',
		new_password => '',
		password_confirm => '',
		maxstorage => $maxstorage,
		maxstorage_display => $maxstorage,
		maxstorage_unlimited => ($maxstorage eq '0') ? 1 : 0,
		maxresource => $maxresource,
		maxresource_display => $maxresource,
		maxresource_unlimited => ($maxresource eq '0') ? 1 : 0,
		accessdeny => $ulist->getAccessDeny($target) || '',
		comment => $ulist->getComment($target) || '',
		maxusercreation => $ulist->getMaxUserCreation($target),
		TF::UserExpiration::from_iso8601($ulist->getExpiration($target) || ''),
		TF::UserPermissions::from_user($ulist, $target),
	}, '');
}

sub submitted_values {
	my $cgi = shift;
	my %values;

	$values{active} = (($cgi->param('active') || '') eq 'on') ? 'on' : 'off';
	$values{usertype} = trim(TF::CommonUtils::decodeUTF8($cgi->param('usertype') || 'normal'));
	$values{userid} = trim(TF::CommonUtils::decodeUTF8($cgi->param('userid') || ''));
	$values{fullname} = trim(TF::CommonUtils::decodeUTF8($cgi->param('fullname') || ''));
	$values{mailaddr} = normalize_mail_list(TF::CommonUtils::decodeUTF8($cgi->param('mailaddr') || ''));
	$values{allowhosts} = normalize_allowhosts(TF::CommonUtils::decodeUTF8($cgi->param('allowhosts') || ''));
	$values{new_password} = TF::CommonUtils::decodeUTF8($cgi->param('new_user_password') || '');
	$values{password_confirm} = TF::CommonUtils::decodeUTF8($cgi->param('new_user_password_confirm') || '');
	$values{comment} = TF::CommonUtils::decodeUTF8($cgi->param('comment') || '');

	my $raw_maxstorage = trim(TF::CommonUtils::decodeUTF8($cgi->param('maxstorage') || '0'));
	my $raw_maxresource = trim(TF::CommonUtils::decodeUTF8($cgi->param('maxresource') || '0'));

	$values{maxstorage_unlimited} = (($cgi->param('maxstorage_unlimited') || '') eq 'on') ? 1 : 0;
	$values{maxresource_unlimited} = (($cgi->param('maxresource_unlimited') || '') eq 'on') ? 1 : 0;

	if ($values{maxstorage_unlimited}) {
		$values{maxstorage_display} = '0';
		$values{maxstorage} = '0';
	}
	else {
		$values{maxstorage_display} = $raw_maxstorage;
		$values{maxstorage} = expand_scaled_number($raw_maxstorage);
	}

	if ($values{maxresource_unlimited}) {
		$values{maxresource_display} = '0';
		$values{maxresource} = '0';
	}
	else {
		$values{maxresource_display} = $raw_maxresource;
		$values{maxresource} = expand_scaled_number($raw_maxresource);
	}

	return %values;
}

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;
	my $allow_current_type = shift ? 1 : 0;
	return ($LANGPARAM eq 'ja' ? 'ユーザ種別に対するアクセス権限の設定が不正です' : 'The permissions are not valid for this user type.')
		unless TF::UserPermissions::valid($values, ($ENV{TF_GROUPCONSTRAINTS} || '') eq 'on');
	return $ERRORTABLE->{invalidExpiration} unless TF::UserExpiration::valid($values);
	return $ERRORTABLE->{required} if !$values->{userid} || !$values->{fullname};
	return $ERRORTABLE->{passwordMismatch} if $values->{new_password} ne $values->{password_confirm};
	return $ERRORTABLE->{invalidUserType} if !$allow_current_type && !is_allowed_usertype($values->{usertype});
	return $ERRORTABLE->{invalidUserId} if !is_valid_userid($values->{userid});
	return $ERRORTABLE->{invalidMail} if !is_valid_mail_list($values->{mailaddr});
	return $ERRORTABLE->{allowhostsTooLong} if length($values->{allowhosts} || '') > $ALLOWHOSTS_MAX_LENGTH;
	return $ERRORTABLE->{invalidAllowhosts} if !is_valid_allowhosts($values->{allowhosts});
	return $ERRORTABLE->{invalidQuota} if !defined $values->{maxstorage} || !defined $values->{maxresource};
	return $ERRORTABLE->{required} if !$values->{password} && !$values->{new_password};
	return '';
}

sub request_update_user {
	my ($target, $values, $sid) = @_;

	my $ua  = LWP::UserAgent->new(ssl_opts => { verify_hostname => 0 });
	my $req = HTTP::Request->new;

	$req->uri($USER_MGMT_URL . uri_escape_utf8($target));
	$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' => '001101000');
	$req->content(encode('UTF-8', build_user_update_xml($values)));
	$ua->agent(TF::CommonUtils::getCgiUserAgent());

	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 "useredit: PROPPATCH propstat failed ($code)\n";
		return (0, $ERRORTABLE->{updateFailed} . " ($code)");
	}

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

sub request_delete_user {
	my ($target, $sid) = @_;

	my $ua  = LWP::UserAgent->new(ssl_opts => { verify_hostname => 0 });
	my $req = HTTP::Request->new;

	$req->uri($USER_MGMT_URL . uri_escape_utf8($target));
	$req->method('DELETE');
	$req->protocol('HTTP/1.1');
	$req->authorization_basic($REMOTE_USER, $PASSWD);
	$req->header('x-tf-session' => $sid) if $sid;
	$ua->agent(TF::CommonUtils::getCgiUserAgent());

	my $res = $ua->request($req);
	if ($res->is_success) {
		return (1, '');
	}

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

sub build_user_update_xml {
	my $values = shift;
	my $state_tag = ($values->{active} eq 'on') ? '<TF:active/>' : '<TF:inactive/>';
	my $usertype_tag = '<TF:normal/>';
	if ($values->{usertype} eq 'groupleader') {
		$usertype_tag = '<TF:groupleader/>';
	}
	elsif ($values->{usertype} eq 'admin') {
		$usertype_tag = '<TF:admin/>';
	}
	elsif ($values->{usertype} eq 'limited') {
		$usertype_tag = '<TF:limited/>';
	}

	my $mail_tag = '<TF:mailaddr/>';
	if ($values->{mailaddr} ne '') {
		$mail_tag = '<TF:mailaddr>' . escape_xml($values->{mailaddr}) . '</TF:mailaddr>';
	}
	my $allowhosts_tag = '<TF:allowhosts/>';
	if (($values->{allowhosts} || '') ne '') {
		$allowhosts_tag = '<TF:allowhosts>' . escape_xml($values->{allowhosts}) . '</TF:allowhosts>';
	}
	my $accessdeny_tag = '<TF:accessdeny>' . escape_xml($values->{accessdeny} || '') . '</TF:accessdeny>';
	my $comment_tag = '<TF:comment>' . escape_xml($values->{comment} || '') . '</TF:comment>';
	my $privilege_tag = build_privilege_xml($values);
	my $expiration_tag = TF::UserExpiration::build_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:userdiscovery><TF:userinfo>'
		. '<TF:name>' . escape_xml($values->{fullname}) . '</TF:name>'
		. $mail_tag
		. '<TF:password>' . escape_xml($values->{password}) . '</TF:password>'
		. '<TF:maxstorage>' . escape_xml($values->{maxstorage}) . '</TF:maxstorage>'
		. '<TF:maxresource>' . escape_xml($values->{maxresource}) . '</TF:maxresource>'
		. '<TF:maxusercreation>' . escape_xml($values->{maxusercreation} || '0') . '</TF:maxusercreation>'
		. $allowhosts_tag
		. $accessdeny_tag
		. $comment_tag
		. '<TF:state>' . $state_tag . '</TF:state>'
		. $expiration_tag
		. $privilege_tag
		. '<TF:usertype>' . $usertype_tag . '</TF:usertype>'
		. '</TF:userinfo></TF:userdiscovery></D:prop></D:set></D:propertyupdate>';
}

sub build_privilege_xml {
	return TF::UserPermissions::build_xml(shift || {});
}

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);
		if (defined $key && $key eq 'tf-session') {
			return $val || '';
		}
	}
	return '';
}

sub normalize_mail_list {
	my $raw = shift || '';
	my %seen;
	my @tokens;
	foreach my $token (split(/,/, $raw)) {
		$token = trim($token);
		next if $token eq '' || $seen{$token};
		$seen{$token} = 1;
		push @tokens, $token;
	}
	return join(', ', @tokens);
}

sub normalize_allowhosts {
	my $raw = shift || '';
	my %seen;
	my @tokens;
	foreach my $token (split(/,/, $raw)) {
		$token = trim($token);
		$token =~ s/\s+//g;
		next if $token eq '' || $seen{$token};
		$seen{$token} = 1;
		push @tokens, $token;
	}
	return join(', ', @tokens);
}

sub is_valid_mail_list {
	my $raw = shift || '';
	return 1 if $raw eq '';
	foreach my $token (split(/,\s*/, $raw)) {
		next if $token =~ /^[^@\s,]+@[^@\s,]+$/;
		return 0;
	}
	return 1;
}

sub is_valid_allowhosts {
	my $raw = shift || '';
	return 1 if $raw eq '';
	foreach my $token (split(/,\s*/, $raw)) {
		return 0 if !is_valid_ip_cidr($token);
	}
	return 1;
}

sub is_valid_ip_cidr {
	my $token = shift || '';
	my ($addr, $prefix) = split(/\//, $token, 2);
	return 0 if $addr eq '' || $token =~ m{//};
	if ($addr =~ /:/) {
		return 0 if $addr !~ /^[0-9A-Fa-f:.]+$/;
		return 0 if defined $prefix && ($prefix !~ /^\d+$/ || $prefix < 0 || $prefix > 128);
		return 1;
	}
	return 0 if $addr !~ /^(\d{1,3}\.){3}\d{1,3}$/;
	foreach my $octet (split(/\./, $addr)) {
		return 0 if $octet > 255;
	}
	return 0 if defined $prefix && ($prefix !~ /^\d+$/ || $prefix < 0 || $prefix > 32);
	return 1;
}

sub is_valid_userid {
	my $userid = shift || '';
	return 0 if $userid eq '';
	return 0 if $userid =~ m{/};
	return 0 if $userid =~ /^\./;
	return 1;
}

sub is_allowed_usertype {
	my $type = shift || '';
	return 1 if $type eq 'normal' || $type eq 'limited';
	return 1 if $USERTYPE eq 'admin' && ($type eq 'groupleader' || $type eq 'admin');
	return 0;
}

sub expand_scaled_number {
	my $raw = shift;
	return undef if !defined $raw;
	$raw = trim($raw);
	return undef if $raw eq '';
	return undef if $raw !~ /^([0-9]+)([kKmMgGtT]?)$/;
	my ($num, $suffix) = ($1, uc($2 || ''));
	my %multi = (
		'' => 1,
		K => 1024,
		M => 1024 * 1024,
		G => 1024 * 1024 * 1024,
		T => 1024 * 1024 * 1024 * 1024,
	);
	return int($num) * $multi{$suffix};
}

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 trim {
	my $str = shift;
	return '' if !defined $str;
	$str =~ s/^[\s\x{3000}]+//;
	$str =~ s/[\s\x{3000}]+$//;
	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 radio_row {
	my ($value, $label, $checked) = @_;
	return {
		value => $value,
		label => $label,
		checked => $checked ? 1 : 0,
	};
}

sub usertype_label {
	my $type = shift || '';
	return localize('typeAdmin') if $type eq 'admin';
	return localize('typeGroupLeader') if $type eq 'groupleader';
	return localize('typeLimited') if $type eq 'limited';
	return localize('typeNormal');
}

sub localize {
	my $key = shift || '';
	my %ja = (
		title => 'ユーザ更新',
		subtitle => '既存ユーザの基本情報と状態を更新します。',
		backLabel => '一覧へ戻る',
		saveLabel => '更新',
		deleteLabel => '削除',
		deleteDialogTitle => 'ユーザ削除',
		deleteConfirm => 'このユーザを削除します。よろしいですか？',
		deleteCancelLabel => 'キャンセル',
		deleteConfirmLabel => '削除',
			clientPasswordMismatch => 'パスワード確認が一致しません',
			allowhostsTooLong => '許可IPアドレスは255文字以内で入力してください',
		sectionState => '状態と種別',
		sectionBasic => '基本情報',
		sectionQuota => '利用上限',
		userIdLabel => 'ユーザID',
		userIdDesc => 'ユーザIDは変更できません',
		nameLabel => '表示名',
		nameDesc => '画面表示に使用する名称',
		mailLabel => 'メールアドレス',
		mailDesc => '通知先として利用するメールアドレス。カンマで区切ると複数登録できます。',
		mailPlaceholder => 'mail@example.com, second@example.com',
		allowhostsLabel => '許可IPアドレス',
		allowhostsDesc => 'ログインを許可するIPアドレス。CIDR形式と単体IPをカンマ区切りで複数登録できます。',
		allowhostsPlaceholder => '192.0.2.10, 198.51.100.0/24',
		removeAddressLabel => '削除',
		passwordLabel => '新しいパスワード',
		passwordDesc => '変更する場合のみ入力します',
		passwordToggleLabel => '表示',
		passwordHideLabel => '非表示',
		passwordConfirmLabel => '新しいパスワード確認',
		passwordConfirmDesc => '変更する場合のみ再入力します',
		commentLabel => 'コメント',
		commentDesc => 'このユーザに関する管理用のメモ',
		maxStorageLabel => '容量上限',
		maxStorageDesc => '利用可能な最大容量。0 または KMGT 付きで入力できます。',
		unlimitedStorageLabel => '無制限',
		maxResourceLabel => 'ファイル数上限',
		maxResourceDesc => '登録可能な最大ファイル数。0 または KMGT 付きで入力できます。',
		unlimitedResourceLabel => '無制限',
		activeLabel => '有効状態',
		activeDesc => 'このユーザがログインできるか',
		expirationLabel => '有効期限',
		expirationDesc => 'この日時を過ぎるとログインできなくなります',
		expirationUnlimitedLabel => '無期限',
		toggleOnLabel => 'ON',
		toggleOffLabel => 'OFF',
		typeLabel => 'ユーザ種別',
		typeDesc => '付与する基本種別',
		typeNormal => '一般ユーザ',
		typeLimited => '制限ユーザ',
		typeGroupLeader => 'グループリーダー',
		typeAdmin => '管理者',
	);
	my %en = (
		title => 'Edit User',
		subtitle => 'Update an existing user.',
		backLabel => 'Back to list',
		saveLabel => 'Update',
		deleteLabel => 'Delete',
		deleteDialogTitle => 'Delete user',
		deleteConfirm => 'Delete this user. Are you sure?',
		deleteCancelLabel => 'Cancel',
		deleteConfirmLabel => 'Delete',
			clientPasswordMismatch => 'Password confirmation does not match.',
			allowhostsTooLong => 'Allowed IP addresses must be 255 characters or fewer.',
		sectionState => 'State and type',
		sectionBasic => 'Basic information',
		sectionQuota => 'Quota',
		userIdLabel => 'User ID',
		userIdDesc => 'User ID cannot be changed.',
		nameLabel => 'Display name',
		nameDesc => 'Name shown in the UI',
		mailLabel => 'Mail address',
		mailDesc => 'Address used for notification mail. Separate multiple addresses with commas.',
		mailPlaceholder => 'mail@example.com, second@example.com',
		allowhostsLabel => 'Allowed IP addresses',
		allowhostsDesc => 'IP addresses allowed to sign in. Enter single IPs or CIDR blocks separated by commas.',
		allowhostsPlaceholder => '192.0.2.10, 198.51.100.0/24',
		removeAddressLabel => 'Remove',
		passwordLabel => 'New password',
		passwordDesc => 'Enter only when changing it.',
		passwordToggleLabel => 'Show',
		passwordHideLabel => 'Hide',
		passwordConfirmLabel => 'Confirm new password',
		passwordConfirmDesc => 'Re-enter only when changing it.',
		commentLabel => 'Comment',
		commentDesc => 'Administrative notes about this user',
		maxStorageLabel => 'Max storage',
		maxStorageDesc => 'Maximum available storage. Enter 0 or use K/M/G/T suffixes.',
		unlimitedStorageLabel => 'Unlimited',
		maxResourceLabel => 'Max files',
		maxResourceDesc => 'Maximum number of files. Enter 0 or use K/M/G/T suffixes.',
		unlimitedResourceLabel => 'Unlimited',
		activeLabel => 'Active state',
		activeDesc => 'Whether the user can sign in.',
		expirationLabel => 'Expiration',
		expirationDesc => 'The user cannot sign in after this date and time.',
		expirationUnlimitedLabel => 'No expiration',
		toggleOnLabel => 'ON',
		toggleOffLabel => 'OFF',
		typeLabel => 'User type',
		typeDesc => 'Base type assigned to the user',
		typeNormal => 'Normal user',
		typeLimited => 'Limited user',
		typeGroupLeader => 'Group leader',
		typeAdmin => 'Administrator',
	);
	return ($LANGPARAM eq 'ja') ? $ja{$key} : $en{$key};
}
