#!/usr/bin/perl -w
#------------------------------------------------------------------------------
# Group member management CGI
#------------------------------------------------------------------------------

use strict;
use utf8;
use CGI;
use HTML::Template;
use LWP::UserAgent;
use HTTP::Request;
use XML::DOM;
use TF::CommonUtils;
use TF::GroupList;
use TF::UserList;
use TF::XMLUtils;

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/groupmembers.tmpl.' . $LANGPARAM;
my ($SCHEMA, $SERVER_PORT, $URI_PREFIX) = TF::CommonUtils::getCgiConnectInfo();
my $BASE_URL      = $URI_PREFIX . $LOCATION;
my $CLIENT_INFO   = 'mb111111';

my $ERRORTABLE_ja = {
	CGIError => 'CGIエラーです',
	invalidToken => '画面を再読み込みしてからやり直してください',
	forbidden => 'この操作は許可されていません',
	groupNotFound => '対象グループが見つかりません',
	memberNotFound => '対象ユーザはこのグループに所属していません',
	deleteFailed => '所属解除に失敗しました',
};
my $ERRORTABLE_en = {
	CGIError => 'CGI error found.',
	invalidToken => 'Reload the page and try again.',
	forbidden => 'This operation is not allowed.',
	groupNotFound => 'The target group was not found.',
	memberNotFound => 'The target user is not a member of this group.',
	deleteFailed => 'Failed to remove the member.',
};
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;
	}

	if (($cgi->request_method || 'GET') eq 'GET' && (($cgi->param('ajax') || '') eq 'children')) {
		show_group_children_json($cgi);
		return;
	}
	if (($cgi->request_method || 'GET') eq 'GET' && (($cgi->param('ajax') || '') eq 'members')) {
		show_group_members_json($cgi);
		return;
	}
	if (($cgi->request_method || 'GET') eq 'POST' && (($cgi->param('hidCmd') || '') eq 'updateMailwatch')) {
		update_mailwatch_json($cgi);
		return;
	}
	if (($cgi->request_method || 'GET') eq 'POST' && (($cgi->param('hidCmd') || '') eq 'addMember')) {
		add_member_json($cgi);
		return;
	}

	my ($groups_ref, $group_error) = load_groups();
	if (!$groups_ref) {
		TF::CommonUtils::showErrorPage($group_error, $LANGPARAM);
		return;
	}

	my @groups = @{$groups_ref};
	my $selected_groupid = trim(TF::CommonUtils::decodeUTF8($cgi->param('groupid') || ''));

	my $status = '';
	my $selected_group = undef;
	if ($selected_groupid && !group_exists(\@groups, $selected_groupid)) {
		$selected_group = load_group_by_id($selected_groupid);
	}

	my ($members_ref, $member_error) = $selected_groupid ? load_group_members($selected_groupid) : ([], '');
	my @members = @{$members_ref || []};
	$status ||= $member_error || '';
	my ($user_candidates_ref, $user_error) = load_user_candidates();
	$status ||= $user_error || '';

	if (($cgi->request_method || 'GET') eq 'POST' && (($cgi->param('hidCmd') || '') eq 'remove')) {
		my $err = validate_csrf($cgi);
		if ($err) {
			$status = $err;
		}
		else {
			my $userid = trim(TF::CommonUtils::decodeUTF8($cgi->param('userid') || ''));
			my $href = trim(TF::CommonUtils::decodeUTF8($cgi->param('href') || ''));
			my $member = find_member(\@members, $userid, $href);
			if (!$member) {
				$status = $ERRORTABLE->{memberNotFound};
			}
			else {
				my ($ok, $errmsg) = request_delete_member($member->{href}, get_session_id());
				$status = $errmsg if !$ok;
				if ($ok) {
					$status = localize('removeSuccess');
					($members_ref, $member_error) = load_group_members($selected_groupid);
					@members = @{$members_ref || []};
					$status = $member_error if $member_error;
				}
			}
		}
	}

	show_response($cgi, \@groups, \@members, $selected_groupid, $status,
		$selected_group, $user_candidates_ref || []);
}

sub load_groups {
	return load_group_children('', 0);
}

