Commit 6cf701f9 authored by Leigh Stoller's avatar Leigh Stoller

Cleanup in the web interface to prevent XSS attacks.

We had a couple of different problems actually.

* We allow users to insert html into many DB fields (say, a project or
  experiment description).

* We did not sanitize that output when displaying back.

* We did not sanitize initial page arguments that were reflected in the
  output (say, in a form).

Since no one has the time to analyze every line of code, I took a couple of
shortcuts. The first is that I changed the regex table to not allow any <>
chars to go from the user into the DB. Brutal, but in fact there are only a
couple of places where a user legitimately needs them. For example, a
startup command that includes redirection. I handle those as special
cases. As more come up, we can fix them.

I did a quick pass through all of the forms, and made sure that we run
htmlspecialchars on everything including initial form args. This was not
too bad cause of the way all of the forms are structured, with a
"formfields" array.

I also removed a bunch of obsolete code and added an update script to
actually remove them from the www directory.

Lastly, I purged some XMLRPC code I did a long time ago in the Begin
Experiment path. Less complexity, easier to grok and fix.

	modified:   sql/database-fill.sql
	modified:   sql/dbfill-update.sql
parent d1d3ff11
#
# Delete some obsolete www files.
#
use strict;
use libinstall;
use installvars;
my $DBFILL_UPDATE = "$TOP_SRCDIR/sql/dbfill-update.sql";
sub InstallUpdate($$)
{
my ($version, $phase) = @_;
if ($phase eq "pre") {
Phase "dbfill", "Updating regex table", sub {
ExecQuietFatal("cat $DBFILL_UPDATE | mysql tbdb");
};
my @deletedfiles = (
"approveuser_form.php3",
"approvewauser.php3",
"beginexp_html.php3",
"beginexp_xml.php3",
"deletesfskey.php3",
"kb-manage.php3",
"kb-search.php3",
"nsgen.php3",
"plab_ez.php3",
"plab_ez_footnote1.html",
"plab_ez_footnote2.html",
"plab_ez_footnote3.html",
"plab_ez_footnote4.html",
"plab_ez_footnote5.html",
"plab_ez_footnote6.html",
"plab_ez_footnote7.html",
"plab_ez_footnote8.html",
"plabmetrics.php3",
"plabstats.php3",
"robotmap.php3",
"showsfskeys.php3");
my @deleteddirs = (
"robotrack",
"webdb",
"hyperview");
foreach my $file (@deletedfiles) {
$file = "$TBROOT/www/$file";
next
if (! -e $file);
Phase "$file", "Deleting $file", sub {
DeleteFileFatal($file);
};
}
foreach my $dir (@deleteddirs) {
$dir = "$TBROOT/www/$dir";
next
if (! -e $dir);
Phase "$dir", "Deleting $dir", sub {
ExecQuietFatal("/bin/rm -rf $dir");
};
}
}
return 0;
}
1;
# Local Variables:
# mode:perl
# End:
This diff is collapsed.
REPLACE INTO table_regex VALUES ('eventlist','arguments','text','redirect','default:html_text',0,1024,NULL);
REPLACE INTO table_regex VALUES ('eventlist','atstring','text','redirect','default:html_text',0,1024,NULL);
REPLACE INTO table_regex VALUES ('virt_nodes','startupcmd','text','redirect','default:html_tinytext',0,0,NULL);
REPLACE INTO table_regex VALUES ('virt_programs','command','text','redirect','default:html_tinytext',0,0,NULL);
REPLACE INTO table_regex VALUES ('sitevariables','defaultvalue','text','redirect','default:html_text',0,0,NULL);
REPLACE INTO table_regex VALUES ('sitevariables','description','text','redirect','default:html_text',0,0,NULL);
REPLACE INTO table_regex VALUES ('default','fulltext','text','regex','^[\\040-\\073\\075\\077-\\176\\012\\015\\011]*$',0,20000,NULL);
REPLACE INTO table_regex VALUES ('default','html_fulltext','text','regex','^[\\040-\\176\\012\\015\\011]*$',0,20000,NULL);
REPLACE INTO table_regex VALUES ('default','tinytext','text','regex','^[\\040-\\073\\075\\077-\\176]*$',0,256,NULL);
REPLACE INTO table_regex VALUES ('default','html_tinytext','text','regex','^[\\040-\\176]*$',0,256,NULL);
REPLACE INTO table_regex VALUES ('default','text','text','regex','^[\\040-\\073\\075\\077-\\176]*$',0,65535,NULL);
REPLACE INTO table_regex VALUES ('default','html_text','text','regex','^[\\040-\\176]*$',0,65535,NULL);
REPLACE INTO table_regex VALUES ('default','default','text','regex','^[\\040-\\073\\075\\077-\\176]*$',0,256,'Default regex if one is not defined for a table/slot. Allow any standard ascii character, but no binary data');
REPLACE INTO table_regex VALUES ('default','tinyint','int','regex','^[\\d]+$',-128,127,'Default regex for tiny int fields. Allow any standard ascii integer, but no binary data');
REPLACE INTO table_regex VALUES ('default','boolean','int','regex','^(0|1)$',0,1,'Default regex for tiny int fields that are int booleans. Allow any 0 or 1');
REPLACE INTO table_regex VALUES ('default','tinyuint','int','regex','^[\\d]+$',0,255,'Default regex for tiny int fields. Allow any standard ascii integer, but no binary data');
REPLACE INTO table_regex VALUES ('default','int','int','regex','^[\\d]+$',-2147483648,2147483647,'Default regex for int fields. Allow any standard ascii integer, but no binary data');
REPLACE INTO table_regex VALUES ('default','float','float','regex','^[+-]?\\ *(\\d+(\\.\\d*)?|\\.\\d+)([eE][+-]?\\d+)?$',-2147483648,2147483647,'Default regex for float fields. Allow any digits and the decimal point');
REPLACE INTO table_regex VALUES ('users','usr_affil_abbrev','text','regex','default:tinytext',0,16,NULL);
REPLACE INTO table_regex VALUES ('nseconfigs','nseconfig','text','redirect','default:fulltext',0,16777215,NULL);
REPLACE INTO table_regex VALUES ('projects','why','text','redirect','default:fulltext',0,4096,NULL);
REPLACE INTO table_regex VALUES ('experiments','description','text','redirect','default:fulltext',1,256,NULL);
REPLACE INTO table_regex VALUES ('images','description','text','redirect','default:fulltext',1,256,NULL);
REPLACE INTO table_regex VALUES ('experiment_templates','description','text','redirect','default:fulltext',1,4096,NULL);
REPLACE INTO table_regex VALUES ('experiment_template_metadata','name','text','redirect','default:tinytext',1,64,NULL);
REPLACE INTO table_regex VALUES ('experiment_template_metadata','value','text','redirect','default:fulltext',0,4096,NULL);
REPLACE INTO table_regex VALUES ('experiment_runs','description','text','redirect','default:tinytext',1,256,NULL);
REPLACE INTO table_regex VALUES ('experiment_template_instances','description','text','redirect','default:tinytext',1,256,NULL);
......@@ -22,7 +22,7 @@ HTMLINSTALL = $(INSTALL_SBINDIR)/htmlinstall
include $(OBJDIR)/Makeconf
SUBDIRS = garcia-telemetry tutorial
SUBDIRS = tutorial
#
# Force dependencies to make sure configure regenerates if the .in file
......@@ -68,10 +68,6 @@ AUTOICONS += $(wildcard $(SRCDIR)/autostatus-icons/*.png)
FLOORMAPFILES = $(wildcard $(SRCDIR)/floormap/*.jpg)
FLOORMAPFILES += $(wildcard $(SRCDIR)/floormap/*.gif)
ROBOTRACKFILES = $(wildcard $(SRCDIR)/robotrack/*.php3)
ROBOTRACKFILES += $(wildcard $(SRCDIR)/robotrack/*.jpg)
ROBOTRACKFILES += $(wildcard $(SRCDIR)/robotrack/*.jar)
WIRELESSSTATSFILES = $(wildcard $(SRCDIR)/wireless-stats/*.php3)
WIRELESSSTATSFILES += $(wildcard $(SRCDIR)/wireless-stats/*.jar)
......@@ -116,9 +112,6 @@ ifeq ($(PGENISUPPORT),1)
PGENIFILES += $(wildcard $(SRCDIR)/protogeni/*.xml)
endif
WEBDBFILES = $(wildcard $(SRCDIR)/webdb/*.php3)
WEBDBFILES += $(wildcard $(SRCDIR)/webdb/*.php)
HYFILES = $(wildcard $(SRCDIR)/hyperviewer/*.php3)
HYFILES += $(wildcard $(SRCDIR)/hyperviewer/*.html)
HYFILES += $(wildcard $(SRCDIR)/hyperviewer/*.jpg)
......@@ -180,7 +173,6 @@ ALLPIXES = $(notdir $(PIXFILES))
ALLDOCS = $(notdir $(DOCFILES))
ALLTUTS = $(notdir $(TUTFILES))
ALLICONS = $(notdir $(AUTOICONS))
ALLWEBDB = $(notdir $(WEBDBFILES))
ALLPGENI = $(notdir $(PGENIFILES))
ALLDOWNLOADS = $(notdir $(DOWNLOADFILES))
ALLCVSWEB = $(notdir $(CVSWEBFILES))
......@@ -191,7 +183,6 @@ ALLHY = $(notdir $(HYFILES))
ALLTT = $(notdir $(TTFILES))
ALLUM = $(notdir $(UMFILES))
ALLJS = $(notdir $(JSFILES))
ALLROBO = $(notdir $(ROBOTRACKFILES))
ALLWISTATS = $(notdir $(WIRELESSSTATSFILES))
ALLBLOB = $(notdir $(BLOBFILES))
......@@ -201,7 +192,6 @@ INSTALLFILES = $(addprefix $(INSTALL_SBINDIR)/, htmlinstall) \
$(addprefix $(INSTALL_WWWDIR)/pix/, $(ALLPIXES)) \
$(addprefix $(INSTALL_WWWDIR)/tutorial/, $(ALLTUTS)) \
$(addprefix $(INSTALL_WWWDIR)/doc/, $(ALLDOCS)) \
$(addprefix $(INSTALL_WWWDIR)/webdb/, $(ALLWEBDB)) \
$(addprefix $(INSTALL_WWWDIR)/protogeni/, $(ALLPGENI)) \
$(addprefix $(INSTALL_WWWDIR)/downloads/, $(ALLDOWNLOADS)) \
$(addprefix $(INSTALL_WWWDIR)/buildui/, $(ALLBUI)) \
......@@ -210,7 +200,6 @@ INSTALLFILES = $(addprefix $(INSTALL_SBINDIR)/, htmlinstall) \
$(addprefix $(INSTALL_WWWDIR)/timetree/, $(ALLTT)) \
$(addprefix $(INSTALL_WWWDIR)/usermap/, $(ALLUM)) \
$(addprefix $(INSTALL_WWWDIR)/js/, $(ALLJS)) \
$(addprefix $(INSTALL_WWWDIR)/robotrack/, $(ALLROBO)) \
$(addprefix $(INSTALL_WWWDIR)/wireless-stats/, $(ALLWISTATS)) \
$(addprefix $(INSTALL_WWWDIR)/autostatus-icons/, $(ALLICONS)) \
$(addprefix $(INSTALL_WWWDIR)/blob/, $(ALLBLOB)) \
......
<?php
#
# EMULAB-COPYRIGHT
# Copyright (c) 2000-2008 University of Utah and the Flux Group.
# Copyright (c) 2000-2012 University of Utah and the Flux Group.
# All rights reserved.
#
include("defs.php3");
......@@ -132,7 +132,7 @@ if ($approval == "postpone") {
echo "<input type=hidden name=pcplab_okay value=$pcplab_okay>\n";
echo "<input type=hidden name=ron_okay value=$ron_okay>\n";
echo "<input type=hidden name=message value='".
htmlspecialchars($message, ENT_QUOTES) . "'>\n";
CleanString($message) . "'>\n";
echo "<b><input type=submit name=back value=Back></b>\n";
echo "</form>\n";
......
<?php
#
# EMULAB-COPYRIGHT
# Copyright (c) 2000-2008 University of Utah and the Flux Group.
# Copyright (c) 2000-2012 University of Utah and the Flux Group.
# All rights reserved.
#
include("defs.php3");
......@@ -228,7 +228,7 @@ echo "<tr>
<td align=center class=left>
<textarea name=message rows=15 cols=70>";
if (isset($message)) {
echo ereg_replace("\r", "", $message);
echo ereg_replace("\r", "", CleanString($message));
}
echo "</textarea>
</td>
......
<?php
#
# EMULAB-COPYRIGHT
# Copyright (c) 2000-2003, 2006, 2007 University of Utah and the Flux Group.
# Copyright (c) 2000-2012 University of Utah and the Flux Group.
# All rights reserved.
#
include("defs.php3");
......@@ -154,17 +154,17 @@ while (list ($uid_idx, $grouplist) = each ($approvelist)) {
$date_applied = "--";
}
$name = $user->name();
$email = $user->email();
$title = $user->title();
$affil = $user->affil();
$addr = $user->addr();
$addr2 = $user->addr2();
$city = $user->city();
$state = $user->state();
$zip = $user->zip();
$country = $user->country();
$phone = $user->phone();
$name = CleanString($user->name());
$email = CleanString($user->email());
$title = CleanString($user->title());
$affil = CleanString($user->affil());
$addr = CleanString($user->addr());
$addr2 = CleanString($user->addr2());
$city = CleanString($user->city());
$state = CleanString($user->state());
$zip = CleanString($user->zip());
$country = CleanString($user->country());
$phone = CleanString($user->phone());
echo "<tr>
<td rowspan=2>$newuid</td>
......
<?php
#
# EMULAB-COPYRIGHT
# Copyright (c) 2000-2002, 2006 University of Utah and the Flux Group.
# All rights reserved.
#
include("defs.php3");
#
# Only known and logged in users can be verified.
#
$this_user = CheckLoginOrDie();
$uid = $this_user->uid();
$isadmin = ISADMIN();
if (! $isadmin) {
USERERROR("Only testbed administrators people can access this page!", 1);
}
ignore_user_abort(1);
#
# Walk the list of post variables, looking for the special post format.
# See approvewauser_form.php3:
#
# uid menu node_id
# name=stoller$$approval-node_id value=approved,denied,postpone
# name=stoller$$trust-node_id value=user,local_root
#
# We make two passes over the post vars. The first does a sanity check so
# that we can bail out without doing anything. This allows the user to
# back up and make changes without worrying about some stuff being done and
# other stuff not.
#
while (list ($header, $value) = each ($HTTP_POST_VARS)) {
#echo "$header: $value<br>\n";
$approval_string = strstr($header, "\$\$approval-");
if (! $approval_string) {
continue;
}
$user = substr($header, 0, strpos($header, "\$\$", 0));
$node_id = substr($approval_string, strlen("\$\$approval-"));
$approval = $value;
if (!$user || strcmp($user, "") == 0) {
TBERROR("Parse error finding user in approvewauser.php3", 1);
}
if (!$node_id || strcmp($node_id, "") == 0) {
TBERROR("Parse error finding node_id in approvewauser.php3", 1);
}
if (!$approval || strcmp($approval, "") == 0) {
TBERROR("Parse error finding approval in approvewauser.php3", 1);
}
#
# There should be a corresponding trust variable in the POST vars.
# Note that we construct the variable name and indirect to it.
#
$foo = "$user\$\$trust-$node_id";
$newtrust = $$foo;
if (!$newtrust || strcmp($newtrust, "") == 0) {
TBERROR("Parse error finding trust in approvewauser.php3", 1);
}
#echo "User $user, NodeID $node_id,
# Approval $approval, Trust $newtrust<br>\n";
if (strcmp($newtrust, "user") &&
strcmp($newtrust, "local_root")) {
TBERROR("Invalid trust $newtrust for user $user approvewauser.php3.",
1);
}
#
# Verify an actual user that is being approved.
#
if (! ($target_user = User::Lookup($user))) {
TBERROR("Trying to approve unknown user $user.", 1);
}
#
# Check if already approved. If already an approved account,
# something went wrong.
#
$query_result =
DBQueryFatal("select trust from widearea_accounts ".
"where uid='$user' and node_id='$node_id' and ".
" trust!='none'");
if (mysql_num_rows($query_result)) {
$row = mysql_fetch_array($query_result);
$trust = $row[trust];
USERERROR("$user is already approved on $node_id with $trust!", 1);
}
#
# Verify approval value.
#
if (strcmp($approval, "postpone") &&
strcmp($approval, "deny") &&
strcmp($approval, "nuke") &&
strcmp($approval, "approve")) {
TBERROR("Invalid approval value $approval in approvewauser.php3.", 1);
}
}
#
# Standard Testbed Header
#
PAGEHEADER("Widearea Accounts Approval Form");
reset($HTTP_POST_VARS);
#
# Okay, all sanity tests passed for all post vars. Now do the actual work.
#
while (list ($header, $value) = each ($HTTP_POST_VARS)) {
#echo "$header: $value<br>\n";
$approval_string = strstr($header, "\$\$approval-");
if (! $approval_string) {
continue;
}
$user = substr($header, 0, strpos($header, "\$\$", 0));
$node_id = substr($approval_string, strlen("\$\$approval-"));
$approval = $value;
#
# Corresponding trust value.
#
$foo = "$user\$\$trust-$node_id";
$newtrust = $$foo;
#
# Get the current status for the user, which we might need to change.
#
# We change the status only if this person is getting a new account.
# In this case, the status will be either "newuser" or "unapproved",
# and we will change it to "unapproved" or "active", respectively.
# If the status is "active", we leave it alone.
#
if (! ($target_user = User::Lookup($user))) {
TBERROR("Trying to approve unknown user $user.", 1);
}
$curstatus = $target_user->status();
$user_email = $target_user->email();
$user_name = $target_user->name();
#echo "Status = $curstatus, Email = $user_email<br>\n";
#
# Email info for current user.
#
$uid_name = $this_user->name();
$uid_email = $this_user->email();
#
# Well, looks like everything is okay. Change the project membership
# value appropriately.
#
if (strcmp($approval, "postpone") == 0) {
echo "<p>
Account status for user $user was
<b>postponed</b> for later decision.\n";
continue;
}
if (strcmp($approval, "deny") == 0) {
#
# Must delete the widearea_account record since we require that the
# user reapply once denied. Send the luser email to let him know.
#
$query_result =
DBQueryFatal("delete from widearea_accounts ".
"where uid='$user' and node_id='$node_id'");
TBMAIL("$user_name '$user' <$user_email>",
"Account request on $node_id denied",
"\n".
"This message is to notify you that you have been denied\n".
"local account access on $node_id!\n".
"\n\n".
"Thanks,\n".
"Testbed Operations\n",
"From: $uid_name <$uid_email>\n".
"Cc: $TBMAIL_OPS\n".
"Bcc: $TBMAIL_AUDIT\n".
"Errors-To: $TBMAIL_WWW");
echo "<p>
User $user was <b>denied</b> an account on $node_id.
<br>
The user will need to reapply again if this was in error.\n";
continue;
}
if (strcmp($approval, "nuke") == 0) {
#
# Must delete the group_membership record since we require that the
# user reapply once denied. Send the luser email to let him know.
#
$query_result =
DBQueryFatal("delete from widearea_accounts ".
"where uid='$user' and node_id='$node_id'");
#
# See if user is in any other projects (even unapproved).
#
$project_list = $target_user->ProjectMembershipList();
#
# If yes, then we cannot safely delete the user account.
#
if (count($project_list)) {
echo "<p>
User $user was <b>denied</b> an account on $node_id.
<br>
Since the user is a member (or requesting membership)
in other projects, the account cannot be safely removed.\n";
continue;
}
#
# No other project membership. If the user is unapproved/newuser,
# it means he was never approved in any project, and so will
# likely not be missed. He will be unapproved if he did his
# verification.
#
if (strcmp($curstatus, "newuser") &&
strcmp($curstatus, "unapproved")) {
echo "<p>
User $user was <b>denied</b> an account on $node_id.
<br>
Since the user has been approved by, or was active in other
projects in the past, the account cannot be safely removed.
\n";
continue;
}
$target_user->Delete();
echo "<p>
User $user was <b>denied</b> an account on $node_id.
<br>
The account has also been <b>terminated</b> with prejudice!\n";
continue;
}
if (strcmp($approval, "approve") == 0) {
#
# Change the trust value accordingly.
#
$query_result =
DBQueryFatal("UPDATE widearea_accounts ".
"set trust='$newtrust',date_approved=now() ".
"WHERE uid='$user' and node_id='$node_id'");
#
# Change the status if necessary. This only happens for new
# users being added to their first project. After this, the status is
# going to be "active", and we just leave it that way.
#
if (strcmp($curstatus, "active")) {
if (strcmp($curstatus, "newuser") == 0) {
$newstatus = "unverified";
}
elseif (strcmp($curstatus, "unapproved") == 0) {
$newstatus = "active";
}
elseif (strcmp($curstatus, "unverified") == 0) {
$newstatus = "unverified";
}
else {
TBERROR("Invalid $user status $curstatus!", 1);
}
$target_user->SetStatus($newstatus);
}
$url = CreateURL("showpubkeys", $target_user);
TBMAIL("$user_name '$user' <$user_email>",
"Widearea account granted on '$node_id' ",
"\n".
"This message is to notify you that you have been granted an\n".
"account on $node_id with $newtrust permissions.\n".
"\n".
"In order to log into this node, you must upload an ssh key\n".
"via: ${TBBASE}/$url\n".
"\n\n".
"Thanks,\n".
"Testbed Operations\n",
"From: $uid_name <$uid_email>\n".
"Cc: $TBMAIL_OPS\n".
"Bcc: $TBMAIL_AUDIT\n".
"Errors-To: $TBMAIL_WWW");
echo "<p>
User $user was <b>granted</b> an account on $node_id
with $newtrust permissions.\n";
#
# XXX
#
DBQueryFatal("update nodes set update_accounts=update_accounts+1 ".
"where node_id='$node_id' and update_accounts<2");
continue;
}
}
#
# Standard Testbed Footer
#
PAGEFOOTER();
?>
<?php
#
# EMULAB-COPYRIGHT
# Copyright (c) 2000-2008 University of Utah and the Flux Group.
# All rights reserved.
#
include("defs.php3");
#
# Only admin types can use this page.
#
$this_user = CheckLoginOrDie();
$auth_usr = $this_user->uid();
$isadmin = ISADMIN();
if (! $isadmin) {
USERERROR("Only testbed administrators people can access this page!", 1);
}
#
# Standard Testbed Header
#
PAGEHEADER("Widearea Accounts Approval Form");
echo "
<h2>Approve local accounts on specific widearea nodes</h2>
<center>
<h4>You have the following choices for <b>Action</b>:</h4>
<table cellspacing=2 border=0>
<tr>
<td><b>Postpone</b></td>
<td>Do nothing; application remains, pending a decision.</td>
</tr>
<tr>
<td><b>Deny</b></td>
<td>Deny user application and so notify the user.</td>
</tr>
<tr>
<td><b>Nuke</b></td>
<td>Nuke user application. Kills user account, without
notice to user. Useful for bogus applications.</td>
</tr>
<tr>
<td><b>Approve</b></td>
<td>Approve the user</td>
</tr>
</table>
<br />
<h4>You have the following choices for <b>Trust</b>:</h4>
<table cellspacing=2 cellpadding=4 border=0>
<tr>
<td><b>User</b></td>
<td>User may log into the node</td>
</tr>
<tr>
<td><b>Root</b></td>
<td>User gets local root on the node</td>
</tr>
</table>
<br />
</center>
<br />
\n";
#
# Find all of the unapproved widearea account requests.
#
$query_result =
DBQueryFatal("select w.* from widearea_accounts as w ".
"left join users as u on u.uid_idx=w.uid_idx ".
"WHERE u.status!='" . TBDB_USERSTATUS_UNVERIFIED . "' and ".
"u.status!='" . TBDB_USERSTATUS_NEWUSER . "' and ".
"w.trust='" . TBDB_TRUSTSTRING_NONE . "'");
if (mysql_num_rows($query_result) == 0) {
USERERROR("There are no new accounts that need approval.", 1);
}
#
# Now build a table with a bunch of selections. The thing to note about the
# form inside this table is that the selection fields are constructed with
# name= on the fly, from the uid of the user to be approved. In other words:
#
# uid menu node_id
# name=stoller$$approval-wa33 value=approved,denied,postpone
# name=stoller$$trust-wa33 value=user,local_root
#
# so that we can go through the entire list of post variables, looking
# for these. The alternative is to work backwards, and I do not like that.
#
echo "<table width=\"100%\" border=2 cellpadding=2 cellspacing=2
align=\"center\">\n";
echo "<tr>
<th rowspan=2>User</th>
<th rowspan=2>Node ID</th>
<th rowspan=2>Date<br>Applied</th>
<th rowspan=2>Action</th>
<th rowspan=2>Trust</th>
<th>Name</th>
<th>Title</th>
<th>Affil</th>
<th>E-mail</th>
<th>Phone</th>
</tr>
<tr>
<th colspan=5>Address</th>
</tr>\n";
echo "<form action='approvewauser.php3' method='post'>\n";
while ($usersrow = mysql_fetch_array($query_result)) {
$newuid = $usersrow["uid"];
$node_id = $usersrow["node_id"];
$date_applied = $usersrow["date_applied"];
#
# Cause this field was added late and might be null.
#
if (! $date_applied) {
$date_applied = "--";
}
if (! ($user = User::Lookup($newuid))) {
TBERROR("Could not lookup user $uid_idx", 1);
}
$name = $user->name();
$email = $user->email();
$title = $user->title();
$affil = $user->affil();
$addr = $user->addr();
$addr2 = $user->addr2();
$city = $user->city();
$state = $user->state();
$zip = $user->zip();
$country = $user->country();
$phone = $user->phone();
echo "<tr>
<td colspan=10> </td>
</tr>
<tr>
<td rowspan=2>$newuid</td>
<td rowspan=2>
<A href='shownode.php3?node_id=$node_id'>$node_id</a></td>
<td rowspan=2>$date_applied</td>
<td rowspan=2>