sub load_group_children {
	my ($parent_groupid, $depth) = @_;
	my @groups;
	my $glist = TF::GroupList->new();
	my $code = $glist->queryGroupTreeList($BASE_URL, $REMOTE_USER, $PASSWD, $parent_groupid || '');
	if ($code < 200 || $code >= 300) {
		print STDERR "groupmembers: group tree SEARCH failed ($code) parent=$parent_groupid\n";
		return (undef, $ERRORTABLE->{forbidden});
	}

	for my $id ($glist->getIDList()) {
		next if $parent_groupid && $id eq $parent_groupid;
		my $childcount = $glist->getChildCount($id) || 0;
		my $is_box = $glist->can('isBox') && $glist->isBox($id) ? 1 : 0;
		push @groups, {
			id => $id,
			name => $glist->getName($id) || $id,
			grpcoluri => $glist->getGroupFolderUri($id) || '',
			managementuri => $glist->getManagementUri($id) || '',
			comment => $glist->getComment($id) || '',
			childcount => $childcount,
			active => $glist->isActive($id) ? 1 : 0,
			isBox => $is_box,
			depth => $depth,
		};
	}
	return (\@groups, '');
}

sub load_group_by_id {
	my $groupid = shift || '';
	return undef if !$groupid;

	my $glist = TF::GroupList->new();
	my $code = $glist->queryGroupTreeList($BASE_URL, $REMOTE_USER, $PASSWD, $groupid);
	if ($code < 200 || $code >= 300) {
		print STDERR "groupmembers: selected group SEARCH failed ($code) groupid=$groupid\n";
		return undef;
	}

	for my $id ($glist->getIDList()) {
		next if $id ne $groupid;
		my $is_box = $glist->can('isBox') && $glist->isBox($id) ? 1 : 0;
		return {
			id => $id,
			name => $glist->getName($id) || $id,
			grpcoluri => $glist->getGroupFolderUri($id) || '',
			managementuri => $glist->getManagementUri($id) || '',
			comment => $glist->getComment($id) || '',
			childcount => $glist->getChildCount($id) || 0,
			active => $glist->isActive($id) ? 1 : 0,
			isBox => $is_box,
			depth => 0,
		};
	}
	return undef;
}

sub show_group_children_json {
	my $cgi = shift;
	my $parent_groupid = trim(TF::CommonUtils::decodeUTF8($cgi->param('parent') || ''));
	my $depth = int($cgi->param('depth') || 0);
	$depth = 0 if $depth < 0;

	my ($groups_ref, $errmsg) = load_group_children($parent_groupid, $depth + 1);
	if (!$groups_ref) {
		print $cgi->header(-type => 'application/json', -charset => 'UTF-8', -status => '500 Internal Server Error');
		print '{"ok":false,"message":"' . json_escape($errmsg || $ERRORTABLE->{forbidden}) . '"}';
		return;
	}

	my @items;
	for my $group (@{$groups_ref || []}) {
		push @items, '{'
			. '"id":"' . json_escape($group->{id}) . '",'
			. '"name":"' . json_escape($group->{name}) . '",'
			. '"description":"' . json_escape($group->{comment} || localize('noDescription')) . '",'
			. '"depth":' . int($group->{depth} || 0) . ','
			. '"indent":' . (int($group->{depth} || 0) * 18) . ','
			. '"childcount":' . int($group->{childcount} || 0) . ','
			. '"hasChildren":' . (($group->{childcount} || 0) > 0 ? 'true' : 'false') . ','
			. '"inactive":' . (($group->{active} || 0) ? 'false' : 'true') . ','
			. '"icon":"' . json_escape(group_icon($group)) . '",'
			. '"createLink":"' . json_escape($LOCATION . '/.cgi-bin/groupcreate?parent=' . url_escape($group->{id})) . '",'
			. '"editLink":"' . json_escape($LOCATION . '/.cgi-bin/groupedit?groupid=' . url_escape($group->{id})) . '",'
			. '"statusLabel":"' . json_escape(($group->{active} || 0) ? localize('active') : localize('inactive')) . '",'
			. '"childCountLabel":"' . json_escape(localize('childCountLabel')) . '",'
			. '"link":"?groupid=' . json_escape(url_escape($group->{id})) . '"'
			. '}';
	}

	print $cgi->header(-type => 'application/json', -charset => 'UTF-8');
	print '{"ok":true,"groups":[' . join(',', @items) . ']}';
}

sub show_group_members_json {
	my $cgi = shift;
	my $groupid = trim(TF::CommonUtils::decodeUTF8($cgi->param('groupid') || ''));
	my $groupname = trim(TF::CommonUtils::decodeUTF8($cgi->param('groupname') || ''));
	if (!$groupid) {
		print $cgi->header(-type => 'application/json', -charset => 'UTF-8', -status => '400 Bad Request');
		print '{"ok":false,"message":"' . json_escape($ERRORTABLE->{groupNotFound}) . '"}';
		return;
	}

	my ($members_ref, $member_error) = load_group_members($groupid);
	my $group = load_group_by_id($groupid);
	if ($member_error) {
		print $cgi->header(-type => 'application/json', -charset => 'UTF-8', -status => '500 Internal Server Error');
		print '{"ok":false,"message":"' . json_escape($member_error) . '"}';
		return;
	}

	print $cgi->header(-type => 'application/json', -charset => 'UTF-8');
	print group_members_json($groupid, $groupname || ($group ? $group->{name} : $groupid),
		$group ? ($group->{managementuri} || '') : '', $members_ref || []);
}

sub add_member_json {
	my $cgi = shift;
	my $err = validate_csrf($cgi);
	if ($err) {
		print $cgi->header(-type => 'application/json', -charset => 'UTF-8', -status => '403 Forbidden');
		print '{"ok":false,"message":"' . json_escape($err) . '"}';
		return;
	}

	my $groupid = trim(TF::CommonUtils::decodeUTF8($cgi->param('groupid') || ''));
	my $userid = trim(TF::CommonUtils::decodeUTF8($cgi->param('userid') || ''));
	if (!$groupid || !$userid) {
		print $cgi->header(-type => 'application/json', -charset => 'UTF-8', -status => '400 Bad Request');
		print '{"ok":false,"message":"' . json_escape($ERRORTABLE->{groupNotFound}) . '"}';
		return;
	}

	my $group = load_group_by_id($groupid);
	if (!$group || !$group->{managementuri}) {
		print $cgi->header(-type => 'application/json', -charset => 'UTF-8', -status => '404 Not Found');
		print '{"ok":false,"message":"' . json_escape($ERRORTABLE->{groupNotFound}) . '"}';
		return;
	}

	my ($ok, $errmsg) = request_add_member($group->{managementuri}, $userid, get_session_id());
	if (!$ok) {
		print $cgi->header(-type => 'application/json', -charset => 'UTF-8', -status => '500 Internal Server Error');
		print '{"ok":false,"message":"' . json_escape($errmsg || localize('addUserFailed')) . '"}';
		return;
	}

	my ($members_ref, $member_error) = load_group_members($groupid);
	if ($member_error) {
		print $cgi->header(-type => 'application/json', -charset => 'UTF-8', -status => '500 Internal Server Error');
		print '{"ok":false,"message":"' . json_escape($member_error) . '"}';
		return;
	}

	print $cgi->header(-type => 'application/json', -charset => 'UTF-8');
	print group_members_json($groupid, $group->{name} || $groupid, $group->{managementuri} || '', $members_ref || []);
}

sub load_group_members {
	my $groupid = shift || '';
	my $xml = query_group_content($groupid);
	if (!defined $xml) {
		return ([], $ERRORTABLE->{groupNotFound});
	}
	return ([], '') if $xml eq '';
	return (parse_group_members($xml), '');
}

sub load_user_candidates {
	my $ulist = TF::UserList->new();
	my $code = $ulist->queryUserList($BASE_URL, $REMOTE_USER, $PASSWD);
	if ($code < 200 || $code >= 300) {
		print STDERR "groupmembers: user detaillist SEARCH failed ($code)\n";
		return ([], $ERRORTABLE->{forbidden});
	}

	my @users;
	for my $id ($ulist->getIDList()) {
		next if !$ulist->hasUserItem($id);
		push @users, {
			userid => $id,
			name => $ulist->getName($id) || '',
			mailaddr => $ulist->getMailAddr($id) || '',
			inactive => $ulist->isActive($id) ? 0 : 1,
			statusLabel => $ulist->isActive($id) ? localize('active') : localize('inactive'),
		};
	}

	return (\@users, '');
}

sub query_group_content {
	my $groupid = shift || '';
	my $ua  = LWP::UserAgent->new(ssl_opts => { verify_hostname => 0 });
	my $req = HTTP::Request->new;
	$req->uri($BASE_URL . '/.management/GROUP/');
	$req->method('SEARCH');
	$req->protocol('HTTP/1.1');
	$req->authorization_basic($REMOTE_USER, $PASSWD);
	$req->header('TF-Client-Info' => $CLIENT_INFO);
	$req->content('<?xml version="1.0" encoding="UTF-8" standalone="no" ?>'
		. '<D:searchrequest xmlns:D="DAV:" xmlns:TF="http://www.teamfile.com/DTD/TF/">'
		. '<TF:groupinformationsearch><TF:groupid>' . escape_xml($groupid) . '</TF:groupid><TF:availableuser/></TF:groupinformationsearch>'
		. '</D:searchrequest>');
	$ua->agent(TF::CommonUtils::getCgiUserAgent());

	my $res = $ua->request($req);
	if (!$res->is_success) {
		return '' if ($res->code || 500) == 404;
		print STDERR "groupmembers: group availableuser SEARCH failed (" . ($res->code || 500) . ") " . ($res->status_line || '') . "\n";
		return undef;
	}
	return $res->content();
}

sub parse_group_members {
	my $data = shift || '';
	my @members;
	my $parser = XML::DOM::Parser->new;
	my $doc = eval { $parser->parse($data) };
	if (!$doc) {
		print STDERR "groupmembers: failed to parse group content XML\n";
		return \@members;
	}

	for my $res ($doc->getElementsByTagName('D:response')) {
		my $userinfo = first_child($res, 'TF:userinfo');
		next if !$userinfo;
		my $href = TF::XMLUtils::getText($res, 'D:href') || '';
		my $userid = TF::XMLUtils::getText($userinfo, 'TF:userid') || '';
		next if $userid eq '';
		push @members, {
			userid => $userid,
			name => TF::XMLUtils::getText($userinfo, 'TF:name') || '',
			mailaddr => TF::XMLUtils::getText($userinfo, 'TF:mailaddr') || '',
				href => $href,
				isAdmin => first_child($userinfo, 'TF:admin') ? 1 : 0,
				isGroupleader => first_child($userinfo, 'TF:groupleader') ? 1 : 0,
				isInactive => first_child($userinfo, 'TF:inactive') ? 1 : 0,
				privilege => member_privilege_value($userinfo),
				mailwatch => TF::XMLUtils::getText($userinfo, 'TF:triggermethod') || '',
			};
	}
	$doc->dispose;
	return \@members;
}

sub first_child {
	my ($elem, $name) = @_;
	my $nodes = $elem->getElementsByTagName($name);
	return $nodes ? $nodes->item(0) : undef;
}

sub request_delete_member {
	my ($href, $sid) = @_;
	my $ua  = LWP::UserAgent->new(ssl_opts => { verify_hostname => 0 });
	my $req = HTTP::Request->new;
	$req->uri(absolute_url($href));
	$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' => $CLIENT_INFO);
	$ua->agent(TF::CommonUtils::getCgiUserAgent());

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

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

sub request_add_member {
	my ($group_management_uri, $userid, $sid) = @_;
	my $src = $BASE_URL . '/.management/USER/' . url_escape($userid);
	my $dst_uri = $group_management_uri || '';
	$dst_uri .= '/' if $dst_uri !~ m{/$};
	$dst_uri .= DIVY_PREFIX_RUSER_CGI() . url_escape($userid);

	my $ua  = LWP::UserAgent->new(ssl_opts => { verify_hostname => 0 });
	my $req = HTTP::Request->new;
	$req->uri($src);
	$req->method('COPY');
	$req->protocol('HTTP/1.1');
	$req->authorization_basic($REMOTE_USER, $PASSWD);
	$req->header('Destination' => absolute_url($dst_uri));
	$req->header('Overwrite' => 'F');
	$req->header('Depth' => '0');
	$req->header('x-tf-session' => $sid) if $sid;
	$req->header('TF-Client-Info' => $CLIENT_INFO);
	$ua->agent(TF::CommonUtils::getCgiUserAgent());

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

	my $code = $res->code || 500;
	print STDERR "groupmembers: COPY member failed ($code) " . ($res->status_line || '') . " userid=$userid group=$group_management_uri\n";
	return (0, $ERRORTABLE->{forbidden}) if $code == 403;
	return (0, $ERRORTABLE->{groupNotFound}) if $code == 404;
	return (0, localize('addUserFailed') . " ($code)");
}

sub DIVY_PREFIX_RUSER_CGI {
	return '.RU_';
}

sub group_members_json {
	my ($groupid, $groupname, $management_uri, $members_ref) = @_;
	my @items;
	for my $member (@{$members_ref || []}) {
		push @items, '{'
			. '"userid":"' . json_escape($member->{userid}) . '",'
			. '"name":"' . json_escape($member->{name}) . '",'
			. '"href":"' . json_escape($member->{href}) . '",'
			. '"type":"' . json_escape(member_type_label($member)) . '",'
			. '"privilege":"' . json_escape(member_privilege_label($member)) . '",'
			. '"statusLabel":"' . json_escape($member->{isInactive} ? localize('inactive') : localize('active')) . '",'
			. '"mailwatch":"' . json_escape($member->{mailwatch}) . '",'
			. '"inactive":' . ($member->{isInactive} ? 'true' : 'false')
			. '}';
	}
	return '{"ok":true,'
		. '"groupid":"' . json_escape($groupid) . '",'
		. '"groupName":"' . json_escape($groupname || $groupid) . '",'
		. '"managementUri":"' . json_escape($management_uri || '') . '",'
		. '"token":"' . json_escape($TF_TOKEN) . '",'
		. '"removeLabel":"' . json_escape(localize('removeLabel')) . '",'
		. '"noMembers":"' . json_escape(localize('noMembers')) . '",'
		. '"columns":{'
		. '"userid":"' . json_escape(localize('colUserId')) . '",'
		. '"name":"' . json_escape(localize('colName')) . '",'
		. '"type":"' . json_escape(localize('colType')) . '",'
		. '"privilege":"' . json_escape(localize('colPrivilege')) . '",'
		. '"mailwatch":"' . json_escape(localize('colMailwatch')) . '",'
		. '"action":"' . json_escape(localize('colAction')) . '"'
		. '},'
		. '"members":[' . join(',', @items) . ']}';
}

sub update_mailwatch_json {
	my $cgi = shift;
	my $err = validate_csrf($cgi);
	if ($err) {
		print $cgi->header(-type => 'application/json', -charset => 'UTF-8', -status => '403 Forbidden');
		print '{"ok":false,"message":"' . json_escape($err) . '"}';
		return;
	}

	my $href = trim(TF::CommonUtils::decodeUTF8($cgi->param('href') || ''));
	my $enabled = ($cgi->param('enabled') || '') eq '1' ? 1 : 0;
	if (!$href) {
		print $cgi->header(-type => 'application/json', -charset => 'UTF-8', -status => '400 Bad Request');
		print '{"ok":false,"message":"' . json_escape($ERRORTABLE->{memberNotFound}) . '"}';
		return;
	}

	my ($ok, $errmsg) = request_update_mailwatch($href, $enabled, get_session_id());
	if (!$ok) {
		print $cgi->header(-type => 'application/json', -charset => 'UTF-8', -status => '500 Internal Server Error');
		print '{"ok":false,"message":"' . json_escape($errmsg || localize('mailwatchUpdateFailed')) . '"}';
		return;
	}

	print $cgi->header(-type => 'application/json', -charset => 'UTF-8');
	print '{"ok":true,"message":"' . json_escape($enabled ? localize('mailwatchSetSuccess') : localize('mailwatchRemoveSuccess')) . '"}';
}

sub request_update_mailwatch {
	my ($href, $enabled, $sid) = @_;
	my $ua  = LWP::UserAgent->new(ssl_opts => { verify_hostname => 0 });
	my $req = HTTP::Request->new;
	$req->uri(absolute_url($href));
	$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' => $CLIENT_INFO);
	$req->header('Content-Type' => 'text/xml; charset=UTF-8');
	if ($enabled) {
		$req->content('<?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:mailwatch>'
			. '<TF:watchowner><TF:watchmethod>POST PUT DELETE MKCOL MOVE COPY PROPPATCH</TF:watchmethod></TF:watchowner>'
			. '<TF:triggermethod>POST PUT DELETE MKCOL MOVE COPY PROPPATCH</TF:triggermethod>'
			. '<TF:notification><TF:mail/></TF:notification>'
			. '</TF:mailwatch></D:prop></D:set>'
			. '</D:propertyupdate>');
	}
	else {
		$req->content('<?xml version="1.0" encoding="UTF-8" standalone="no" ?>'
			. '<D:propertyupdate xmlns:D="DAV:" xmlns:TF="http://www.teamfile.com/DTD/TF/">'
			. '<D:remove><D:prop><TF:mailwatch/></D:prop></D:remove>'
			. '</D:propertyupdate>');
	}
	$ua->agent(TF::CommonUtils::getCgiUserAgent());

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

	my $code = $res->code || 500;
	print STDERR "groupmembers: PROPPATCH mailwatch " . ($enabled ? "set" : "remove") . " failed ($code) " . ($res->status_line || '') . "\n";
	return (0, $ERRORTABLE->{forbidden}) if $code == 403;
	return (0, $ERRORTABLE->{memberNotFound}) if $code == 404;
	return (0, localize('mailwatchUpdateFailed') . " ($code)");
}

sub show_response {
	my ($cgi, $groups_ref, $members_ref, $selected_groupid, $status,
		$selected_group, $user_candidates_ref) = @_;
	my @group_rows;
	for my $group (@{$groups_ref || []}) {
		push @group_rows, {
			id => esc($group->{id}),
			name => esc($group->{name}),
			description => esc($group->{comment} || localize('noDescription')),
			depth => int($group->{depth} || 0),
			indent => int($group->{depth} || 0) * 18,
			hasChildren => ($group->{childcount} || 0) > 0 ? 1 : 0,
			childcount => int($group->{childcount} || 0),
			childCountLabel => esc(localize('childCountLabel')),
			icon => group_icon($group),
			createLink => $LOCATION . '/.cgi-bin/groupcreate?parent=' . url_escape($group->{id}),
			editLink => $LOCATION . '/.cgi-bin/groupedit?groupid=' . url_escape($group->{id}),
			statusLabel => ($group->{active} || 0) ? localize('active') : localize('inactive'),
			inactive => ($group->{active} || 0) ? 0 : 1,
			selected => ($group->{id} eq $selected_groupid) ? 1 : 0,
			link => '?groupid=' . url_escape($group->{id}),
		};
	}

	my @member_rows;
	for my $member (@{$members_ref || []}) {
		push @member_rows, {
			userid => esc($member->{userid}),
			name => esc($member->{name}),
			mailaddr => esc($member->{mailaddr}),
			href => esc($member->{href}),
			token => $TF_TOKEN,
			selectedGroupId => esc($selected_groupid || ''),
			removeConfirm => esc(localize('removeConfirm')),
				removeLabel => esc(localize('removeLabel')),
				type => esc(member_type_label($member)),
				privilege => esc(member_privilege_label($member)),
				statusLabel => $member->{isInactive} ? localize('inactive') : localize('active'),
				mailwatch => esc($member->{mailwatch}),
				hasMailwatch => $member->{mailwatch} ? 1 : 0,
				inactive => $member->{isInactive} ? 1 : 0,
				activeChecked => $member->{isInactive} ? 0 : 1,
			};
	}

	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 => $LOCATION . '/.cgi-bin/user',
			backLabel => localize('backLabel'),
			createGroupUri => $LOCATION . '/.cgi-bin/groupcreate',
			createGroupLabel => localize('createGroupLabel'),
			canCreateTopGroup => $USERTYPE eq 'admin' ? 1 : 0,
			groupListTitle => localize('groupListTitle'),
			memberListTitle => localize('memberListTitle'),
			removeLabel => localize('removeLabel'),
			removeDialogTitle => localize('removeDialogTitle'),
			removeConfirm => localize('removeConfirm'),
			removeCancelLabel => localize('removeCancelLabel'),
			removeConfirmLabel => localize('removeConfirmLabel'),
			mailwatchRemoveDialogTitle => localize('mailwatchRemoveDialogTitle'),
			mailwatchRemoveConfirm => localize('mailwatchRemoveConfirm'),
			mailwatchRemoveConfirmLabel => localize('mailwatchRemoveConfirmLabel'),
			mailwatchSetSuccess => localize('mailwatchSetSuccess'),
			mailwatchUpdateFailed => localize('mailwatchUpdateFailed'),
			addUserLabel => localize('addUserLabel'),
			addUserDialogTitle => localize('addUserDialogTitle'),
			addUserSearchPlaceholder => localize('addUserSearchPlaceholder'),
			addUserEmptyHint => localize('addUserEmptyHint'),
			addUserNoResults => localize('addUserNoResults'),
			addUserResultLimit => localize('addUserResultLimit'),
			addUserSelectLabel => localize('addUserSelectLabel'),
			addUserFailed => localize('addUserFailed'),
			userCandidatesJson => users_json($user_candidates_ref || []),
			currentMembersJson => member_ids_json($members_ref || []),
			noGroups => localize('noGroups'),
			noMembers => localize('noMembers'),
			noGroupSelected => localize('noGroupSelected'),
			colUserId => localize('colUserId'),
				colName => localize('colName'),
				colType => localize('colType'),
				colPrivilege => localize('colPrivilege'),
				colMailwatch => localize('colMailwatch'),
				colAction => localize('colAction'),
			status => $status,
			hasStatus => $status ? 1 : 0,
			token => $TF_TOKEN,
			groupRows => \@group_rows,
			memberRows => \@member_rows,
			hasGroups => scalar(@group_rows) > 0 ? 1 : 0,
			hasMembers => scalar(@member_rows) > 0 ? 1 : 0,
			hasSelectedGroup => $selected_groupid ? 1 : 0,
			selectedGroupId => esc($selected_groupid || ''),
			);
		print $cgi->header(-charset => 'UTF-8');
		print $htmltmpl->output;
		close($tmplfh);
		return;
	}
	TF::CommonUtils::showErrorPage($ERRORTABLE->{CGIError}, $LANGPARAM);
}

sub users_json {
	my $users_ref = shift || [];
	my @items;
	for my $user (@{$users_ref || []}) {
		push @items, '{'
			. '"userid":"' . json_escape($user->{userid}) . '",'
			. '"name":"' . json_escape($user->{name}) . '",'
			. '"mailaddr":"' . json_escape($user->{mailaddr}) . '",'
			. '"inactive":' . ($user->{inactive} ? 'true' : 'false') . ','
			. '"statusLabel":"' . json_escape($user->{statusLabel}) . '"'
			. '}';
	}
	my $json = '[' . join(',', @items) . ']';
	$json =~ s{</}{<\\/}g;
	return $json;
}

sub member_ids_json {
	my $members_ref = shift || [];
	my @items;
	for my $member (@{$members_ref || []}) {
		push @items, '{"userid":"' . json_escape($member->{userid}) . '"}';
	}
	my $json = '[' . join(',', @items) . ']';
	$json =~ s{</}{<\\/}g;
	return $json;
}

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

sub find_member {
	my ($members_ref, $userid, $href) = @_;
	for my $member (@{$members_ref || []}) {
		return $member if $member->{userid} eq $userid && $member->{href} eq $href;
	}
	return undef;
}

sub group_exists {
	my ($groups_ref, $groupid) = @_;
	for my $group (@{$groups_ref || []}) {
		return 1 if $group->{id} eq $groupid;
	}
	return 0;
}

sub selected_group_name {
	my ($groups_ref, $groupid, $selected_group) = @_;
	for my $group (@{$groups_ref || []}) {
		return $group->{name} if $group->{id} eq $groupid;
	}
	return $selected_group->{name} if $selected_group && $selected_group->{id} eq $groupid;
	return $groupid || '';
}

sub member_type_label {
	my $member = shift || {};
	return localize('typeAdmin') if $member->{isAdmin};
	return localize('typeGroupleader') if $member->{isGroupleader};
	return localize('typeNormal');
}

sub member_privilege_value {
	my $userinfo = shift;
	return 'readwrite' if first_child($userinfo, 'TF:readwrite');
	return 'upload' if first_child($userinfo, 'TF:upload');
	return 'read' if first_child($userinfo, 'TF:read');
	return '';
}

sub member_privilege_label {
	my $member = shift || {};
	return localize('privilegeUpload') if ($member->{privilege} || '') eq 'upload';
	return localize('privilegeRead') if ($member->{privilege} || '') eq 'read';
	return localize('privilegeReadwrite');
}

sub group_icon {
	my $group = shift || {};
	return '/icons/teamfile/Folder-Cloud.png' if $group->{isBox};
	return '/icons/teamfile/Folder-Share.png';
}

sub absolute_url {
	my $href = shift || '';
	return $href if $href =~ m{^https?://}i;
	return $URI_PREFIX . $href if $href =~ m{^/};
	return $BASE_URL . '/' . $href;
}

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 trim {
	my $s = shift || '';
	$s =~ s/^\s+//;
	$s =~ s/\s+$//;
	return $s;
}

sub url_escape {
	my $s = shift || '';
	$s =~ s/([^A-Za-z0-9_.~-])/sprintf("%%%02X", ord($1))/eg;
	return $s;
}

sub esc {
	my $s = shift || '';
	$s =~ s/&/&amp;/g;
	$s =~ s/</&lt;/g;
	$s =~ s/>/&gt;/g;
	$s =~ s/"/&quot;/g;
	$s =~ s/'/&#39;/g;
	return $s;
}

sub escape_xml {
	return esc(shift || '');
}

sub json_escape {
	my $s = shift || '';
	$s =~ s/\\/\\\\/g;
	$s =~ s/"/\\"/g;
	$s =~ s/\r/\\r/g;
	$s =~ s/\n/\\n/g;
	$s =~ s/\t/\\t/g;
	$s =~ s/([\x00-\x1f])/sprintf("\\u%04x", ord($1))/eg;
	return $s;
}

sub localize {
	my $key = shift || '';
	my %ja = (
		title => 'グループ管理',
		subtitle => '操作可能なグループの所属ユーザを確認し、不要な所属を解除します。',
		backLabel => 'ユーザ一覧へ戻る',
		createGroupLabel => '追加',
		groupListTitle => '操作グループ',
		memberListTitle => '所属ユーザ',
		removeLabel => '解除',
		removeDialogTitle => '所属解除',
		removeConfirm => 'このユーザをグループから解除します。よろしいですか？',
		removeCancelLabel => 'キャンセル',
		removeConfirmLabel => '解除',
		noGroups => '操作できるグループがありません。',
		noMembers => '所属ユーザはいません。',
		noGroupSelected => '左のグループを選択してください。',
		noDescription => '説明はありません',
		childCountLabel => '下位グループ',
			colUserId => 'ユーザID',
			colName => '表示名',
			colType => '種別',
			colPrivilege => '権限',
			colMailwatch => 'メール通知',
			colAction => '操作',
		active => '有効',
		inactive => '無効',
			typeAdmin => '管理者',
			typeGroupleader => 'グループ管理者',
			typeNormal => '一般ユーザ',
			privilegeReadwrite => '読み書き',
			privilegeUpload => 'アップロード専用',
			privilegeRead => '読み取り専用',
			removeSuccess => '所属を解除しました',
			mailwatchRemoveDialogTitle => 'メール通知解除',
			mailwatchRemoveConfirm => 'このユーザのメール通知を解除します。よろしいですか？',
			mailwatchRemoveConfirmLabel => '解除',
			mailwatchSetSuccess => 'メール通知を設定しました',
			mailwatchRemoveSuccess => 'メール通知を解除しました',
			mailwatchUpdateFailed => 'メール通知の変更に失敗しました',
			addUserLabel => 'ユーザ追加',
			addUserDialogTitle => 'ユーザ追加',
			addUserSearchPlaceholder => 'ユーザID、表示名、メールアドレスで検索',
			addUserEmptyHint => '検索文字を入力してください。',
			addUserNoResults => '該当するユーザはいません。',
			addUserResultLimit => '表示は50件までです。条件を絞り込んでください。',
			addUserSelectLabel => '追加',
			addUserFailed => 'ユーザ追加に失敗しました',
		);
	my %en = (
		title => 'Group Management',
		subtitle => 'Review members of manageable groups and remove unnecessary memberships.',
		backLabel => 'Back to users',
		createGroupLabel => 'Add',
		groupListTitle => 'Groups',
		memberListTitle => 'Members',
		removeLabel => 'Remove',
		removeDialogTitle => 'Remove membership',
		removeConfirm => 'Remove this user from the group?',
		removeCancelLabel => 'Cancel',
		removeConfirmLabel => 'Remove',
		noGroups => 'No manageable groups.',
		noMembers => 'No members.',
		noGroupSelected => 'Select a group on the left.',
		noDescription => 'No description',
		childCountLabel => 'Child groups',
			colUserId => 'User ID',
			colName => 'Name',
			colType => 'Type',
			colPrivilege => 'Privilege',
			colMailwatch => 'Mail notification',
			colAction => 'Action',
		active => 'Active',
		inactive => 'Inactive',
			typeAdmin => 'Admin',
			typeGroupleader => 'Group leader',
			typeNormal => 'Normal',
			privilegeReadwrite => 'Read/write',
			privilegeUpload => 'Upload only',
			privilegeRead => 'Read only',
			removeSuccess => 'Membership removed.',
			mailwatchRemoveDialogTitle => 'Remove mail notification',
			mailwatchRemoveConfirm => 'Remove mail notification for this user?',
			mailwatchRemoveConfirmLabel => 'Remove',
			mailwatchSetSuccess => 'Mail notification enabled.',
			mailwatchRemoveSuccess => 'Mail notification removed.',
			mailwatchUpdateFailed => 'Failed to update mail notification.',
			addUserLabel => 'Add user',
			addUserDialogTitle => 'Add user',
			addUserSearchPlaceholder => 'Search by user ID, name, or email address',
			addUserEmptyHint => 'Enter search text.',
			addUserNoResults => 'No matching users.',
			addUserResultLimit => 'Showing up to 50 users. Refine the search.',
			addUserSelectLabel => 'Add',
			addUserFailed => 'Failed to add user.',
		);
	my $table = ($LANGPARAM eq 'ja') ? \%ja : \%en;
	return exists $table->{$key} ? $table->{$key} : $key;
}
