Index: branches/stable/DNSDB.pm
===================================================================
--- branches/stable/DNSDB.pm	(revision 535)
+++ branches/stable/DNSDB.pm	(revision 544)
@@ -28,5 +28,5 @@
 use Crypt::PasswdMD5;
 use Net::SMTP;
-use NetAddr::IP;
+use NetAddr::IP qw(:lower);
 use POSIX;
 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
@@ -39,10 +39,12 @@
 	&changeGroup
 	&loadConfig &connectDB &finish
-	&addDomain &delDomain &domainName &domainID
+	&addDomain &delDomain &domainName &revName &domainID &addRDNS
+	&getZoneCount &getZoneList
 	&addGroup &delGroup &getChildren &groupName
 	&addUser &updateUser &delUser &userFullName &userStatus &getUserData
 	&getSOA	&getRecLine &getDomRecs &getRecCount
 	&addRec &updateRec &delRec
-        &getParents
+	&getTypelist
+	&parentID
 	&isParent
 	&domStatus &importAXFR
@@ -59,10 +61,12 @@
 		&changeGroup
 		&loadConfig &connectDB &finish
-		&addDomain &delDomain &domainName &domainID
+		&addDomain &delDomain &domainName &revName &domainID &addRDNS
+		&getZoneCount &getZoneList
 		&addGroup &delGroup &getChildren &groupName
 		&addUser &updateUser &delUser &userFullName &userStatus &getUserData
 		&getSOA &getRecLine &getDomRecs &getRecCount
 		&addRec &updateRec &delRec
-	        &getParents
+		&getTypelist
+		&parentID
 		&isParent
 		&domStatus &importAXFR
@@ -139,4 +143,468 @@
 		perpage		=> 15,
 	);
+
+## (Semi)private variables
+# Hash of functions for validating record types.  Filled in initGlobals() since
+# it relies on visibility flags from the rectypes table in the DB
+my %validators;
+
+
+##
+## utility functions
+# _rectable()
+# Takes default+rdns flags, returns appropriate table name
+sub _rectable {
+  my $def = shift;
+  my $rev = shift;
+
+  return 'records' if $def ne 'y';
+  return 'default_records' if $rev ne 'y';
+  return 'default_rev_records';
+} # end _rectable()
+
+# _recparent()
+# Takes default+rdns flags, returns appropriate parent-id column name
+sub _recparent {
+  my $def = shift;
+  my $rev = shift;
+
+  return 'group_id' if $def eq 'y';
+  return 'rdns_id' if $rev eq 'y';
+  return 'domain_id';
+} # end _recparent()
+
+# Check an IP to be added in a reverse zone to see if it's really in the requested parent.
+# Takes a database handle, default and reverse flags, IP (fragment) to check, parent zone ID,
+# and a reference to a NetAddr::IP object (also used to pass back a fully-reconstructed IP for
+# database insertion)
+sub _ipparent {
+  my $dbh = shift;
+  my $defrec = shift;
+  my $revrec = shift;
+  my $val = shift;
+  my $id = shift;
+  my $addr = shift;
+
+  return if $revrec ne 'y';	# this sub not useful in forward zones
+
+  $$addr = NetAddr::IP->new($$val);	 #necessary?
+
+  # subsub to split, reverse, and overlay an IP fragment on a netblock
+  sub __rev_overlay {
+    my $splitme = shift;	# ':' or '.', m'lud?
+    my $parnet = shift;
+    my $val = shift;
+    my $addr = shift;
+
+    my $joinme = $splitme;
+    $splitme = '\.' if $splitme eq '.';
+    my @working = reverse(split($splitme, $parnet->addr));
+    my @parts = reverse(split($splitme, $$val));
+    for (my $i = 0; $i <= $#parts; $i++) {
+      $working[$i] = $parts[$i];
+    }
+    my $checkme = NetAddr::IP->new(join($joinme, reverse(@working))) or return 0;
+    return 0 unless $checkme->within($parnet);
+    $$addr = $checkme;	# force "correct" IP to be recorded.
+    return 1;
+  }
+
+  my ($parstr) = $dbh->selectrow_array("SELECT revnet FROM revzones WHERE rdns_id = ?", undef, ($id));
+  my $parnet = NetAddr::IP->new($parstr);
+
+  # Fail early on v6-in-v4 or v4-in-v6.  We're not accepting these ATM.
+  return 0 if $parnet->addr =~ /\./ && $$val =~ /:/;
+  return 0 if $parnet->addr =~ /:/ && $$val =~ /\./;
+
+  if ($$addr && $$val =~ /^[\da-fA-F][\da-fA-F:]+[\da-fA-F]$/) {
+    # the only case where NetAddr::IP's acceptance of legitimate IPs is "correct" is for a proper IPv6 address.
+    # the rest we have to restructure before fiddling.  *sigh*
+    return 1 if $$addr->within($parnet);
+  } else {
+    # We don't have a complete IP in $$val (yet)
+    if ($parnet->addr =~ /:/) {
+      $$val =~ s/^:+//;	 # gotta strip'em all...
+      return __rev_overlay(':', $parnet, $val, $addr);
+    }
+    if ($parnet->addr =~ /\./) {
+      $$val =~ s/^\.+//;
+      return __rev_overlay('.', $parnet, $val, $addr);
+    }
+    # should be impossible to get here...
+  }
+  # ... and here.
+  # can't do nuttin' in forward zones
+} # end _ipparent()
+
+# A little different than _ipparent above;  this tries to *find* the parent zone of a hostname
+sub _hostparent {
+  my $dbh = shift;
+  my $hname = shift;
+  
+  my @hostbits = split /\./, $hname;
+  my $sth = $dbh->prepare("SELECT count(*),domain_id FROM domains WHERE domain = ? GROUP BY domain_id");
+  foreach (@hostbits) {
+    $sth->execute($hname);
+    my ($found, $parid) = $sth->fetchrow_array;
+    if ($found) {
+      return $parid;
+    }
+    $hname =~ s/^$_\.//;
+  }
+} # end _hostparent()
+
+##
+## Record validation subs.
+##
+
+# A record
+sub _validate_1 {
+  my $dbh = shift;
+
+  my %args = @_;
+
+  return ('FAIL', 'Reverse zones cannot contain A records') if $args{revrec} eq 'y';
+
+  # Coerce all hostnames to end in ".DOMAIN" for group/default records,
+  # or the intended parent domain for live records.
+  my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id}));
+  ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
+
+  # Check IP is well-formed, and that it's a v4 address
+  # Fail on "compact" IPv4 variants, because they are not consistent and predictable.
+  return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv4 address")
+	unless ${$args{val}} =~ /^\d+\.\d+\.\d+\.\d+$/;
+  return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv4 address")
+	unless $args{addr} && !$args{addr}->{isv6};
+  # coerce IP/value to normalized form for storage
+  ${$args{val}} = $args{addr}->addr;
+
+  return ('OK','OK');
+} # done A record
+
+# NS record
+sub _validate_2 {
+  my $dbh = shift;
+
+  my %args = @_;
+
+  # Coerce the hostname to "DOMAIN" for forward default records, "ZONE" for reverse default records,
+  # or the intended parent zone for live records.
+##fixme:  allow for delegating <subdomain>.DOMAIN?
+  if ($args{revrec} eq 'y') {
+    my $pname = ($args{defrec} eq 'y' ? 'ZONE' : revName($dbh,$args{id}));
+    ${$args{host}} = $pname if ${$args{host}} ne $pname;
+  } else {
+    my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id}));
+    ${$args{host}} = $pname if ${$args{host}} ne $pname;
+  }
+
+# Let this lie for now.  Needs more magic.
+#  # Check IP is well-formed, and that it's a v4 address
+#  return ('FAIL',"A record must be a valid IPv4 address")
+#	unless $addr && !$addr->{isv6};
+#  # coerce IP/value to normalized form for storage
+#  $$val = $addr->addr;
+
+  return ('OK','OK');
+} # done NS record
+
+# CNAME record
+sub _validate_5 {
+  my $dbh = shift;
+
+  my %args = @_;
+
+# Not really true, but these are only useful for delegating smaller-than-/24 IP blocks.
+# This is fundamentally a messy operation and should really just be taken care of by the
+# export process, not manual maintenance of the necessary records.
+  return ('FAIL', 'Reverse zones cannot contain CNAME records') if $args{revrec} eq 'y';
+
+  # Coerce all hostnames to end in ".DOMAIN" for group/default records,
+  # or the intended parent domain for live records.
+  my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id}));
+  ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
+
+  return ('OK','OK');
+} # done CNAME record
+
+# SOA record
+sub _validate_6 {
+  # Smart monkeys won't stick their fingers in here;  we have
+  # separate dedicated routines to deal with SOA records.
+  return ('OK','OK');
+} # done SOA record
+
+# PTR record
+sub _validate_12 {
+  my $dbh = shift;
+
+  my %args = @_;
+
+  if ($args{revrec} eq 'y') {
+    if ($args{defrec} eq 'n') {
+      return ('FAIL', "IP or IP fragment ${$args{val}} is not within ".revName($dbh, $args{id}))
+	unless _ipparent($dbh, $args{defrec}, $args{revrec}, $args{val}, $args{id}, \$args{addr});
+      ${$args{val}} = $args{addr}->addr;
+    } else {
+      if (${$args{val}} =~ /\./) {
+	# looks like a v4 or fragment
+	if (${$args{val}} =~ /^\d+\.\d+\.\d+\.\d+$/) {
+	  # woo!  a complete IP!  validate it and normalize, or fail.
+	  $args{addr} = NetAddr::IP->new(${$args{val}})
+		or return ('FAIL', "IP/value looks like IPv4 but isn't valid");
+	  ${$args{val}} = $args{addr}->addr;
+	} else {
+	  ${$args{val}} =~ s/^\.*/ZONE./ unless ${$args{val}} =~ /^ZONE/;
+	}
+      } elsif (${$args{val}} =~ /[a-f:]/) {
+	# looks like a v6 or fragment
+	${$args{val}} =~ s/^:*/ZONE::/ if !$args{addr} && ${$args{val}} !~ /^ZONE/;
+	if ($args{addr}) {
+	  if ($args{addr}->addr =~ /^0/) {
+	    ${$args{val}} =~ s/^:*/ZONE::/ unless ${$args{val}} =~ /^ZONE/;
+	  } else {
+	    ${$args{val}} = $args{addr}->addr;
+	  }
+	}
+      } else {
+	# bare number (probably).  These could be v4 or v6, so we'll
+	# expand on these on creation of a reverse zone.
+	${$args{val}} = "ZONE,${$args{val}}" unless ${$args{val}} =~ /^ZONE/;
+      }
+      ${$args{host}} =~ s/\.*$/\.$config{domain}/ if ${$args{host}} !~ /(?:$config{domain}|ADMINDOMAIN)$/;
+    }
+
+# Multiple PTR records do NOT generally do what most people believe they do,
+# and tend to fail in the most awkward way possible.  Check and warn.
+# We use $val instead of $addr->addr since we may be in a defrec, and may have eg "ZONE::42" or "ZONE.12"
+
+    my @checkvals = (${$args{val}});
+    if (${$args{val}} =~ /,/) {
+      # push . and :: variants into checkvals if val has ,
+      my $tmp;
+      ($tmp = ${$args{val}}) =~ s/,/./;
+      push @checkvals, $tmp;
+      ($tmp = ${$args{val}}) =~ s/,/::/;
+      push @checkvals, $tmp;
+    }
+    my $pcsth = $dbh->prepare("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec})." WHERE val = ?");
+    foreach my $checkme (@checkvals) {
+      my $ptrcount;
+      ($ptrcount) = $dbh->selectrow_array("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec}).
+	" WHERE val = ?", undef, ($checkme));
+      return ('WARN', "PTR record for $checkme already exists;  adding another will probably not do what you want")
+	if $ptrcount;
+    }
+  } else {
+    # Not absolutely true but only useful if you hack things up for sub-/24 v4 reverse delegations
+    # Simpler to just create the reverse zone and grant access for the customer to edit it, and create direct
+    # PTR records on export
+    return ('FAIL',"Forward zones cannot contain PTR records");
+  }
+
+  return ('OK','OK');
+} # done PTR record
+
+# MX record
+sub _validate_15 {
+  my $dbh = shift;
+
+  my %args = @_;
+
+# Not absolutely true but WTF use is an MX record for a reverse zone?
+  return ('FAIL', 'Reverse zones cannot contain MX records') if $args{revrec} eq 'y';
+
+  return ('FAIL', "Distance is required for MX records") unless defined(${$args{dist}});
+  ${$args{dist}} =~ s/\s*//g;
+  return ('FAIL',"Distance is required, and must be numeric") unless ${$args{dist}} =~ /^\d+$/;
+
+  ${$args{fields}} = "distance,";
+  push @{$args{vallist}}, ${$args{dist}};
+
+  # Coerce all hostnames to end in ".DOMAIN" for group/default records,
+  # or the intended parent domain for live records.
+  my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id}));
+  ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
+
+  return ('OK','OK');
+} # done MX record
+
+# TXT record
+sub _validate_16 {
+  # Could arguably put a WARN return here on very long (>512) records
+  return ('OK','OK');
+} # done TXT record
+
+# RP record
+sub _validate_17 {
+  # Probably have to validate these some day
+  return ('OK','OK');
+} # done RP record
+
+# AAAA record
+sub _validate_28 {
+  my $dbh = shift;
+
+  my %args = @_;
+
+  return ('FAIL', 'Reverse zones cannot contain AAAA records') if $args{revrec} eq 'y';
+
+  # Coerce all hostnames to end in ".DOMAIN" for group/default records,
+  # or the intended parent domain for live records.
+  my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id}));
+  ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
+
+  # Check IP is well-formed, and that it's a v6 address
+  return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv6 address")
+	unless $args{addr} && $args{addr}->{isv6};
+  # coerce IP/value to normalized form for storage
+  ${$args{val}} = $args{addr}->addr;
+
+  return ('OK','OK');
+} # done AAAA record
+
+# SRV record
+sub _validate_33 {
+  my $dbh = shift;
+
+  my %args = @_;
+
+# Not absolutely true but WTF use is an SRV record for a reverse zone?
+  return ('FAIL', 'Reverse zones cannot contain SRV records') if $args{revrec} eq 'y';
+
+  return ('FAIL', "Distance is required for SRV records") unless defined(${$args{dist}});
+  ${$args{dist}} =~ s/\s*//g;
+  return ('FAIL',"Distance is required, and must be numeric") unless ${$args{dist}} =~ /^\d+$/;
+
+  return ('FAIL',"SRV records must begin with _service._protocol [${$args{host}}]")
+	unless ${$args{host}} =~ /^_[A-Za-z]+\._[A-Za-z]+\.[a-zA-Z0-9-]+/;
+  return ('FAIL',"Port and weight are required for SRV records")
+	unless defined(${$args{weight}}) && defined(${$args{port}});
+  ${$args{weight}} =~ s/\s*//g;
+  ${$args{port}} =~ s/\s*//g;
+
+  return ('FAIL',"Port and weight are required, and must be numeric")
+	unless ${$args{weight}} =~ /^\d+$/ && ${$args{port}} =~ /^\d+$/;
+
+  ${$args{fields}} = "distance,weight,port,";
+  push @{$args{vallist}}, (${$args{dist}}, ${$args{weight}}, ${$args{port}});
+
+  # Coerce all hostnames to end in ".DOMAIN" for group/default records,
+  # or the intended parent domain for live records.
+  my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id}));
+  ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
+
+  return ('OK','OK');
+} # done SRV record
+
+# Now the custom types
+
+# A+PTR record.  With a very little bit of magic we can also use this sub to validate AAAA+PTR.  Whee!
+sub _validate_65280 {
+  my $dbh = shift;
+
+  my %args = @_;
+
+  my $code = 'OK';
+  my $msg = 'OK';
+
+  if ($args{defrec} eq 'n') {
+    # live record;  revrec determines whether we validate the PTR or A component first.
+
+    if ($args{revrec} eq 'y') {
+      ($code,$msg) = _validate_12($dbh, %args);
+      return ($code,$msg) if $code eq 'FAIL';
+
+      # Check if the reqested domain exists.  If not, coerce the type down to PTR and warn.
+      if (!(${$args{domid}} = _hostparent($dbh, ${$args{host}}))) {
+	my $addmsg = "Record added as PTR instead of $typemap{${$args{rectype}}};  domain not found for ${$args{host}}";
+	$msg .= "\n$addmsg" if $code eq 'WARN';
+	$msg = $addmsg if $code eq 'OK';
+        ${$args{rectype}} = $reverse_typemap{PTR};
+        return ('WARN', $msg);
+      }
+
+      # Add domain ID to field list and values
+      ${$args{fields}} .= "domain_id,";
+      push @{$args{vallist}}, ${$args{domid}};
+
+    } else {
+      ($code,$msg) = _validate_1($dbh, %args) if ${$args{rectype}} == 65280;
+      ($code,$msg) = _validate_28($dbh, %args) if ${$args{rectype}} == 65281;
+      return ($code,$msg) if $code eq 'FAIL';
+
+      # Check if the requested reverse zone exists - note, an IP fragment won't
+      # work here since we don't *know* which parent to put it in.
+      # ${$args{val}} has been validated as a valid IP by now, in one of the above calls.
+      my ($revid) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet >> ?".
+	" ORDER BY masklen(revnet) DESC", undef, (${$args{val}}));
+      if (!$revid) {
+        $msg = "Record added as ".(${$args{rectype}} == 65280 ? 'A' : 'AAAA').
+		" instead of $typemap{${$args{rectype}}};  reverse zone not found for ${$args{val}}";
+	${$args{rectype}} = (${$args{rectype}} == 65280 ? $reverse_typemap{A} : $reverse_typemap{AAAA});
+	return ('WARN', $msg);
+      }
+
+      # Check for duplicate PTRs.  Note we don't have to play games with $code and $msg, because
+      # by definition there can't be duplicate PTRs if the reverse zone isn't managed here.
+      my ($ptrcount) = $dbh->selectrow_array("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec}).
+	" WHERE val = ?", undef, ${$args{val}});
+      if ($ptrcount) {
+	$msg = "PTR record for ${$args{val}} already exists;  adding another will probably not do what you want";
+	$code = 'WARN';
+      }
+
+      ${$args{fields}} .= "rdns_id,";
+      push @{$args{vallist}}, $revid;
+    }
+
+  } else {	# defrec eq 'y'
+    if ($args{revrec} eq 'y') {
+      ($code,$msg) = _validate_12($dbh, %args);
+      return ($code,$msg) if $code eq 'FAIL';
+      if (${$args{rectype}} == 65280) {
+	return ('FAIL',"A+PTR record must be a valid IPv4 address or fragment")
+		if ${$args{val}} =~ /:/;
+	${$args{val}} =~ s/^ZONE,/ZONE./;       # Clean up after uncertain IP-fragment-type from _validate_12
+      } elsif (${$args{rectype}} == 65281) {
+	return ('FAIL',"AAAA+PTR record must be a valid IPv6 address or fragment")
+		if ${$args{val}} =~ /\./;
+	${$args{val}} =~ s/^ZONE,/ZONE::/;      # Clean up after uncertain IP-fragment-type from _validate_12
+      }
+    } else {
+      # This is easy.  I also can't see a real use-case for A/AAAA+PTR in *all* forward
+      # domains, since you wouldn't be able to substitute both domain and reverse zone
+      # sanely, and you'd end up with guaranteed over-replicated PTR records that would
+      # confuse the hell out of pretty much anything that uses them.
+##fixme: make this a config flag?
+      return ('FAIL', "$typemap{${$args{rectype}}} records not allowed in default domains");
+    }
+  }
+
+  return ($code, $msg);
+} # done A+PTR record
+
+# AAAA+PTR record
+# A+PTR above has been magicked to handle AAAA+PTR as well.
+sub _validate_65281 {
+  return _validate_65280(@_);
+} # done AAAA+PTR record
+
+# PTR template record
+sub _validate_65282 {
+  return ('OK','OK');
+} # done PTR template record
+
+# A+PTR template record
+sub _validate_65283 {
+  return ('OK','OK');
+} # done AAAA+PTR template record
+
+# AAAA+PTR template record
+sub _validate_65284 {
+  return ('OK','OK');
+} # done AAAA+PTR template record
+
 
 
@@ -327,9 +795,17 @@
 
 # load record types from database
-  my $sth = $dbh->prepare("select val,name from rectypes");
+  my $sth = $dbh->prepare("SELECT val,name,stdflag FROM rectypes");
   $sth->execute;
-  while (my ($recval,$recname) = $sth->fetchrow_array()) {
+  while (my ($recval,$recname,$stdflag) = $sth->fetchrow_array()) {
     $typemap{$recval} = $recname;
     $reverse_typemap{$recname} = $recval;
+    # now we fill the record validation function hash
+    if ($stdflag < 5) {
+      my $fn = "_validate_$recval";
+      $validators{$recval} = \&$fn;
+    } else {
+      my $fn = "sub { return ('FAIL','Type $recval ($recname) not supported'); }";
+      $validators{$recval} = eval $fn;
+    }
   }
 } # end initGlobals
@@ -534,32 +1010,41 @@
 # Log an action
 # Internal sub
-# Takes a database handle, domain_id, user_id, group_id, email, name and log entry
+# Takes a database handle and log entry hash containing at least:
+# user_id, group_id, log entry
+# and optionally one or more of:
+# username/email, user full name, domain_id, rdns_id
 ##fixme:  convert to trailing hash for user info
 # User info must contain a (user ID OR username)+fullname
 sub _log {
   my $dbh = shift;
-  my ($domain_id,$user_id,$group_id,$username,$name,$entry) = @_;
+
+  my %args = @_;
+
+  $args{rdns_id} = 0 if !$args{rdns_id};
+  $args{domain_id} = 0 if !$args{domain_id};
 
 ##fixme:  need better way(s?) to snag userinfo for log entries.  don't want to have
 # to pass around yet *another* constant (already passing $dbh, shouldn't need to)
   my $fullname;
-  if (!$user_id) {
-    ($user_id, $fullname) = $dbh->selectrow_array("SELECT user_id, firstname || ' ' || lastname FROM users".
-	" WHERE username=?", undef, ($username));
-  } elsif (!$username) {
-    ($username, $fullname) = $dbh->selectrow_array("SELECT username, firstname || ' ' || lastname FROM users".
-	" WHERE user_id=?", undef, ($user_id));
-  } else {
+  if (!$args{user_id}) {
+    ($args{user_id}, $fullname) = $dbh->selectrow_array("SELECT user_id, firstname || ' ' || lastname FROM users".
+	" WHERE username=?", undef, ($args{username}));
+  }
+  if (!$args{username}) {
+    ($args{username}, $fullname) = $dbh->selectrow_array("SELECT username, firstname || ' ' || lastname FROM users".
+	" WHERE user_id=?", undef, ($args{user_id}));
+  }
+  if (!$args{fullname}) {
     ($fullname) = $dbh->selectrow_array("SELECT firstname || ' ' || lastname FROM users".
-	" WHERE user_id=?", undef, ($user_id));
-  }
-
-  $name = $fullname if !$name;
+	" WHERE user_id=?", undef, ($args{user_id}));
+  }
+
+  $args{name} = $fullname if !$args{name};
 
 ##fixme:  farm out the actual logging to different subs for file, syslog, internal, etc based on config
-  $dbh->do("INSERT INTO log (domain_id,user_id,group_id,email,name,entry) VALUES (?,?,?,?,?,?)", undef,
-	($domain_id,$user_id,$group_id,$username,$name,$entry));
-#            123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
-#                     1         2         3         4         5         6         7
+  $dbh->do("INSERT INTO log (domain_id,rdns_id,user_id,group_id,email,name,entry) VALUES (?,?,?,?,?,?,?)",
+	undef,
+	($args{domain_id},$args{rdns_id},$args{user_id},$args{group_id},$args{username},$args{name},$args{entry}));
+
 } # end _log
 
@@ -619,6 +1104,6 @@
     ($dom_id) = $dbh->selectrow_array("SELECT domain_id FROM domains WHERE domain=?", undef, ($domain));
 
-    _log($dbh, $dom_id, $userinfo{id}, $group, $userinfo{name}, $userinfo{fullname},
-	"Added ".($state ? 'active' : 'inactive')." domain $domain");
+    _log($dbh, (domain_id => $dom_id, user_id => $userinfo{id}, group_id => $group, username => $userinfo{username},
+	entry => "Added ".($state ? 'active' : 'inactive')." domain $domain"));
 
     # ... and now we construct the standard records from the default set.  NB:  group should be variable.
@@ -634,13 +1119,15 @@
 	my @tmp1 = split /:/, $host;
 	my @tmp2 = split /:/, $val;
-	_log($dbh, $dom_id, $userinfo{id}, $group, $userinfo{name}, $userinfo{fullname},
+	_log($dbh, (domain_id => $dom_id, user_id => $userinfo{id}, group_id => $group,
+		username => $userinfo{username}, entry =>
 		"[new $domain] Added SOA record [contact $tmp1[0]] [master $tmp1[1]] ".
-		"[refresh $tmp2[0]] [retry $tmp2[1]] [expire $tmp2[2]] [minttl $tmp2[3]], TTL $ttl");
+		"[refresh $tmp2[0]] [retry $tmp2[1]] [expire $tmp2[2]] [minttl $tmp2[3]], TTL $ttl"));
       } else {
 	my $logentry = "[new $domain] Added record '$host $typemap{$type}";
 	$logentry .= " [distance $dist]" if $typemap{$type} eq 'MX';
 	$logentry .= " [priority $dist] [weight $weight] [port $port]" if $typemap{$type} eq 'SRV';
-	_log($dbh, $dom_id, $userinfo{id}, $group, $userinfo{name}, $userinfo{fullname},
-		$logentry." $val', TTL $ttl");
+	_log($dbh, (domain_id => $dom_id, user_id => $userinfo{id}, group_id => $group,
+		username => $userinfo{username}, entry =>
+		$logentry." $val', TTL $ttl"));
       }
     }
@@ -713,4 +1200,18 @@
 
 
+## DNSDB::revName()
+# Return the reverse zone name based on an rDNS ID
+# Takes a database handle and the rDNS ID
+# Returns the reverse zone name or undef on failure
+sub revName {
+  $errstr = '';
+  my $dbh = shift;
+  my $revid = shift;
+  my ($revname) = $dbh->selectrow_array("SELECT revnet FROM revzones WHERE rdns_id=?", undef, ($revid) );
+  $errstr = $DBI::errstr if !$revname;
+  return $revname if $revname;
+} # end revName()
+
+
 ## DNSDB::domainID()
 # Takes a database handle and domain name
@@ -724,4 +1225,189 @@
   return $domid if $domid;
 } # end domainID()
+
+
+## DNSDB::addRDNS
+# Adds a reverse DNS zone
+# Takes a database handle, CIDR block, numeric group, boolean(ish) state (active/inactive),
+# and user info hash (for logging).
+# Returns a status code and message
+sub addRDNS {
+  my $dbh = shift;
+  my $zone = NetAddr::IP->new(shift);
+  return ('FAIL',"Zone name must be a valid CIDR netblock") unless ($zone && $zone->addr !~ /^0/);
+  my $revpatt = shift;
+  my $group = shift;
+  my $state = shift;
+
+  my %userinfo = @_;	# remaining bits.
+# user ID, username, user full name
+
+  $state = 1 if $state =~ /^active$/;
+  $state = 1 if $state =~ /^on$/;
+  $state = 0 if $state =~ /^inactive$/;
+  $state = 0 if $state =~ /^off$/;
+
+  return ('FAIL',"Invalid zone status") if $state !~ /^\d+$/;
+
+# quick check to start to see if we've already got one
+  my ($rdns_id) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revzone=?", undef, ("$zone"));
+
+  return ('FAIL', "Zone already exists") if $rdns_id;
+
+  # Allow transactions, and raise an exception on errors so we can catch it later.
+  # Use local to make sure these get "reset" properly on exiting this block
+  local $dbh->{AutoCommit} = 0;
+  local $dbh->{RaiseError} = 1;
+
+#$dbh->selectrow_array("SELECT currval('users_user_id_seq')");
+  # Wrap all the SQL in a transaction
+  eval {
+    # insert the domain...
+    $dbh->do("INSERT INTO revzones (revnet,group_id,status) VALUES (?,?,?)", undef, ($zone, $group, $state));
+
+    # get the ID...
+    ($rdns_id) = $dbh->selectrow_array("SELECT currval('revzones_rdns_id_seq')");
+
+    _log($dbh, (rdns_id => $rdns_id, user_id => $userinfo{id}, group_id => $group, username => $userinfo{name},
+	entry => "Added ".($state ? 'active' : 'inactive')." reverse zone $zone"));
+
+    # ... and now we construct the standard records from the default set.  NB:  group should be variable.
+    my $sth = $dbh->prepare("SELECT host,type,val,ttl FROM default_rev_records WHERE group_id=?");
+    my $sth_in = $dbh->prepare("INSERT INTO records (rdns_id,host,type,val,ttl)".
+	" VALUES ($rdns_id,?,?,?,?)");
+    $sth->execute($group);
+    while (my ($host,$type,$val,$ttl) = $sth->fetchrow_array()) {
+      $host =~ s/ADMINDOMAIN/$config{domain}/g;
+##work
+# - replace ZONE in $val
+# - skip records not appropriate for the zone (skip A+PTR on v6 zones, and AAAA+PTR on v4 zones)
+#      $val =~ s/DOMAIN/$domain/g;
+      $sth_in->execute($host,$type,$val,$ttl);
+      if ($typemap{$type} eq 'SOA') {
+	my @tmp1 = split /:/, $host;
+	my @tmp2 = split /:/, $val;
+	_log($dbh, (rdns_id => $rdns_id, user_id => $userinfo{id}, group_id => $group,
+		username => $userinfo{name}, entry =>
+		"[new $zone] Added SOA record [contact $tmp1[0]] [master $tmp1[1]] ".
+		"[refresh $tmp2[0]] [retry $tmp2[1]] [expire $tmp2[2]] [minttl $tmp2[3]], TTL $ttl"));
+      } else {
+	my $logentry = "[new $zone] Added record '$host $typemap{$type}";
+#	$logentry .= " [distance $dist]" if $typemap{$type} eq 'MX';
+#	$logentry .= " [priority $dist] [weight $weight] [port $port]" if $typemap{$type} eq 'SRV';
+	_log($dbh, (rdns_id => $rdns_id, user_id => $userinfo{id}, group_id => $group,
+		username => $userinfo{name}, entry =>
+		$logentry." $val', TTL $ttl"));
+      }
+    }
+
+    # once we get here, we should have suceeded.
+    $dbh->commit;
+  }; # end eval
+
+  if ($@) {
+    my $msg = $@;
+    eval { $dbh->rollback; };
+    return ('FAIL',$msg);
+  } else {
+    return ('OK',$rdns_id);
+  }
+
+} # end addRDNS()
+
+
+## DNSDB::getZoneCount
+# Get count of zones in group or groups
+# Takes a database handle and hash containing:
+#  - the "current" group
+#  - an array of "acceptable" groups
+#  - a flag for forward/reverse zones
+#  - Optionally accept a "starts with" and/or "contains" filter argument
+# Returns an integer count of the resulting zone list.
+sub getZoneCount {
+  my $dbh = shift;
+
+  my %args = @_;
+
+  my @filterargs;
+  $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/;
+  push @filterargs, "^$args{startwith}" if $args{startwith};
+  $args{filter} =~ s/\./\[\.\]/g if $args{filter};	# only match literal dots, usually in reverse zones
+  push @filterargs, $args{filter} if $args{filter};
+
+  my $sql;
+  # Not as compact, and fix-me-twice if the common bits get wrong, but much easier to read
+  if ($args{revrec} eq 'n') {
+    $sql = "SELECT count(*) FROM domains".
+	" WHERE group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")".
+	($args{startwith} ? " AND domain ~* ?" : '').
+	($args{filter} ? " AND domain ~* ?" : '');
+  } else {
+    $sql = "SELECT count(*) FROM revzones".
+	" WHERE group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")".
+	($args{startwith} ? " AND CAST(revnet AS VARCHAR) ~* ?" : '').
+	($args{filter} ? " AND CAST(revnet AS VARCHAR) ~* ?" : '');
+  }
+  my ($count) = $dbh->selectrow_array($sql, undef, @filterargs);
+  return $count;
+} # end getZoneCount()
+
+
+## DNSDB::getZoneList()
+# Get a list of zones in the specified group(s)
+# Takes the same arguments as getZoneCount() above
+# Returns a reference to an array of hashrefs suitable for feeding to HTML::Template
+sub getZoneList {
+  my $dbh = shift;
+
+  my %args = @_;
+
+  my @zonelist;
+
+  $args{sortorder} = 'ASC' if !grep $args{sortorder}, ('ASC','DESC');
+  $args{offset} = 0 if !$args{offset} || $args{offset} !~ /^(?:all|\d+)$/;
+
+  my @filterargs;
+  $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/;
+  push @filterargs, "^$args{startwith}" if $args{startwith};
+  $args{filter} =~ s/\./\[\.\]/g if $args{filter};	# only match literal dots, usually in reverse zones
+  push @filterargs, $args{filter} if $args{filter};
+
+  my $sql;
+  # Not as compact, and fix-me-twice if the common bits get wrong, but much easier to read
+  if ($args{revrec} eq 'n') {
+    $args{sortby} = 'domain' if !grep $args{sortby}, ('revnet','group','status');
+    $sql = "SELECT domain_id,domain,status,groups.group_name AS group FROM domains".
+	" INNER JOIN groups ON domains.group_id=groups.group_id".
+	" WHERE domains.group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")".
+	($args{startwith} ? " AND domain ~* ?" : '').
+	($args{filter} ? " AND domain ~* ?" : '');
+  } else {
+##fixme:  arguably startwith here is irrelevant.  depends on the UI though.
+    $args{sortby} = 'revnet' if !grep $args{sortby}, ('domain','group','status');
+    $sql = "SELECT rdns_id,revnet,status,groups.group_name AS group FROM revzones".
+	" INNER JOIN groups ON revzones.group_id=groups.group_id".
+	" WHERE revzones.group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")".
+	($args{startwith} ? " AND CAST(revnet AS VARCHAR) ~* ?" : '').
+	($args{filter} ? " AND CAST(revnet AS VARCHAR) ~* ?" : '');
+  }
+  # A common tail.
+  $sql .= " ORDER BY ".($args{sortby} eq 'group' ? 'groups.group_name' : $args{sortby})." $args{sortorder} ".
+	($args{offset} eq 'all' ? '' : " LIMIT $config{perpage}".
+	" OFFSET ".$args{offset}*$config{perpage});
+  my $sth = $dbh->prepare($sql);
+  $sth->execute(@filterargs);
+  my $rownum = 0;
+
+  while (my @data = $sth->fetchrow_array) {
+    my %row;
+    $row{domainid} = $data[0];
+    $row{domain} = $data[1];
+    $row{status} = $data[2];
+    $row{group} = $data[3];
+    push @zonelist, \%row;
+  }
+
+  return \@zonelist;
+} # end getZoneList()
 
 
@@ -762,7 +1448,5 @@
     $sth->execute($pargroup,$groupname);
 
-    $sth = $dbh->prepare("SELECT group_id FROM groups WHERE group_name=?");
-    $sth->execute($groupname);
-    my ($groupid) = $sth->fetchrow_array();
+    my ($groupid) = $dbh->selectrow_array("SELECT group_id FROM groups WHERE group_name=?", undef, ($groupname));
 
 # Permissions
@@ -789,6 +1473,8 @@
 
 # Default records
-    $sth = $dbh->prepare("INSERT INTO default_records (group_id,host,type,val,distance,weight,port,ttl) ".
+    my $sthf = $dbh->prepare("INSERT INTO default_records (group_id,host,type,val,distance,weight,port,ttl) ".
 	"VALUES ($groupid,?,?,?,?,?,?,?)");
+    my $sthr = $dbh->prepare("INSERT INTO default_rev_records (group_id,host,type,val,ttl) ".
+	"VALUES ($groupid,?,?,?,?)");
     if ($inherit) {
       # Duplicate records from parent.  Actually relying on inherited records feels
@@ -797,5 +1483,11 @@
       $sth2->execute($pargroup);
       while (my @clonedata = $sth2->fetchrow_array) {
-	$sth->execute(@clonedata);
+	$sthf->execute(@clonedata);
+      }
+      # And now the reverse records
+      $sth2 = $dbh->prepare("SELECT group_id,host,type,val,ttl FROM default_rev_records WHERE group_id=?");
+      $sth2->execute($pargroup);
+      while (my @clonedata = $sth2->fetchrow_array) {
+	$sthr->execute(@clonedata);
       }
     } else {
@@ -803,10 +1495,13 @@
       # reasonable basic defaults for SOA, MX, NS, and minimal hosting
       # could load from a config file, but somewhere along the line we need hardcoded bits.
-      $sth->execute('ns1.example.com:hostmaster.example.com', 6, '10800:3600:604800:10800', 0, 0, 0, 86400);
-      $sth->execute('DOMAIN', 1, '192.168.4.2', 0, 0, 0, 7200);
-      $sth->execute('DOMAIN', 15, 'mx.example.com', 10, 0, 0, 7200);
-      $sth->execute('DOMAIN', 2, 'ns1.example.com', 0, 0, 0, 7200);
-      $sth->execute('DOMAIN', 2, 'ns2.example.com', 0, 0, 0, 7200);
-      $sth->execute('www.DOMAIN', 5, 'DOMAIN', 0, 0, 0, 7200);
+      $sthf->execute('ns1.example.com:hostmaster.example.com', 6, '10800:3600:604800:10800', 0, 0, 0, 86400);
+      $sthf->execute('DOMAIN', 1, '192.168.4.2', 0, 0, 0, 7200);
+      $sthf->execute('DOMAIN', 15, 'mx.example.com', 10, 0, 0, 7200);
+      $sthf->execute('DOMAIN', 2, 'ns1.example.com', 0, 0, 0, 7200);
+      $sthf->execute('DOMAIN', 2, 'ns2.example.com', 0, 0, 0, 7200);
+      $sthf->execute('www.DOMAIN', 5, 'DOMAIN', 0, 0, 0, 7200);
+      # reasonable basic defaults for generic reverse zone.  Same as initial SQL tabledef.
+      $sthr->execute('hostmaster.ADMINDOMAIN:ns1.ADMINDOMAIN', 6, '10800:3600:604800:10800', 86400);
+      $sthr->execute('unused-%r.ADMINDOMAIN', 65283, 'ZONE', 3600);
     }
 
@@ -1222,25 +1917,24 @@
 ## DNSDB::getSOA()
 # Return all suitable fields from an SOA record in separate elements of a hash
-# Takes a database handle, default/live flag, and group (default) or domain (live) ID
+# Takes a database handle, default/live flag, domain/reverse flag, and parent ID
 sub getSOA {
   $errstr = '';
   my $dbh = shift;
   my $def = shift;
+  my $rev = shift;
   my $id = shift;
   my %ret;
 
-  # (ab)use distance and weight columns to store SOA data
-
-  my $sql = "SELECT record_id,host,val,ttl,distance from";
-  if ($def eq 'def' or $def eq 'y') {
-    $sql .= " default_records WHERE group_id=? AND type=$reverse_typemap{SOA}";
-  } else {
-    # we're editing a live SOA record;  find based on domain
-    $sql .= " records WHERE domain_id=? AND type=$reverse_typemap{SOA}";
-  }
+  # (ab)use distance and weight columns to store SOA data?  can't for default_rev_records...
+  # - should really attach serial to the zone parent somewhere
+
+  my $sql = "SELECT record_id,host,val,ttl from "._rectable($def,$rev).
+	" WHERE "._recparent($def,$rev)." = ? AND type=$reverse_typemap{SOA}";
+
   my $sth = $dbh->prepare($sql);
   $sth->execute($id);
-
-  my ($recid,$host,$val,$ttl,$serial) = $sth->fetchrow_array() or return;
+##fixme:  stick a flag somewhere if the record doesn't exist.  by the API, this is an impossible case, but...
+
+  my ($recid,$host,$val,$ttl) = $sth->fetchrow_array() or return;
   my ($contact,$prins) = split /:/, $host;
   my ($refresh,$retry,$expire,$minttl) = split /:/, $val;
@@ -1248,5 +1942,5 @@
   $ret{recid}	= $recid;
   $ret{ttl}	= $ttl;
-  $ret{serial}	= $serial;
+#  $ret{serial}	= $serial;	# ca't use distance for serial with default_rev_records
   $ret{prins}	= $prins;
   $ret{contact}	= $contact;
@@ -1260,16 +1954,35 @@
 
 
+## DNSDB::updateSOA()
+# Update the specified SOA record
+# Takes a database handle, default/live flag, forward/reverse flag, and SOA data hash
+sub updateSOA {
+  my $dbh = shift;
+  my $defrec = shift;
+  my $revrec = shift;
+
+  my %soa = @_;
+
+##fixme: data validation: make sure {recid} is really the SOA for {parent}
+  my $sql = "UPDATE "._rectable($defrec, $revrec)." SET host=?, val=?, ttl=? WHERE record_id=? AND type=6";
+  $dbh->do($sql, undef, ("$soa{contact}:$soa{prins}", "$soa{refresh}:$soa{retry}:$soa{expire}:$soa{minttl}",
+        $soa{ttl}, $soa{recid}));
+
+} # end updateSOA()
+
+
 ## DNSDB::getRecLine()
 # Return all data fields for a zone record in separate elements of a hash
-# Takes a database handle, default/live flag, and record ID
+# Takes a database handle, default/live flag, forward/reverse flag, and record ID
 sub getRecLine {
   $errstr = '';
   my $dbh = shift;
-  my $def = shift;
+  my $defrec = shift;
+  my $revrec = shift;
   my $id = shift;
 
-  my $sql = "SELECT record_id,host,type,val,distance,weight,port,ttl".
-	(($def eq 'def' or $def eq 'y') ? ',group_id FROM default_' : ',domain_id FROM ').
-	"records WHERE record_id=?";
+  my $sql = "SELECT record_id,host,type,val,ttl".($revrec eq 'n' ? ',distance,weight,port' : '').
+	(($defrec eq 'y') ? ',group_id FROM ' : ',domain_id,rdns_id FROM ').
+	_rectable($defrec,$revrec)." WHERE record_id=?";
   my $ret = $dbh->selectrow_hashref($sql, undef, ($id) );
 
@@ -1284,5 +1997,12 @@
   }
 
-  $ret->{parid} = (($def eq 'def' or $def eq 'y') ? $ret->{group_id} : $ret->{domain_id});
+  # explicitly set a parent id
+  if ($defrec eq 'y') {
+    $ret->{parid} = $ret->{group_id};
+  } else {
+    $ret->{parid} = (($revrec eq 'n') ? $ret->{domain_id} : $ret->{rdns_id});
+    # and a secondary if we have a custom type that lives in both a forward and reverse zone
+    $ret->{secid} = (($revrec eq 'y') ? $ret->{domain_id} : $ret->{rdns_id}) if $ret->{type} > 65279;
+  }
 
   return $ret;
@@ -1299,5 +2019,6 @@
   $errstr = '';
   my $dbh = shift;
-  my $type = shift;
+  my $def = shift;
+  my $rev = shift;
   my $id = shift;
   my $nrecs = shift || 'all';
@@ -1310,9 +2031,7 @@
   my $filter = shift || '';
 
-  $type = 'y' if $type eq 'def';
-
-  my $sql = "SELECT r.record_id,r.host,r.type,r.val,r.distance,r.weight,r.port,r.ttl FROM ";
-  $sql .= "default_" if $type eq 'y';
-  $sql .= "records r ";
+  my $sql = "SELECT r.record_id,r.host,r.type,r.val,r.ttl";
+  $sql .= ",r.distance,r.weight,r.port" if $rev eq 'n';
+  $sql .= " FROM "._rectable($def,$rev)." r ";
 
   # whee!  multisort means just passing comma-separated fields in sortby!
@@ -1326,17 +2045,19 @@
 
   $sql .= "INNER JOIN rectypes t ON r.type=t.val ";	# for sorting by type alphabetically
-  if ($type eq 'y') {
-    $sql .= "WHERE r.group_id=?";
-  } else {
-    $sql .= "WHERE r.domain_id=?";
-  }
+  $sql .= "WHERE "._recparent($def,$rev)." = ?";
   $sql .= " AND NOT r.type=$reverse_typemap{SOA}";
   $sql .= " AND host ~* ?" if $filter;
   # use alphaorder column for "correct" ordering of sort-by-type instead of DNS RR type number
   $sql .= " ORDER BY $newsort $direction";
-  $sql .= " LIMIT $nrecs OFFSET ".($nstart*$nrecs) if $nstart ne 'all';
 
   my @bindvars = ($id);
   push @bindvars, $filter if $filter;
+
+  # just to be ultraparanoid about SQL injection vectors
+  if ($nstart ne 'all') {
+    $sql .= " LIMIT ? OFFSET ?";
+    push @bindvars, $nrecs;
+    push @bindvars, ($nstart*$nrecs);
+  }
   my $sth = $dbh->prepare($sql) or warn $dbh->errstr;
   $sth->execute(@bindvars) or warn "$sql: ".$sth->errstr;
@@ -1353,10 +2074,12 @@
 
 ## DNSDB::getRecCount()
-# Return count of non-SOA records in domain (or default records in a group)
-# Takes a database handle, default/live flag, group/domain ID, and optional filtering modifier
+# Return count of non-SOA records in zone (or default records in a group)
+# Takes a database handle, default/live flag, reverse/forward flag, group/domain ID,
+# and optional filtering modifier
 # Returns the count
 sub getRecCount {
   my $dbh = shift;
   my $defrec = shift;
+  my $revrec = shift;
   my $id = shift;
   my $filter = shift || '';
@@ -1368,10 +2091,10 @@
   my @bindvars = ($id);
   push @bindvars, $filter if $filter;
-  my ($count) = $dbh->selectrow_array("SELECT count(*) FROM ".
-        ($defrec eq 'y' ? 'default_' : '')."records ".
-        "WHERE ".($defrec eq 'y' ? 'group' : 'domain')."_id=? ".
-        "AND NOT type=$reverse_typemap{SOA}".
-	($filter ? " AND host ~* ?" : ''),
-	undef, (@bindvars) );
+  my $sql = "SELECT count(*) FROM ".
+	_rectable($defrec,$revrec).
+	" WHERE "._recparent($defrec,$revrec)."=? ".
+	"AND NOT type=$reverse_typemap{SOA}".
+	($filter ? " AND host ~* ?" : '');
+  my ($count) = $dbh->selectrow_array($sql, undef, (@bindvars) );
 
   return $count;
@@ -1387,12 +2110,15 @@
 # and weight/port for SRV
 # Returns a status code and detail message in case of error
+##fixme:  pass a hash with the record data, not a series of separate values
 sub addRec {
   $errstr = '';
   my $dbh = shift;
   my $defrec = shift;
-  my $id = shift;
+  my $revrec = shift;
+  my $id = shift;	# parent (group_id for defrecs, rdns_id for reverse records,
+			# domain_id for domain records)
 
   my $host = shift;
-  my $rectype = shift;
+  my $rectype = shift;	# reference so we can coerce it if "+"-types can't find both zones
   my $val = shift;
   my $ttl = shift;
@@ -1418,38 +2144,39 @@
   }
 
+  my $domid = 0;
+  my $revid = 0;
+
+  my $retcode = 'OK';	# assume everything will go OK
+  my $retmsg = '';
+
+  # do simple validation first
   return ('FAIL', "TTL must be numeric") unless $ttl =~ /^\d+$/;
 
-  my $fields = ($defrec eq 'y' ? 'group_id' : 'domain_id').",host,type,val,ttl";
-  my $vallen = "?,?,?,?,?";
-  my @vallist = ($id,$host,$rectype,$val,$ttl);
-
-  my $dist;
-  if ($rectype == $reverse_typemap{MX} or $rectype == $reverse_typemap{SRV}) {
-    $dist = shift;
-    return ('FAIL',"Distance is required for $typemap{$rectype} records") unless defined($dist);
-    $dist =~ s/\s*//g;
-    return ('FAIL',"Distance is required, and must be numeric") unless $dist =~ /^\d+$/;
-    $fields .= ",distance";
-    $vallen .= ",?";
-    push @vallist, $dist;
-  }
-  my $weight;
-  my $port;
-  if ($rectype == $reverse_typemap{SRV}) {
-    # check for _service._protocol.  NB:  RFC2782 does not say "MUST"...  nor "SHOULD"...
-    # it just says (paraphrased) "... is prepended with _ to prevent DNS collisions"
-    return ('FAIL',"SRV records must begin with _service._protocol [$host]")
-	unless $host =~ /^_[A-Za-z]+\._[A-Za-z]+\.[a-zA-Z0-9-]+/;
-    $weight = shift;
-    $port = shift;
-    return ('FAIL',"Port and weight are required for SRV records") unless defined($weight) && defined($port);
-    $weight =~ s/\s*//g;
-    $port =~ s/\s*//g;
-    return ('FAIL',"Port and weight are required, and must be numeric")
-	unless $weight =~ /^\d+$/ && $port =~ /^\d+$/;
-    $fields .= ",weight,port";
-    $vallen .= ",?,?";
-    push @vallist, ($weight,$port);
-  }
+  # Quick check on hostname parts.  Note the regex is more forgiving than the error message;
+  # domain names technically are case-insensitive, and we use printf-like % codes for a couple
+  # of types.  Other things may also be added to validate default records of several flavours.
+  return ('FAIL', "Hostnames may not contain anything other than (0-9 a-z . _)")
+	if $defrec eq 'n' && $$host !~ /^[0-9a-z_%.]+$/i;
+
+  # Collect these even if we're only doing a simple A record so we can call *any* validation sub
+  my $dist = shift;
+  my $port = shift;
+  my $weight = shift;
+
+  my $fields;
+  my @vallist;
+
+  # Call the validation sub for the type requested.
+  ($retcode,$retmsg) = $validators{$$rectype}($dbh, (defrec => $defrec, revrec => $revrec, id => $id,
+	host => $host, rectype => $rectype, val => $val, addr => $addr,
+	dist => \$dist, port => \$port, weight => \$weight,
+	fields => \$fields, vallist => \@vallist) );
+
+  return ($retcode,$retmsg) if $retcode eq 'FAIL';
+
+  # Set up database fields and bind parameters
+  $fields .= "host,type,val,ttl,"._recparent($defrec,$revrec);
+  push @vallist, ($$host,$$rectype,$$val,$ttl,$id);
+  my $vallen = '?'.(',?'x$#vallist);
 
   # Allow transactions, and raise an exception on errors so we can catch it later.
@@ -1459,5 +2186,5 @@
 
   eval {
-    $dbh->do("INSERT INTO ".($defrec eq 'y' ? 'default_' : '')."records ($fields) VALUES ($vallen)",
+    $dbh->do("INSERT INTO "._rectable($defrec, $revrec)." ($fields) VALUES ($vallen)",
 	undef, @vallist);
     $dbh->commit;
@@ -1469,5 +2196,5 @@
   }
 
-  return ('OK','OK');
+  return ($retcode, $retmsg);
 
 } # end addRec()
@@ -1565,7 +2292,8 @@
   my $dbh = shift;
   my $defrec = shift;
+  my $revrec = shift;
   my $id = shift;
 
-  my $sth = $dbh->prepare("DELETE FROM ".($defrec eq 'y' ? 'default_' : '')."records WHERE record_id=?");
+  my $sth = $dbh->prepare("DELETE FROM "._rectable($defrec,$revrec)." WHERE record_id=?");
   $sth->execute($id);
 
@@ -1577,57 +2305,131 @@
 
   # Reference hashes.
-  my %par_tbl = (
+my %par_tbl = (
 		group	=> 'groups',
 		user	=> 'users',
 		defrec	=> 'default_records',
+		defrevrec	=> 'default_rev_records',
 		domain	=> 'domains',
+		revzone	=> 'revzones',
 		record	=> 'records'
 	);
-  my %id_col = (
+my %id_col = (
 		group	=> 'group_id',
 		user	=> 'user_id',
 		defrec	=> 'record_id',
+		defrevrec	=> 'record_id',
 		domain	=> 'domain_id',
+		revzone	=> 'rdns_id',
 		record	=> 'record_id'
 	);
-  my %par_col = (
+my %par_col = (
 		group	=> 'parent_group_id',
 		user	=> 'group_id',
 		defrec	=> 'group_id',
+		defrevrec	=> 'group_id',
 		domain	=> 'group_id',
+		revzone	=> 'group_id',
 		record	=> 'domain_id'
 	);
-  my %par_type = (
+my %par_type = (
 		group	=> 'group',
 		user	=> 'group',
 		defrec	=> 'group',
+		defrevrec	=> 'group',
 		domain	=> 'group',
+		revzone	=> 'group',
 		record	=> 'domain'
 	);
 
-## DNSDB::getParents()
-# Find out which entities are parent to the requested id
-# Returns arrayref containing hash pairs of id/type
-sub getParents {
-  my $dbh = shift;
-  my $id = shift;
-  my $type = shift;
-  my $depth = shift || 'all';	# valid values:  'all', 'immed', <int> (stop at this group ID)
-
-  my @parlist;
-
-  while (1) {
-    my $result = $dbh->selectrow_hashref("SELECT $par_col{$type} FROM $par_tbl{$type} WHERE $id_col{$type} = ?",
-	undef, ($id) );
-    my %tmp = ($result->{$par_col{$type}} => $par_type{$type});
-    unshift @parlist, \%tmp;
-    last if $result->{$par_col{$type}} == 1;	# group 1 is its own parent
-    $id = $result->{$par_col{$type}};
-    $type = $par_type{$type};
-  }
-
-  return \@parlist;
-
-} # end getParents()
+
+## DNSDB::getTypelist()
+# Get a list of record types for various UI dropdowns
+# Takes database handle, forward/reverse/lookup flag, and optional "tag as selected" indicator (defaults to A)
+# Returns an arrayref to list of hashrefs perfect for HTML::Template
+sub getTypelist {
+  my $dbh = shift;
+  my $recgroup = shift;
+  my $type = shift || $reverse_typemap{A};
+
+  # also accepting $webvar{revrec}!
+  $recgroup = 'f' if $recgroup eq 'n';
+  $recgroup = 'r' if $recgroup eq 'y';
+
+  my $sql = "SELECT val,name FROM rectypes WHERE ";
+  if ($recgroup eq 'r') {
+    # reverse zone types
+    $sql .= "stdflag=2 OR stdflag=3";
+  } elsif ($recgroup eq 'l') {
+    # DNS lookup types.  Note we avoid our custom types >= 65280, since those are entirely internal.
+    $sql .= "(stdflag=1 OR stdflag=2 OR stdflag=3) AND val < 65280";
+  } else {
+    # default;  forward zone types.  technically $type eq 'f' but not worth the error message.
+    $sql .= "stdflag=1 OR stdflag=2";
+  }
+  $sql .= " ORDER BY listorder";
+
+  my $sth = $dbh->prepare($sql);
+  $sth->execute;
+  my @typelist;
+  while (my ($rval,$rname) = $sth->fetchrow_array()) {
+    my %row = ( recval => $rval, recname => $rname );
+    $row{tselect} = 1 if $rval == $type;
+    push @typelist, \%row;
+  }
+
+  # Add SOA on lookups since it's not listed in other dropdowns.
+  if ($recgroup eq 'l') {
+    my %row = ( recval => $reverse_typemap{SOA}, recname => 'SOA' );
+    $row{tselect} = 1 if $reverse_typemap{SOA} == $type;
+    push @typelist, \%row;
+  }
+
+  return \@typelist;
+} # end getTypelist()
+
+
+## DNSDB::parentID()
+# Get ID of entity that is nearest parent to requested id
+# Takes a database handle and a hash of entity ID, entity type, optional parent type flag
+# (domain/reverse zone or group), and optional default/live and forward/reverse flags
+# Returns the ID or undef on failure
+sub parentID {
+  my $dbh = shift;
+
+  my %args = @_;
+
+  # clean up the parent-type.  Set it to group if not set;  coerce revzone to domain for simpler logic
+  $args{partype} = 'group' if !$args{partype};
+  $args{partype} = 'domain' if $args{partype} eq 'revzone';
+
+  # clean up defrec and revrec.  default to live record, forward zone
+  $args{defrec} = 'n' if !$args{defrec};
+  $args{revrec} = 'n' if !$args{revrec};
+
+  if ($par_type{$args{partype}} eq 'domain') {
+    # only live records can have a domain/zone parent
+    return unless ($args{type} eq 'record' && $args{defrec} eq 'n');
+    my $result = $dbh->selectrow_hashref("SELECT ".($args{revrec} eq 'n' ? 'domain_id' : 'rdns_id').
+	" FROM records WHERE record_id = ?",
+	undef, ($args{id}) ) or return;
+    return $result;
+  } else {
+    # snag some arguments that will either fall through or be overwritten to save some code duplication
+    my $tmpid = $args{id};
+    my $type = $args{type};
+    if ($type eq 'record' && $args{defrec} eq 'n') {
+      # Live records go through the records table first.
+      ($tmpid) = $dbh->selectrow_array("SELECT ".($args{revrec} eq 'n' ? 'domain_id' : 'rdns_id').
+	" FROM records WHERE record_id = ?",
+	undef, ($args{id}) ) or return;
+      $type = ($args{revrec} eq 'n' ? 'domain' : 'revzone');
+    }
+    my ($result) = $dbh->selectrow_array("SELECT $par_col{$type} FROM $par_tbl{$type} WHERE $id_col{$type} = ?",
+	undef, ($tmpid) );
+    return $result;
+  }
+# should be impossible to get here with even remotely sane arguments
+  return;
+} # end parentID()
 
 
@@ -1643,17 +2445,18 @@
 
   # Return false on invalid types
-  return 0 if !grep /^$type1$/, ('record','defrec','user','domain','group');
-  return 0 if !grep /^$type2$/, ('record','defrec','user','domain','group');
+  return 0 if !grep /^$type1$/, ('record','defrec','defrevrec','user','domain','revzone','group');
+  return 0 if !grep /^$type2$/, ('record','defrec','defrevrec','user','domain','revzone','group');
 
   # Return false on impossible relations
   return 0 if $type1 eq 'record';	# nothing may be a child of a record
   return 0 if $type1 eq 'defrec';	# nothing may be a child of a record
+  return 0 if $type1 eq 'defrevrec';	# nothing may be a child of a record
   return 0 if $type1 eq 'user';		# nothing may be child of a user
   return 0 if $type1 eq 'domain' && $type2 ne 'record';	# domain may not be a parent of anything other than a record
+  return 0 if $type1 eq 'revzone' && $type2 ne 'record';# reverse zone may not be a parent of anything other than a record
 
   # ennnhhhh....  if we're passed an id of 0, it will never be found.  usual
   # case would be the UI creating a new <thing>, and so we don't have an ID for
   # <thing> to look up yet.  in that case the UI should check the parent as well.
-  # argument for returning 1 is
   return 0 if $id1 == 0;	# nothing can have a parent id of 0
   return 1 if $id2 == 0;	# anything could have a child id of 0 (or "unknown")
@@ -1665,9 +2468,24 @@
   return 1 if $type1 eq 'group' && $type2 eq 'group' && $id1 == $id2;
 
-# almost the same loop as getParents() above
   my $id = $id2;
   my $type = $type2;
   my $foundparent = 0;
 
+  # Records are the only entity with two possible parents.  We need to split the parent checks on
+  # domain/rdns.
+  if ($type eq 'record') {
+    my ($dom,$rdns) = $dbh->selectrow_array("SELECT domain_id,rdns_id FROM records WHERE record_id=?",
+	undef, ($id));
+    # check immediate parent against request
+    return 1 if $type1 eq 'domain' && $id1 == $dom;
+    return 1 if $type1 eq 'revzone' && $id1 == $rdns;
+    # if request is group, check *both* parents.  Only check if the parent is nonzero though.
+    return 1 if $dom && isParent($dbh, $id1, $type1, $dom, 'domain');
+    return 1 if $rdns && isParent($dbh, $id1, $type1, $rdns, 'revzone');
+    # exit here since we've executed the loop below by proxy in the above recursive calls.
+    return 0;
+  }
+
+# almost the same loop as getParents() above
   my $limiter = 0;
   while (1) {
@@ -1677,5 +2495,5 @@
     if (!$result) {
       $limiter++;
-##fixme:  how often will this happen on a live site?
+##fixme:  how often will this happen on a live site?  fail at max limiter <n>?
       warn "no results looking for $sql with id $id (depth $limiter)\n";
       last;
@@ -1686,4 +2504,5 @@
     } else {
 ##fixme: do we care about trying to return a "no such record/domain/user/group" error?
+# should be impossible to create an inconsistent DB just with API calls.
       warn $dbh->errstr." $sql, $id" if $dbh->errstr;
     }
Index: branches/stable/dns.cgi
===================================================================
--- branches/stable/dns.cgi	(revision 535)
+++ branches/stable/dns.cgi	(revision 544)
@@ -62,5 +62,6 @@
 
 # shut up some warnings, in case we arrive somewhere we forgot to set this
-$webvar{defrec} = 'n' if !$webvar{defrec};
+$webvar{defrec} = 'n' if !$webvar{defrec};	# non-default records
+$webvar{revrec} = 'n' if !$webvar{revrec};	# non-reverse (domain) records
 
 # load some local system defaults (mainly DB connect info)
@@ -87,4 +88,6 @@
   $session->param('domlistsortby','domain');
   $session->param('domlistorder','ASC');
+  $session->param('revzonessortby','revnet');
+  $session->param('revzonesorder','ASC');
   $session->param('useradminsortby','user');
   $session->param('useradminorder','ASC');
@@ -204,10 +207,17 @@
 my $page;
 eval {
-  $page = HTML::Template->new(filename => "$templatedir/$webvar{page}.tmpl");
+  # sigh.  can't set loop_context_vars or global_vars once instantiated.
+  $page = HTML::Template->new(filename => "$templatedir/$webvar{page}.tmpl",
+	loop_context_vars => 1, global_vars => 1);
 };
 if ($@) {
-  warn "Bad page $webvar{page} requested";
+  my $msg = $@;
   $page = HTML::Template->new(filename => "$templatedir/badpage.tmpl");
-  $page->param(badpage => $q->escapeHTML($webvar{page}));
+  if (-e "$templatedir/$webvar{page}.tmpl") {
+    $page->param(badtemplate => $q->escapeHTML($msg));
+  } else {
+    warn "Bad page $webvar{page} requested";
+    $page->param(badpage => $q->escapeHTML($webvar{page}));
+  }
   $webvar{page} = 'badpage';
 }
@@ -279,6 +289,7 @@
 
     # I hate special cases.
+##fixme: probably need to handle webvar{revrec}=='y' too
     if ($webvar{page} eq 'reclist' && $webvar{defrec} eq 'y') {
-      my %args = (page => $webvar{page}, id => $curgroup, defrec => $webvar{defrec});
+      my %args = (page => $webvar{page}, id => $curgroup, defrec => $webvar{defrec}, revrec => $webvar{revrec});
       $args{errmsg} = $errmsg if $errmsg;
       changepage(%args);
@@ -300,4 +311,6 @@
 
 } elsif ($webvar{page} eq 'domlist' or $webvar{page} eq 'index') {
+
+  $page->param(domlist => 1);
 
 # hmm.  seeing problems in some possibly-not-so-corner cases.
@@ -312,5 +325,6 @@
       my $stat = domStatus($dbh,$webvar{id},$webvar{domstatus});
 ##fixme  switch to more consise "Enabled <domain"/"Disabled <domain>" as with users?
-      logaction($webvar{id}, $session->param("username"), parentID($webvar{id}, 'dom', 'group'),
+      logaction($webvar{id}, $session->param("username"),
+	parentID($dbh, (id => $webvar{id}, type => 'domain', revrec => $webvar{revrec})),
 	"Changed ".domainName($dbh, $webvar{id})." state to ".($stat ? 'active' : 'inactive'));
       $page->param(resultmsg => "Changed ".domainName($dbh, $webvar{id})." state to ".
@@ -366,5 +380,5 @@
 
   my ($code,$msg) = addDomain($dbh,$webvar{domain},$webvar{group},($webvar{makeactive} eq 'on' ? 1 : 0),
-	(name => $session->param("username"), id => $session->param("uid")));
+	(username => $session->param("username"), id => $session->param("uid")));
 
   if ($code eq 'OK') {
@@ -401,5 +415,5 @@
 
   } elsif ($webvar{del} eq 'ok') {
-    my $pargroup = parentID($webvar{id}, 'dom', 'group');
+    my $pargroup = parentID($dbh, (id => $webvar{id}, type => 'domain', revrec => $webvar{revrec}));
     my $dom = domainName($dbh, $webvar{id});
     my ($code,$msg) = delDomain($dbh, $webvar{id});
@@ -418,10 +432,59 @@
   }
 
+} elsif ($webvar{page} eq 'revzones') {
+
+  $webvar{revrec} = 'y';
+  $page->param(curpage => $webvar{page});
+  listzones();
+
+} elsif ($webvar{page} eq 'newrevzone') {
+
+## scope/access check - use domain settings?  invent new (bleh)
+  changepage(page => "revzones", errmsg => "You are not permitted to add reverse zones")
+       unless ($permissions{admin} || $permissions{domain_create});
+
+  fill_grouplist("grouplist");
+
+  if ($webvar{add_failed}) {
+    $page->param(add_failed => 1);
+    $page->param(errmsg => $webvar{errmsg});
+    $page->param(revzone => $webvar{revzone});
+    $page->param(revpatt => $webvar{revpatt});
+  }
+
+} elsif ($webvar{page} eq 'addrevzone') {
+
+  changepage(page => "revzones", errmsg => "You are not permitted to add reverse zones")
+       unless ($permissions{admin} || $permissions{domain_create});
+
+  # security check - does the user have permission to access this entity?
+  if (!check_scope(id => $webvar{group}, type => 'group')) {
+    changepage(page => "newrevzone", add_failed => 1, revzone => $webvar{revzone}, revpatt => $webvar{revpatt},
+       errmsg => "You do not have permission to add a reverse zone to the requested group");
+  }
+
+  my ($code,$msg) = addRDNS($dbh, $webvar{revzone}, $webvar{revpatt}, $webvar{group},
+	($webvar{makeactive} eq 'on' ? 1 : 0),
+	(username => $session->param("username"), id => $session->param("uid")) );
+
+  if ($code eq 'OK') {
+    logaction(0, $session->param("username"), $webvar{group}, "Added reverse zone $webvar{revzone}", $msg);
+    changepage(page => "reclist", id => $msg, revrec => 'y');
+  } else {
+    logaction(0, $session->param("username"), $webvar{group}, "Failed adding reverse zone $webvar{revzone} ($msg)");
+    changepage(page => "newrevzone", add_failed => 1, revzone => $webvar{revzone}, revpatt => $webvar{revpatt},
+       errmsg => $msg);
+  }
+
+#} elsif ($webvar{page} eq 'delrevzone') {
+
 } elsif ($webvar{page} eq 'reclist') {
 
   # security check - does the user have permission to view this entity?
-  if (!check_scope(id => $webvar{id}, type => ($webvar{defrec} eq 'y' ? 'group' : 'domain'))) {
+  if (!check_scope(id => $webvar{id}, type =>
+	($webvar{defrec} eq 'y' ? 'group' : ($webvar{revrec} eq 'y' ? 'revzone' : 'domain')))) {
     $page->param(errmsg => "You are not permitted to view or change the requested ".
-	($webvar{defrec} eq 'y' ? "group's default records" : "domain's records"));
+	($webvar{defrec} eq 'y' ? "group's default records" :
+		($webvar{revrec} eq 'y' ? "reverse zone's records" : "domain's records")));
     $page->param(perm_err => 1);	# this causes the template to skip the record listing output.
     goto DONERECLIST;	# and now we skip filling in the content which is not printed due to perm_err above
@@ -446,8 +509,9 @@
 
     $page->param(defrec => $webvar{defrec});
+    $page->param(revrec => $webvar{revrec});
     $page->param(id => $webvar{id});
     $page->param(curpage => $webvar{page});
 
-    my $count = getRecCount($dbh, $webvar{defrec}, $webvar{id}, $filter);
+    my $count = getRecCount($dbh, $webvar{defrec}, $webvar{revrec}, $webvar{id}, $filter);
 
     $sortby = 'host';
@@ -460,22 +524,35 @@
 
 # set up the headers
-    my @cols = ('host', 'type', 'val', 'distance', 'weight', 'port', 'ttl');
-    my %colheads = (host => 'Name', type => 'Type', val => 'Address',
+    my @cols;
+    my %colheads;
+    if ($webvar{revrec} eq 'n') {
+      @cols = ('host', 'type', 'val', 'distance', 'weight', 'port', 'ttl');
+      %colheads = (host => 'Name', type => 'Type', val => 'Address',
 	distance => 'Distance', weight => 'Weight', port => 'Port', ttl => 'TTL');
-    my %custom = (id => $webvar{id}, defrec => $webvar{defrec});
+    } else {
+      @cols = ('host', 'type', 'val', 'ttl');
+      %colheads = (host => 'IP Address', type => 'Type', val => 'Hostname', ttl => 'TTL');
+    }
+    my %custom = (id => $webvar{id}, defrec => $webvar{defrec}, revrec => $webvar{revrec});
     fill_colheads($sortby, $sortorder, \@cols, \%colheads, \%custom);
 
 # fill the page-count and first-previous-next-last-all details
     fill_pgcount($count,"records",
-	($webvar{defrec} eq 'y' ? "group ".groupName($dbh,$webvar{id}) : domainName($dbh,$webvar{id})));
+	($webvar{defrec} eq 'y' ? "group ".groupName($dbh,$webvar{id}) : 
+		($webvar{revrec} eq 'y' ? revName($dbh,$webvar{id}) : domainName($dbh,$webvar{id}))
+	));
     fill_fpnla($count);  # should put some params on this sub...
 
     $page->param(defrec => $webvar{defrec});
-    if ($webvar{defrec} eq 'y') {
-      showdomain('y',$curgroup);
-    } else {
-      showdomain('n',$webvar{id});
+    showzone($webvar{defrec}, $webvar{revrec}, $webvar{id});
+    if ($webvar{defrec} eq 'n') {
+#      showzone('n',$webvar{id});
 ##fixme:  permission for viewing logs?
-      $page->param(logdom => 1);
+##fixme:  determine which slice of the log we view (group, domain, revzone)
+      if ($webvar{revrec} eq 'n') {
+	$page->param(logdom => 1);
+      } else {
+	$page->param(logrdns => 1);
+      }
     }
 
@@ -484,4 +561,8 @@
       $session->clear('resultmsg');
     }
+    if ($session->param('warnmsg')) {
+      $page->param(warnmsg => $session->param('warnmsg'));
+      $session->clear('warnmsg');
+    }
     if ($session->param('errmsg')) {
       $page->param(errmsg => $session->param('errmsg'));
@@ -497,16 +578,22 @@
 
   # security check - does the user have permission to access this entity?
-  if (!check_scope(id => $webvar{id}, type => ($webvar{defrec} eq 'y' ? 'defrec' : 'record'))) {
+  if (!check_scope(id => $webvar{id}, type =>
+	($webvar{defrec} eq 'y' ? ($webvar{revrec} eq 'y' ? 'defrevrec' : 'defrec') : 'record'))) {
     $page->param(perm_err => "You are not permitted to edit the requested record");
     goto DONEREC;
   }
   # round 2, check the parent.
-  if (!check_scope(id => $webvar{parentid}, type => ($webvar{defrec} eq 'y' ? 'group' : 'domain'))) {
+  if (!check_scope(id => $webvar{parentid}, type =>
+	($webvar{defrec} eq 'y' ? 'group' : ($webvar{revrec} eq 'y' ? 'revzone' : 'domain')))) {
     my $msg = ($webvar{defrec} eq 'y' ?
 	"You are not permitted to add or edit default records in the requested group" :
-	"You are not permitted to add or edit records in the requested domain");
+	"You are not permitted to add or edit records in the requested domain/zone");
     $page->param(perm_err => $msg);
     goto DONEREC;
   }
+
+  $page->param(defrec => $webvar{defrec});
+  $page->param(revrec => $webvar{revrec});
+  $page->param(fwdzone => $webvar{revrec} eq 'n');
 
   if ($webvar{recact} eq 'new') {
@@ -518,5 +605,4 @@
     $page->param(recact => "add");
     $page->param(parentid => $webvar{parentid});
-    $page->param(defrec => $webvar{defrec});
 
     fill_recdata();
@@ -527,10 +613,6 @@
 	unless ($permissions{admin} || $permissions{record_create});
 
-##fixme: this should probably go in DNSDB::addRec(), need to ponder what to do about PTR and friends
-    # prevent out-of-domain records from getting added by appending the domain, or DOMAIN for default records
-    my $pname = ($webvar{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$webvar{parentid}));
-    $webvar{name} =~ s/\.*$/\.$pname/ if $webvar{name} !~ /$pname$/;
-
-    my @recargs = ($dbh,$webvar{defrec},$webvar{parentid},$webvar{name},$webvar{type},$webvar{address},$webvar{ttl});
+    my @recargs = ($dbh,$webvar{defrec},$webvar{revrec},$webvar{parentid},
+	\$webvar{name},\$webvar{type},\$webvar{address},$webvar{ttl});
     if ($webvar{type} == $reverse_typemap{MX} or $webvar{type} == $reverse_typemap{SRV}) {
       push @recargs, $webvar{distance};
@@ -543,7 +625,8 @@
     my ($code,$msg) = addRec(@recargs);
 
-    if ($code eq 'OK') {
+    if ($code eq 'OK' || $code eq 'WARN') {
+      my $restr;
       if ($webvar{defrec} eq 'y') {
-	my $restr = "Added default record '$webvar{name} $typemap{$webvar{type}}";
+	$restr = "Added default record '$webvar{name} $typemap{$webvar{type}}";
 	$restr .= " [distance $webvar{distance}]" if $typemap{$webvar{type}} eq 'MX';
 	$restr .= " [priority $webvar{distance}] [weight $webvar{weight}] [port $webvar{port}]"
@@ -551,14 +634,18 @@
 	$restr .= " $webvar{address}', TTL $webvar{ttl}";
 	logaction(0, $session->param("username"), $webvar{parentid}, $restr);
-	changepage(page => "reclist", id => $webvar{parentid}, defrec => $webvar{defrec}, resultmsg => $restr);
       } else {
-	my $restr = "Added record '$webvar{name} $typemap{$webvar{type}}";
+	$restr = "Added record '$webvar{name} $typemap{$webvar{type}}";
 	$restr .= " [distance $webvar{distance}]" if $typemap{$webvar{type}} eq 'MX';
 	$restr .= " [priority $webvar{distance}] [weight $webvar{weight}] [port $webvar{port}]"
 		if $typemap{$webvar{type}} eq 'SRV';
 	$restr .= " $webvar{address}', TTL $webvar{ttl}";
-	logaction($webvar{parentid}, $session->param("username"), parentID($webvar{parentid}, 'dom', 'group'), $restr);
-	changepage(page => "reclist", id => $webvar{parentid}, defrec => $webvar{defrec}, resultmsg => $restr);
-      }
+	logaction($webvar{parentid}, $session->param("username"),
+		parentID($dbh, (id => $webvar{parentid}, type => 'domain', revrec => $webvar{revrec})), $restr);
+      }
+      my %pageparams = (page => "reclist", id => $webvar{parentid},
+	defrec => $webvar{defrec}, revrec => $webvar{revrec});
+      $pageparams{warnmsg} = $msg."<br><br>\n".$restr if $code eq 'WARN';
+      $pageparams{resultmsg} = $restr if $code eq 'OK';
+      changepage(%pageparams);
     } else {
       $page->param(failed	=> 1);
@@ -568,8 +655,6 @@
       $page->param(recact	=> "add");
       $page->param(parentid	=> $webvar{parentid});
-      $page->param(defrec	=> $webvar{defrec});
       $page->param(id		=> $webvar{id});
       fill_recdata();	# populate the form... er, mostly.
-      $page->param(name => $webvar{name});
       if ($config{log_failures}) {
 	if ($webvar{defrec} eq 'y') {
@@ -577,5 +662,6 @@
 		"Failed adding default record '$webvar{name} $typemap{$webvar{type}} $webvar{address}', TTL $webvar{ttl} ($msg)");
 	} else {
-	  logaction($webvar{parentid}, $session->param("username"), parentID($webvar{parentid}, 'dom', 'group'),
+	  logaction($webvar{parentid}, $session->param("username"),
+		parentID($dbh, (id => $webvar{parentid}, type => 'domain', revrec => $webvar{revrec})),
 		"Failed adding record '$webvar{name} $typemap{$webvar{type}} $webvar{address}', TTL $webvar{ttl} ($msg)");
 	}
@@ -592,6 +678,5 @@
     $page->param(parentid	=> $webvar{parentid});
     $page->param(id		=> $webvar{id});
-    $page->param(defrec		=> $webvar{defrec});
-    my $recdata = getRecLine($dbh, $webvar{defrec}, $webvar{id});
+    my $recdata = getRecLine($dbh, $webvar{defrec}, $webvar{revrec}, $webvar{id});
     $page->param(name		=> $recdata->{host});
     $page->param(address	=> $recdata->{val});
@@ -600,5 +685,5 @@
     $page->param(port		=> $recdata->{port});
     $page->param(ttl		=> $recdata->{ttl});
-    fill_rectypes($recdata->{type});
+    $page->param(typelist	=> getTypelist($dbh, $webvar{revrec}, $webvar{type}));
 
   } elsif ($webvar{recact} eq 'update') {
@@ -613,5 +698,5 @@
 
     # get current/previous record info so we can log "updated 'foo A 1.2.3.4' to 'foo A 2.3.4.5'"
-    my $oldrec = getRecLine($dbh, $webvar{defrec}, $webvar{id});
+    my $oldrec = getRecLine($dbh, $webvar{defrec}, $webvar{revrec}, $webvar{id});
 
     my ($code,$msg) = updateRec($dbh,$webvar{defrec},$webvar{id},
@@ -629,5 +714,8 @@
 	my $restr = "Updated record from '$oldrec->{host} $typemap{$oldrec->{type}} $oldrec->{val}', TTL $oldrec->{ttl}\n".
 		"to '$webvar{name} $typemap{$webvar{type}} $webvar{address}', TTL $webvar{ttl}";
-	logaction($webvar{parentid}, $session->param("username"), parentID($webvar{id}, 'rec', 'group'), $restr);
+	logaction($webvar{parentid}, $session->param("username"),
+		parentID($dbh, (id => $webvar{id}, type => 'record', defrec => $webvar{defrec},
+			revrec => $webvar{revrec}, partype => 'group')),
+		$restr);
 	changepage(page => "reclist", id => $webvar{parentid}, defrec => $webvar{defrec}, resultmsg => $restr);
       }
@@ -639,5 +727,4 @@
       $page->param(recact	=> "update");
       $page->param(parentid	=> $webvar{parentid});
-      $page->param(defrec	=> $webvar{defrec});
       $page->param(id		=> $webvar{id});
       fill_recdata();
@@ -647,5 +734,6 @@
 		"Failed updating default record '$typemap{$webvar{type}} $webvar{name} $webvar{address}', TTL $webvar{ttl} ($msg)");
 	} else {
-	  logaction($webvar{parentid}, $session->param("username"), parentID($webvar{parentid}, 'dom', 'group'),
+	  logaction($webvar{parentid}, $session->param("username"),
+		parentID($dbh, (id => $webvar{parentid}, type => 'domain', revrec => $webvar{revrec})),
 		"Failed updating record '$typemap{$webvar{type}} $webvar{name} $webvar{address}', TTL $webvar{ttl} ($msg)");
 	}
@@ -658,5 +746,6 @@
   } else {
     $page->param(parentid => $webvar{parentid});
-    $page->param(dohere => domainName($dbh,$webvar{parentid}));
+    $page->param(dohere => domainName($dbh,$webvar{parentid})) if $webvar{revrec} eq 'n';
+    $page->param(dohere => revName($dbh,$webvar{parentid})) if $webvar{revrec} eq 'y';
   }
 
@@ -668,10 +757,13 @@
   # This is a complete separate segment since it uses a different template from add/edit records above
 
-  changepage(page => "reclist", errmsg => "You are not permitted to delete records", id => $webvar{parentid})
+  changepage(page => "reclist", errmsg => "You are not permitted to delete records", id => $webvar{parentid},
+		defrec => $webvar{defrec}, revrec => $webvar{revrec})
 	unless ($permissions{admin} || $permissions{record_delete});
 
   if (!check_scope(id => $webvar{id}, type =>
 	($webvar{defrec} eq 'y' ? ($webvar{revrec} eq 'y' ? 'defrevrec' : 'defrec') : 'record'))) {
-    changepage(page => 'domlist', errmsg => "You do not have permission to delete records in the requested ".
+    # redirect to domlist because we don't have permission for the entity requested
+    changepage(page => 'domlist', revrec => $webvar{revrec},
+	errmsg => "You do not have permission to delete records in the requested ".
 	($webvar{defrec} eq 'y' ? 'group' : 'domain'));
   }
@@ -679,9 +771,10 @@
   $page->param(id => $webvar{id});
   $page->param(defrec => $webvar{defrec});
+  $page->param(revrec => $webvar{revrec});
   $page->param(parentid => $webvar{parentid});
   # first pass = confirm y/n (sorta)
   if (!defined($webvar{del})) {
     $page->param(del_getconf => 1);
-    my $rec = getRecLine($dbh,$webvar{defrec},$webvar{id});
+    my $rec = getRecLine($dbh, $webvar{defrec}, $webvar{revrec}, $webvar{id});
     $page->param(host => $rec->{host});
     $page->param(ftype => $typemap{$rec->{type}});
@@ -689,16 +782,22 @@
   } elsif ($webvar{del} eq 'ok') {
 # get rec data before we try to delete it
-    my $rec = getRecLine($dbh,$webvar{defrec},$webvar{id});
-    my ($code,$msg) = delRec($dbh,$webvar{defrec},$webvar{id});
+    my $rec = getRecLine($dbh, $webvar{defrec}, $webvar{revrec}, $webvar{id});
+    my ($code,$msg) = delRec($dbh, $webvar{defrec}, $webvar{revrec}, $webvar{id});
     if ($code eq 'OK') {
       if ($webvar{defrec} eq 'y') {
+	my $recclass = ($webvar{revrec} eq 'n' ? 'default record' : 'default reverse record');
 ##fixme:  log distance for MX;  log port/weight/distance for SRV
-	my $restr = "Deleted default record '$rec->{host} $typemap{$rec->{type}} $rec->{val}', TTL $rec->{ttl}";
+	my $restr = "Deleted $recclass '$rec->{host} $typemap{$rec->{type}} $rec->{val}', TTL $rec->{ttl}";
 	logaction(0, $session->param("username"), $rec->{parid}, $restr);
-	changepage(page => "reclist", id => $webvar{parentid}, defrec => $webvar{defrec}, resultmsg => $restr);
+	changepage(page => "reclist", id => $webvar{parentid}, defrec => $webvar{defrec},
+		revrec => $webvar{revrec}, resultmsg => $restr);
       } else {
-	my $restr = "Deleted record '$rec->{host} $typemap{$rec->{type}} $rec->{val}', TTL $rec->{ttl}";
-	logaction($rec->{parid}, $session->param("username"), parentID($rec->{parid}, 'dom', 'group'), $restr);
-	changepage(page => "reclist", id => $webvar{parentid}, defrec => $webvar{defrec}, resultmsg => $restr);
+	my $recclass = ($webvar{revrec} eq 'n' ? 'record' : 'reverse record');
+	my $restr = "Deleted $recclass '$rec->{host} $typemap{$rec->{type}} $rec->{val}', TTL $rec->{ttl}";
+	logaction($rec->{parid}, $session->param("username"),
+		parentID($dbh, (id => $rec->{parid}, type => 'domain', revrec => $webvar{revrec})),
+		$restr);
+	changepage(page => "reclist", id => $webvar{parentid}, defrec => $webvar{defrec},
+		revrec => $webvar{revrec}, resultmsg => $restr);
       }
     } else {
@@ -710,13 +809,14 @@
 		" TTL $rec->{ttl} ($msg)");
 	} else {
-	  logaction($rec->{parid}, $session->param("username"), parentID($rec->{parid}, 'dom', 'group'),
+	  logaction($rec->{parid}, $session->param("username"),
+		parentID($dbh, (id => $rec->{parid}, type => 'domain', revrec => $webvar{revrec})),
 		"Failed deleting record '$rec->{host} $typemap{$rec->{type}} $rec->{val}', TTL $rec->{ttl} ($msg)");
 	}
       }
       changepage(page => "reclist", id => $webvar{parentid}, defrec => $webvar{defrec},
-		errmsg => "Error deleting record: $msg");
+		revrec => $webvar{revrec}, errmsg => "Error deleting record: $msg");
     }
   } else {
-    changepage(page => "reclist", id => $webvar{parentid}, defrec => $webvar{defrec});
+    changepage(page => "reclist", id => $webvar{parentid}, defrec => $webvar{defrec}, revrec => $webvar{revrec});
   }
 
@@ -724,5 +824,7 @@
 
   # security check - does the user have permission to view this entity?
-  if (!check_scope(id => $webvar{id}, type => ($webvar{defrec} eq 'y' ? 'group' : 'domain'))) {
+  # id is domain/revzone/group id
+  if (!check_scope(id => $webvar{id}, type =>
+	($webvar{defrec} eq 'y' ? 'group' : ($webvar{revrec} eq 'y' ? 'revzone' : 'domain')))) {
     changepage(page => 'domlist', errmsg => "You do not have permission to edit the ".
 	($webvar{defrec} eq 'y' ? 'default ' : '')."SOA record for the requested ".
@@ -744,9 +846,11 @@
   # security check - does the user have permission to view this entity?
   # pass 1, record ID
-  if (!check_scope(id => $webvar{recid}, type => ($webvar{defrec} eq 'y' ? 'defrec' : 'record'))) {
+  if (!check_scope(id => $webvar{recid}, type =>
+	($webvar{defrec} eq 'y' ? ($webvar{revrec} eq 'y' ? 'defrevrec' : 'defrec') : 'record'))) {
     changepage(page => 'domlist', errmsg => "You do not have permission to edit the requested SOA record");
   }
   # pass 2, parent (group or domain) ID
-  if (!check_scope(id => $webvar{id}, type => ($webvar{defrec} eq 'y' ? 'group' : 'domain'))) {
+  if (!check_scope(id => $webvar{id}, type =>
+	($webvar{defrec} eq 'y' ? 'group' : ($webvar{revrec} eq 'y' ? 'revzone' : 'domain')))) {
     changepage(page => 'domlist', errmsg => "You do not have permission to edit the ".
 	($webvar{defrec} eq 'y' ? 'default ' : '')."SOA record for the requested ".
@@ -787,5 +891,5 @@
       $logdomain = 0;
     } else {
-      $loggroup = parentID($logdomain, 'dom', 'group', $webvar{defrec});
+      $loggroup = parentID($dbh, (id => $logdomain, type => 'domain', revrec => $webvar{revrec}));
     }
 
@@ -898,5 +1002,5 @@
   } elsif ($webvar{del} eq 'ok') {
     my $deleteme = groupName($dbh,$webvar{id}); # get this before we delete it...
-    my $delparent = parentID($webvar{id}, 'group','group');
+    my $delparent = parentID($dbh, (id => $webvar{id}, type => 'group'));
     my ($code,$msg) = delGroup($dbh, $webvar{id});
     if ($code eq 'OK') {
@@ -1041,9 +1145,11 @@
       my ($code, $msg) = changeGroup($dbh, 'domain', $webvar{$_}, $webvar{destgroup});
       if ($code eq 'OK') {
-        logaction($webvar{$_}, $session->param("username"), parentID($webvar{$_}, 'dom', 'group'),
+        logaction($webvar{$_}, $session->param("username"),
+		parentID($dbh, (id => $webvar{$_}, type => 'domain', revrec => $webvar{revrec})),
 		"Moved domain ".domainName($dbh, $webvar{$_})." to group $newgname");
         $row{domok} = ($code eq 'OK');
       } else {
-        logaction($webvar{$_}, $session->param("username"), parentID($webvar{$_}, 'dom', 'group'),
+        logaction($webvar{$_}, $session->param("username"),
+		parentID($dbh, (id => $webvar{$_}, type => 'domain', revrec => $webvar{revrec})),
 		"Failed to move domain ".domainName($dbh, $webvar{$_})." to group $newgname: $msg")
 		if $config{log_failures};
@@ -1072,6 +1178,7 @@
 ##fixme:  error handling on status change
       my $stat = domStatus($dbh,$webvar{$_},($webvar{bulkaction} eq 'activate' ? 'domon' : 'domoff'));
-      logaction($webvar{$_}, $session->param("username"), parentID($webvar{$_}, 'dom', 'group'),
-		"Changed domain ".domainName($dbh, $webvar{$_})." state to ".($stat ? 'active' : 'inactive'));
+      logaction($webvar{$_}, $session->param("username"),
+	parentID($dbh, (id => $webvar{$_}, type => 'domain', revrec => $webvar{revrec})),
+	"Changed domain ".domainName($dbh, $webvar{$_})." state to ".($stat ? 'active' : 'inactive'));
       $row{domok} = 1;
 #      $row{domok} = ($code eq 'OK');
@@ -1097,5 +1204,5 @@
       }
       $row{domain} = domainName($dbh,$webvar{$_});
-      my $pargroup = parentID($webvar{$_}, 'dom', 'group');
+      my $pargroup = parentID($dbh, (id => $webvar{$_}, type => 'domain', revrec => $webvar{revrec}));
       my $dom = domainName($dbh, $webvar{$_});
       my ($code, $msg) = delDomain($dbh, $webvar{$_});
@@ -1130,5 +1237,5 @@
     if ($flag && ($permissions{admin} || $permissions{user_edit})) {
       my $stat = userStatus($dbh,$webvar{id},$webvar{userstatus});
-      logaction(0, $session->param("username"), parentID($webvar{id}, 'user', 'group'),
+      logaction(0, $session->param("username"), parentID($dbh, (id => $webvar{id}, type => 'user')),
 	($stat ? 'Enabled' : 'Disabled')." ".userFullName($dbh, $webvar{id}, '%u'));
       $page->param(resultmsg => ($stat ? 'Enabled' : 'Disabled')." ".userFullName($dbh, $webvar{id}, '%u'));
@@ -1388,5 +1495,5 @@
 
   $page->param(qfor => $webvar{qfor}) if $webvar{qfor};
-  fill_rectypes($webvar{type} ? $webvar{type} : '', 1);
+  $page->param(typelist => getTypelist($dbh, 'l', ($webvar{type} ? $webvar{type} : undef)));
   $page->param(nrecurse => $webvar{nrecurse}) if $webvar{nrecurse};
   $page->param(resolver => $webvar{resolver}) if $webvar{resolver};
@@ -1563,4 +1670,12 @@
     }
     $page->param(logfor => 'domain '.domainName($dbh,$id));
+  } elsif ($webvar{ltype} && $webvar{ltype} eq 'rdns') {
+    $sql .= "rdns_id=?";
+    $id = $webvar{id};
+    if (!check_scope(id => $id, type => 'revzone')) {
+      $page->param(errmsg => "You are not permitted to view log entries for the requested reverse zone");
+      goto DONELOG;
+    }
+    $page->param(logfor => 'reverse zone '.revName($dbh,$id));
   } else {
     # Default to listing curgroup log
@@ -1570,4 +1685,9 @@
     # group log is always for the "current" group
   }
+##fixme:
+# - filtering
+# - show reverse zone column?
+# - pagination/limiting number of records - put newest-first so user
+#   doesn't always need to go to the last page for recent activity?
   my $sth = $dbh->prepare($sql);
   $sth->execute($id);
@@ -1601,4 +1721,7 @@
   $page->param(logingrp => groupName($dbh,$logingroup));
   $page->param(logingrp_num => $logingroup);
+
+##fixme
+  $page->param(mayrdns => 1);
 
   $page->param(maydefrec => $permissions{admin});
@@ -1696,5 +1819,5 @@
   # handle user check
   my $newurl = "http://$ENV{HTTP_HOST}$ENV{SCRIPT_NAME}?sid=$sid";
-  foreach (keys %params) {
+  foreach (sort keys %params) {
     $newurl .= "&$_=".$q->url_encode($params{$_});
   }
@@ -1743,10 +1866,11 @@
 }
 
-sub showdomain {
+sub showzone {
   my $def = shift;
+  my $rev = shift;
   my $id = shift;
 
   # get the SOA first
-  my %soa = getSOA($dbh,$def,$id);
+  my %soa = getSOA($dbh,$def,$rev,$id);
 
   $page->param(contact	=> $soa{contact});
@@ -1758,5 +1882,5 @@
   $page->param(ttl	=> $soa{ttl});
 
-  my $foo2 = getDomRecs($dbh,$def,$id,$perpage,$webvar{offset},$sortby,$sortorder,$filter);
+  my $foo2 = getDomRecs($dbh,$def,$rev,$id,$perpage,$webvar{offset},$sortby,$sortorder,$filter);
 
   my $row = 0;
@@ -1765,6 +1889,8 @@
     $rec->{row} = $row % 2;
     $rec->{defrec} = $def;
+    $rec->{revrec} = $rev;
     $rec->{sid} = $webvar{sid};
     $rec->{id} = $id;
+    $rec->{fwdzone} = $rev eq 'n';
     $rec->{distance} = 'n/a' unless ($rec->{type} eq 'MX' || $rec->{type} eq 'SRV'); 
     $rec->{weight} = 'n/a' unless ($rec->{type} eq 'SRV'); 
@@ -1778,27 +1904,6 @@
 }
 
-# fill in record type list on add/update/edit record template
-sub fill_rectypes {
-  my $type = shift || $reverse_typemap{A};
-  my $soaflag = shift || 0;
-
-  my $sth = $dbh->prepare("SELECT val,name FROM rectypes WHERE stdflag=1 ORDER BY listorder");
-  $sth->execute;
-  my @typelist;
-  while (my ($rval,$rname) = $sth->fetchrow_array()) {
-    my %row = ( recval => $rval, recname => $rname );
-    $row{tselect} = 1 if $rval == $type;
-    push @typelist, \%row;
-  }
-  if ($soaflag) {
-    my %row = ( recval => $reverse_typemap{SOA}, recname => 'SOA' );
-    $row{tselect} = 1 if $reverse_typemap{SOA} == $type;
-    push @typelist, \%row;
-  }
-  $page->param(typelist	=> \@typelist);
-} # fill_rectypes
-
 sub fill_recdata {
-  fill_rectypes($webvar{type});
+  $page->param(typelist => getTypelist($dbh, $webvar{revrec}, $webvar{type}));
 
 # le sigh.  we may get called with many empty %webvar keys
@@ -1807,13 +1912,21 @@
 ##todo:  allow BIND-style bare names, ASS-U-ME that the name is within the domain?
 # prefill <domain> or DOMAIN in "Host" space for new records
-  my $domroot = ($webvar{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$webvar{parentid}));
-  $page->param(name	=> $domroot);
-  $page->param(address	=> $webvar{address});
-  $page->param(distance	=> $webvar{distance})
+  if ($webvar{revrec} eq 'n') {
+    my $domroot = ($webvar{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$webvar{parentid}));
+    $page->param(name	=> $domroot);
+    $page->param(address	=> $webvar{address});
+    $page->param(distance	=> $webvar{distance})
 	if ($webvar{type} == $reverse_typemap{MX} or $webvar{type} == $reverse_typemap{SRV});
-  $page->param(weight	=> $webvar{weight}) if $webvar{type} == $reverse_typemap{SRV};
-  $page->param(port	=> $webvar{port}) if $webvar{type} == $reverse_typemap{SRV};
+    $page->param(weight	=> $webvar{weight}) if $webvar{type} == $reverse_typemap{SRV};
+    $page->param(port	=> $webvar{port}) if $webvar{type} == $reverse_typemap{SRV};
+  } else {
+    my $domroot = ($webvar{defrec} eq 'y' ? 'ADMINDOMAIN' : ".$config{domain}");
+    $page->param(name	=> ($webvar{name} ? $webvar{name} : $domroot));
+    my $zname = ($webvar{defrec} eq 'y' ? 'ZONE' : revName($dbh,$webvar{parentid}));
+    $zname =~ s|\d*/\d+$||;
+    $page->param(address	=> ($webvar{address} ? $webvar{address} : $zname));
+  }
 # retrieve the right ttl instead of falling (way) back to the hardcoded system default
-  my %soa = getSOA($dbh,$webvar{defrec},$webvar{parentid});
+  my %soa = getSOA($dbh,$webvar{defrec},$webvar{revrec},$webvar{parentid});
   $page->param(ttl	=> ($webvar{ttl} ? $webvar{ttl} : $soa{minttl}));
 }
@@ -1893,5 +2006,5 @@
   # on a page showing nothing.
   # For bonus points, this reverts to the original offset on clicking the "All" link (mostly)
-  if ($offset ne 'all') {  
+  if ($offset ne 'all') {
     $offset-- while ($offset * $perpage) >= $pgcount;
   }
@@ -1907,6 +2020,8 @@
 } # end fill_pgcount()
 
-sub listdomains {
-
+
+sub listdomains { listzones(); }	# temp
+
+sub listzones {
 # ACLs
   $page->param(domain_create	=> ($permissions{admin} || $permissions{domain_create}) );
@@ -1918,13 +2033,9 @@
   my $childlist = join(',',@childgroups);
 
-  my $sql = "SELECT count(*) FROM domains WHERE group_id IN ($curgroup".($childlist ? ",$childlist" : '').")".
-	($startwith ? " AND domain ~* ?" : '').
-	($filter ? " AND domain ~* ?" : '');
-  my $sth = $dbh->prepare($sql);
-  $sth->execute(@filterargs);
-  my ($count) = $sth->fetchrow_array;
+  my $count = getZoneCount($dbh, (childlist => $childlist, curgroup => $curgroup, revrec => $webvar{revrec},
+	filter => ($filter ? $filter : undef), startwith => ($startwith ? $startwith : undef) ) );
 
 # fill page count and first-previous-next-last-all bits
-  fill_pgcount($count,"domains",groupName($dbh,$curgroup));
+  fill_pgcount($count,($webvar{revrec} eq 'n' ? 'domains' : 'revzones'),groupName($dbh,$curgroup));
   fill_fpnla($count);
 
@@ -1937,6 +2048,6 @@
 
 # set up the headers
-  my @cols = ('domain', 'status', 'group');
-  my %colheads = (domain => 'Domain', status => 'Status', group => 'Group');
+  my @cols = (($webvar{revrec} eq 'n' ? 'domain' : 'revnet'), 'status', 'group');
+  my %colheads = (domain => 'Domain', revnet => 'Reverse Zone', status => 'Status', group => 'Group');
   fill_colheads($sortby, $sortorder, \@cols, \%colheads);
 
@@ -1946,4 +2057,5 @@
 
 # waffle, waffle - keep state on these as well as sortby, sortorder?
+##fixme:  put this higher so the count doesn't get munched?
   $page->param("start$startwith" => 1) if $startwith && $startwith =~ /^(?:[a-z]|0-9)$/;
 
@@ -1951,36 +2063,15 @@
   $page->param(searchsubs => $searchsubs) if $searchsubs;
 
-##fixme
-##fixme  push the SQL and direct database fiddling off into a sub in DNSDB.pm
-##fixme
-
   $page->param(group => $curgroup);
-  my @domlist;
-  $sql = "SELECT domain_id,domain,status,groups.group_name AS group FROM domains".
-	" INNER JOIN groups ON domains.group_id=groups.group_id".
-	" WHERE domains.group_id IN ($curgroup".($childlist ? ",$childlist" : '').")".
-	($startwith ? " AND domain ~* ?" : '').
-	($filter ? " AND domain ~* ?" : '').
-	" ORDER BY ".($sortby eq 'group' ? 'groups.group_name' : $sortby).
-	" $sortorder ".($offset eq 'all' ? '' : " LIMIT $perpage OFFSET ".$offset*$perpage);
-  $sth = $dbh->prepare($sql);
-  $sth->execute(@filterargs);
-  my $rownum = 0;
-  while (my @data = $sth->fetchrow_array) {
-    my %row;
-    $row{domainid} = $data[0];
-    $row{domain} = $data[1];
-    $row{status} = ($data[2] ? 'Active' : 'Inactive');
-    $row{group} = $data[3];
-    $row{bg} = ($rownum++)%2;
-    $row{mkactive} = !$data[2];
-    $row{sid} = $sid;
-    $row{offset} = $offset;
-# ACLs
-    $row{domain_edit} = ($permissions{admin} || $permissions{domain_edit});
-    $row{domain_delete} = ($permissions{admin} || $permissions{domain_delete});
-    push @domlist, \%row;
-  }
-  $page->param(domtable => \@domlist);
+
+  my $zonelist = getZoneList($dbh, (childlist => $childlist, curgroup => $curgroup,
+	revrec => $webvar{revrec},
+	filter => ($filter ? $filter : undef), startwith => ($startwith ? $startwith : undef),
+	offset => $webvar{offset}, sortby => $sortby, sortorder => $sortorder
+	) );
+# probably don't need this, keeping for reference for now
+#  foreach (@$zonelist) {
+#  }
+  $page->param(domtable => $zonelist);
 } # end listdomains()
 
@@ -2221,4 +2312,5 @@
   my $groupid = shift;
   my $entry = shift;
+  my $revid = shift || 0;
 
 ##fixme: push SQL into DNSDB.pm
@@ -2228,44 +2320,8 @@
   my ($user_id, $fullname) = $sth->fetchrow_array;
 
-  $sth = $dbh->prepare("INSERT INTO log (domain_id,user_id,group_id,email,name,entry) ".
-	"VALUES (?,?,?,?,?,?)") or warn $dbh->errstr;
-  $sth->execute($domid,$user_id,$groupid,$username,$fullname,$entry) or warn $sth->errstr;
+  $sth = $dbh->prepare("INSERT INTO log (domain_id,user_id,group_id,email,name,entry,rdns_id) ".
+	"VALUES (?,?,?,?,?,?,?)") or warn $dbh->errstr;
+  $sth->execute($domid,$user_id,$groupid,$username,$fullname,$entry,$revid) or warn $sth->errstr;
 } # end logaction()
-
-
-##fixme:  generalize to return appropriate id on all cases (ie, use $partype)
-sub parentID {
-  my $id = shift;
-  my $idtype = shift;
-  my $partype = shift;
-  my $defrec = shift || '';
-
-  my $sql = '';
-
-  if ($idtype eq 'dom') {
-    return $id if $defrec eq 'y';  # "domain" + default records, we're really looking at a group.
-    $sql = "SELECT group_id FROM domains WHERE domain_id=?";
-  } elsif ($idtype eq 'rec') {
-    if ($defrec eq 'y') {
-      $sql = "SELECT group_id FROM default_records WHERE record_id=?";
-    } else {
-      $sql = "SELECT d.group_id FROM domains d".
-    	" INNER JOIN records r ON d.domain_id=r.domain_id".
-	" WHERE r.record_id=?";
-    }
-  } elsif ($idtype eq 'group') {
-    $sql = "SELECT parent_group_id FROM groups WHERE group_id=?";
-  } elsif ($idtype eq 'user') {
-    $sql = "SELECT group_id FROM users WHERE user_id=?";
-  } else {
-    return "FOO", "BAR";  # can't get here.... we think.
-  }
-  my $sth = $dbh->prepare($sql);
-  $sth->execute($id);
-  my ($retid) = $sth->fetchrow_array;
-  return $retid if $retid;
-  # ahh! fall of the edge of the world if things went sideways
-  ##fixme:  really need to do a little more error handling, I think
-} # end parentID()
 
 
Index: branches/stable/dns.sql
===================================================================
--- branches/stable/dns.sql	(revision 535)
+++ branches/stable/dns.sql	(revision 544)
@@ -41,7 +41,32 @@
 \.
 
+CREATE TABLE default_rev_records (
+    record_id serial NOT NULL,
+    group_id integer DEFAULT 1 NOT NULL,
+    host text DEFAULT '' NOT NULL,
+    "type" integer DEFAULT 1 NOT NULL,
+    val text DEFAULT '' NOT NULL,
+    ttl integer DEFAULT 86400 NOT NULL,
+    description text
+);
+
+COPY default_rev_records (record_id, group_id, host, "type", val, ttl, description) FROM stdin;
+1	1	hostmaster.ADMINDOMAIN:ns1.ADMINDOMAIN	6	3600:900:1048576:2560	3600	
+2	1	unused-%r.ADMINDOMAIN	65283	ZONE	3600	
+\.
+
 CREATE TABLE domains (
     domain_id serial NOT NULL,
     "domain" character varying(80) NOT NULL,
+    group_id integer DEFAULT 1 NOT NULL,
+    description character varying(255) DEFAULT ''::character varying NOT NULL,
+    status integer DEFAULT 1 NOT NULL,
+    zserial integer,
+    sertype character(1) DEFAULT 'D'::bpchar
+);
+
+CREATE TABLE revzones (
+    rdns_id serial NOT NULL,
+    revnet cidr NOT NULL,
     group_id integer DEFAULT 1 NOT NULL,
     description character varying(255) DEFAULT ''::character varying NOT NULL,
@@ -69,4 +94,5 @@
     log_id serial NOT NULL,
     domain_id integer,
+    rdns_id integer,
     user_id integer,
     group_id integer,
@@ -103,7 +129,8 @@
 \.
 
--- fixme:  need to handle looooong records (eg, SPF)
+-- rdns_id defaults to 0 since many records will not have an associated rDNS entry.
 CREATE TABLE records (
-    domain_id integer NOT NULL,
+    domain_id integer NOT NULL DEFAULT 0,
+    rdns_id integer NOT NULL DEFAULT 0,
     record_id serial NOT NULL,
     host text DEFAULT '' NOT NULL,
@@ -119,5 +146,5 @@
 CREATE TABLE rectypes (
     val integer NOT NULL,
-    name character varying(12) NOT NULL,
+    name character varying(20) NOT NULL,
     stdflag integer DEFAULT 1 NOT NULL,
     listorder integer DEFAULT 255 NOT NULL,
@@ -129,68 +156,77 @@
 COPY rectypes (val, name, stdflag, listorder, alphaorder) FROM stdin;
 1	A	1	1	1
-2	NS	1	2	37
-3	MD	2	255	29
-4	MF	2	255	30
-5	CNAME	1	6	9
-6	SOA	0	8	53
-7	MB	3	255	28
-8	MG	3	255	31
-9	MR	3	255	33
-10	NULL	3	255	43
-11	WKS	3	255	64
-12	PTR	2	4	46
-13	HINFO	3	255	18
-14	MINFO	3	255	32
-15	MX	1	3	34
-16	TXT	1	5	60
-17	RP	2	255	48
-18	AFSDB	3	255	4
-19	X25	3	255	65
-20	ISDN	3	255	21
-21	RT	3	255	50
-22	NSAP	3	255	38
-23	NSAP-PTR	3	255	39
-24	SIG	3	255	51
-25	KEY	3	255	23
-26	PX	3	255	47
-27	GPOS	3	255	17
-28	AAAA	1	2	3
-29	LOC	3	255	25
-30	NXT	3	255	44
-31	EID	3	255	15
-32	NIMLOC	3	255	36
-33	SRV	1	7	55
-34	ATMA	3	255	6
-35	NAPTR	3	255	35
-36	KX	3	255	24
-37	CERT	3	255	8
-38	A6	3	3	2
-39	DNAME	3	255	12
-40	SINK	3	255	52
-41	OPT	3	255	45
-42	APL	3	255	5
-43	DS	3	255	14
-44	SSHFP	3	255	56
-45	IPSECKEY	3	255	20
-46	RRSIG	3	255	49
-47	NSEC	3	255	40
-48	DNSKEY	3	255	13
-49	DHCID	3	255	10
-50	NSEC3	3	255	41
-51	NSEC3PARAM	3	255	42
-55	HIP	3	255	19
-99	SPF	3	255	54
-100	UINFO	3	255	62
-101	UID	3	255	61
-102	GID	3	255	16
-103	UNSPEC	3	255	63
-249	TKEY	3	255	58
-250	TSIG	3	255	59
-251	IXFR	3	255	22
-252	AXFR	3	255	7
-253	MAILB	3	255	27
-254	MAILA	3	255	26
-32768	TA	3	255	57
-32769	DLV	3	255	11
+2	NS	1	5	37
+3	MD	5	255	29
+4	MF	5	255	30
+5	CNAME	1	7	9
+6	SOA	0	0	53
+7	MB	5	255	28
+8	MG	5	255	31
+9	MR	5	255	33
+10	NULL	5	255	43
+11	WKS	5	255	64
+12	PTR	3	10	46
+13	HINFO	5	255	18
+14	MINFO	5	255	32
+15	MX	1	6	34
+16	TXT	1	8	60
+17	RP	4	255	48
+18	AFSDB	5	255	4
+19	X25	5	255	65
+20	ISDN	5	255	21
+21	RT	5	255	50
+22	NSAP	5	255	38
+23	NSAP-PTR	5	255	39
+24	SIG	5	255	51
+25	KEY	5	255	23
+26	PX	5	255	47
+27	GPOS	5	255	17
+28	AAAA	1	3	3
+29	LOC	5	255	25
+30	NXT	5	255	44
+31	EID	5	255	15
+32	NIMLOC	5	255	36
+33	SRV	1	9	55
+34	ATMA	5	255	6
+35	NAPTR	5	255	35
+36	KX	5	255	24
+37	CERT	5	255	8
+38	A6	5	3	2
+39	DNAME	5	255	12
+40	SINK	5	255	52
+41	OPT	5	255	45
+42	APL	5	255	5
+43	DS	5	255	14
+44	SSHFP	5	255	56
+45	IPSECKEY	5	255	20
+46	RRSIG	5	255	49
+47	NSEC	5	255	40
+48	DNSKEY	5	255	13
+49	DHCID	5	255	10
+50	NSEC3	5	255	41
+51	NSEC3PARAM	5	255	42
+55	HIP	5	255	19
+99	SPF	5	255	54
+100	UINFO	5	255	62
+101	UID	5	255	61
+102	GID	5	255	16
+103	UNSPEC	5	255	63
+249	TKEY	5	255	58
+250	TSIG	5	255	59
+251	IXFR	5	255	22
+252	AXFR	5	255	7
+253	MAILB	5	255	27
+254	MAILA	5	255	26
+32768	TA	5	255	57
+32769	DLV	5	255	11
+\.
+
+-- Custom types (ab)using the "Private use" range from 65280 to 65534
+COPY rectypes (val, name, stdflag, listorder, alphaorder) FROM stdin;
+65280	A+PTR	2	2	2
+65281	AAAA+PTR	2	4	4
+65282	PTR template	3	11	2
+65283	A+PTR template	3	12	2
+65284	AAAA+PTR template	3	13	2
 \.
 
@@ -254,7 +290,4 @@
     ADD CONSTRAINT "$1" FOREIGN KEY (group_id) REFERENCES groups(group_id);
 
-ALTER TABLE ONLY records
-    ADD CONSTRAINT "$1" FOREIGN KEY (domain_id) REFERENCES domains(domain_id);
-
 ALTER TABLE ONLY users
     ADD CONSTRAINT "$1" FOREIGN KEY (group_id) REFERENCES groups(group_id);
@@ -264,9 +297,10 @@
 
 -- set starting sequence numbers, since we've inserted data before they're active
-SELECT pg_catalog.setval('misc_misc_id_seq', 1, true);
-SELECT pg_catalog.setval('default_records_record_id_seq', 8, true);
+SELECT pg_catalog.setval('misc_misc_id_seq', 2, false);
+SELECT pg_catalog.setval('default_records_record_id_seq', 8, false);
+SELECT pg_catalog.setval('default_rev_records_record_id_seq', 3, false);
 SELECT pg_catalog.setval('domains_domain_id_seq', 1, false);
-SELECT pg_catalog.setval('groups_group_id_seq', 1, true);
-SELECT pg_catalog.setval('permissions_permission_id_seq', 2, true);
+SELECT pg_catalog.setval('groups_group_id_seq', 2, false);
+SELECT pg_catalog.setval('permissions_permission_id_seq', 3, false);
 SELECT pg_catalog.setval('records_record_id_seq', 1, false);
 SELECT pg_catalog.setval('users_user_id_seq', 2, false);
Index: branches/stable/templates/addrevzone.tmpl
===================================================================
--- branches/stable/templates/addrevzone.tmpl	(revision 544)
+++ branches/stable/templates/addrevzone.tmpl	(revision 544)
@@ -0,0 +1,5 @@
+<TMPL_IF add_failed>
+<TMPL_INCLUDE NAME="newrevzone.tmpl">
+<TMPL_ELSE>
+<TMPL_INCLUDE NAME="reclist.tmpl">
+</TMPL_IF>
Index: branches/stable/templates/badpage.tmpl
===================================================================
--- branches/stable/templates/badpage.tmpl	(revision 535)
+++ branches/stable/templates/badpage.tmpl	(revision 544)
@@ -1,4 +1,5 @@
 <!-- <TMPL_VAR NAME=sid> -->
 <div id="badpage">
+<TMPL_IF badpage>
 Bad page requested:
 <div class="errmsg">
@@ -6,3 +7,10 @@
 </div>
 Press the 'Back' button on your browser to continue.
+</TMPL_IF>
+<TMPL_IF badtemplate>
+Template error:
+<div class="warnmsg">
+<TMPL_VAR NAME=badtemplate>
 </div>
+</TMPL_IF>
+</div>
Index: branches/stable/templates/delrec.tmpl
===================================================================
--- branches/stable/templates/delrec.tmpl	(revision 535)
+++ branches/stable/templates/delrec.tmpl	(revision 544)
@@ -6,5 +6,7 @@
 <h3>Are you really sure you want to delete record:<br />
 <TMPL_VAR NAME=host> <TMPL_VAR NAME=ftype> <TMPL_VAR NAME=recval></h3>
-<a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=delrec&amp;del=cancel&amp;id=<TMPL_VAR NAME=id>&amp;defrec=<TMPL_VAR NAME=defrec>&amp;parentid=<TMPL_VAR NAME=parentid>">cancel</a> &nbsp; | &nbsp; <a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=delrec&amp;del=ok&amp;id=<TMPL_VAR NAME=id>&amp;defrec=<TMPL_VAR NAME=defrec>&amp;parentid=<TMPL_VAR NAME=parentid>">confirm</a>
+<a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=delrec&amp;del=cancel&amp;id=<TMPL_VAR NAME=id>&amp;defrec=<TMPL_VAR NAME=defrec>&amp;revrec=<TMPL_VAR NAME=revrec>&amp;parentid=<TMPL_VAR NAME=parentid>">cancel</a>
+ &nbsp; | &nbsp; 
+<a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=delrec&amp;del=ok&amp;id=<TMPL_VAR NAME=id>&amp;defrec=<TMPL_VAR NAME=defrec>&amp;revrec=<TMPL_VAR NAME=revrec>&amp;parentid=<TMPL_VAR NAME=parentid>">confirm</a>
 </td></tr></table>
 
Index: branches/stable/templates/domlist.tmpl
===================================================================
--- branches/stable/templates/domlist.tmpl	(revision 535)
+++ branches/stable/templates/domlist.tmpl	(revision 544)
@@ -9,9 +9,9 @@
 </TMPL_IF>
 <TMPL_IF errmsg>
-<div class='errmsg'><TMPL_VAR NAME=errmsg></div>
+<div class="errmsg"><TMPL_VAR NAME=errmsg></div>
 </TMPL_IF>
 
 <table width="98%">
-<tr><th colspan="3"><div class="center maintitle">Domain list</div></th></tr>
+<tr><th colspan="3"><div class="center maintitle"><TMPL_IF domlist>Domain<TMPL_ELSE>Reverse zone</TMPL_IF> list</div></th></tr>
 <tr>
 <td class="leftthird"><TMPL_INCLUDE NAME="pgcount.tmpl"></td>
@@ -19,6 +19,14 @@
 <td class="rightthird"><TMPL_INCLUDE NAME="sbox.tmpl"></td>
 </tr>
-<tr><td colspan="3" align="center"><TMPL_INCLUDE NAME="lettsearch.tmpl"></td></tr>
-<tr><td colspan="3" align="right"><TMPL_IF domain_create><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=newdomain">New Domain</a></TMPL_IF></td></tr>
+<TMPL_IF domlist><tr><td colspan="3" align="center"><TMPL_INCLUDE NAME="lettsearch.tmpl"></td></tr></TMPL_IF>
+<tr><td colspan="3" align="right">
+<TMPL_IF domain_create>
+<TMPL_IF domlist>
+<a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=newdomain">New Domain</a>
+<TMPL_ELSE>
+<a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=newrevzone">New Reverse Zone</a>
+</TMPL_IF>
+</TMPL_IF>
+</td></tr>
 </table>
 
@@ -37,14 +45,14 @@
 <TMPL_IF name=domtable>
 <TMPL_LOOP name=domtable>
-<tr class="row<TMPL_VAR name=bg>">
-	<td align="left"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=reclist&amp;id=<TMPL_VAR NAME=domainid>&amp;defrec=n"><TMPL_VAR NAME=domain></a></td>
-	<td><TMPL_VAR name=status></td>
+<tr class="row<TMPL_IF __odd__>1<TMPL_ELSE>0</TMPL_IF>">
+	<td align="left"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=reclist&amp;id=<TMPL_VAR NAME=domainid>&amp;defrec=n<TMPL_UNLESS domlist>&amp;revrec=y</TMPL_UNLESS>"><TMPL_VAR NAME=domain></a></td>
+	<td><TMPL_IF status>Active<TMPL_ELSE>Inactive</TMPL_IF></td>
 	<td><TMPL_VAR name=group></td>
-<TMPL_IF domain_edit>	<td align="center"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=domlist<TMPL_IF NAME=offset>&amp;offset=<TMPL_VAR NAME=offset></TMPL_IF>&amp;id=<TMPL_VAR NAME=domainid>&amp;domstatus=<TMPL_IF NAME=mkactive>domon<TMPL_ELSE>domoff</TMPL_IF>"><TMPL_IF NAME=mkactive>activate<TMPL_ELSE>deactivate</TMPL_IF></a></td></TMPL_IF>
-<TMPL_IF domain_delete>	<td align="center"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=deldom&amp;id=<TMPL_VAR NAME=domainid>"><img src="images/trash2.png" alt="[ Delete ]" /></a></td></TMPL_IF>
+<TMPL_IF domain_edit>	<td align="center"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=<TMPL_VAR NAME=curpage><TMPL_IF NAME=offset>&amp;offset=<TMPL_VAR NAME=offset></TMPL_IF>&amp;id=<TMPL_VAR NAME=domainid>&amp;domstatus=<TMPL_IF status>domoff<TMPL_ELSE>domon</TMPL_IF>"><TMPL_IF status>deactivate<TMPL_ELSE>activate</TMPL_IF></a></td></TMPL_IF>
+<TMPL_IF domain_delete>	<td align="center"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=<TMPL_IF domlist>deldom<TMPL_ELSE>delrevzone</TMPL_IF>&amp;id=<TMPL_VAR NAME=domainid>"><img src="images/trash2.png" alt="[ Delete ]" /></a></td></TMPL_IF>
 </tr>
 </TMPL_LOOP>
 <TMPL_ELSE>
-<tr><td colspan="5" align="center">No domains found</td></tr>
+<tr><td colspan="5" align="center">No <TMPL_IF domlist>domains<TMPL_ELSE>reverse zones</TMPL_IF> found</td></tr>
 </TMPL_IF>
 </table>
Index: branches/stable/templates/fpnla.tmpl
===================================================================
--- branches/stable/templates/fpnla.tmpl	(revision 535)
+++ branches/stable/templates/fpnla.tmpl	(revision 544)
@@ -1,5 +1,5 @@
-<TMPL_IF navfirst><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=<TMPL_VAR NAME=curpage>&amp;offset=0<TMPL_IF id>&amp;id=<TMPL_VAR NAME=id></TMPL_IF><TMPL_IF defrec>&amp;defrec=<TMPL_VAR NAME=defrec></TMPL_IF>"><img src="images/frev.png" alt="[ First ]" />First</a><TMPL_ELSE><img src="images/frev.png" alt="[ First ]" />First</TMPL_IF>&nbsp;
-<TMPL_IF navprev><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=<TMPL_VAR NAME=curpage>&amp;offset=<TMPL_VAR NAME=prevoffs><TMPL_IF id>&amp;id=<TMPL_VAR NAME=id></TMPL_IF><TMPL_IF defrec>&defrec=<TMPL_VAR NAME=defrec></TMPL_IF>"><img src="images/rev.png" alt="[ Previous ]" />Previous</a><TMPL_ELSE><img src="images/rev.png" alt="[ Previous ]" />Previous</TMPL_IF>&nbsp;
-<TMPL_IF navnext><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=<TMPL_VAR NAME=curpage>&amp;offset=<TMPL_VAR NAME=nextoffs><TMPL_IF id>&amp;id=<TMPL_VAR NAME=id></TMPL_IF><TMPL_IF defrec>&amp;defrec=<TMPL_VAR NAME=defrec></TMPL_IF>">Next<img src="images/fwd.png" alt="[ Next ]" /></a><TMPL_ELSE>Next<img src="images/fwd.png" alt="[ Next ]" /></TMPL_IF>&nbsp;
-<TMPL_IF navlast><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=<TMPL_VAR NAME=curpage>&amp;offset=<TMPL_VAR NAME=lastoffs><TMPL_IF id>&amp;id=<TMPL_VAR NAME=id></TMPL_IF><TMPL_IF defrec>&amp;defrec=<TMPL_VAR NAME=defrec></TMPL_IF>">Last<img src="images/ffwd.png" alt="[ Last ]" /></a><TMPL_ELSE>Last<img src="images/ffwd.png" alt="[ Last ]" /></TMPL_IF>&nbsp;
-<TMPL_IF navall><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=<TMPL_VAR NAME=curpage>&amp;offset=all<TMPL_IF id>&amp;id=<TMPL_VAR NAME=id></TMPL_IF><TMPL_IF defrec>&amp;defrec=<TMPL_VAR NAME=defrec></TMPL_IF>">All</a><TMPL_ELSE><TMPL_UNLESS onepage><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=<TMPL_VAR NAME=curpage>&amp;offset=0<TMPL_IF id>&amp;id=<TMPL_VAR NAME=id></TMPL_IF><TMPL_IF defrec>&amp;defrec=<TMPL_VAR NAME=defrec></TMPL_IF>"><TMPL_VAR NAME=perpage> per page</a></TMPL_UNLESS></TMPL_IF>
+<TMPL_IF navfirst><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=<TMPL_VAR NAME=curpage>&amp;offset=0<TMPL_IF id>&amp;id=<TMPL_VAR NAME=id></TMPL_IF><TMPL_IF defrec>&amp;defrec=<TMPL_VAR NAME=defrec></TMPL_IF><TMPL_IF revrec>&amp;revrec=<TMPL_VAR NAME=revrec></TMPL_IF>"><img src="images/frev.png" alt="[ First ]" />First</a><TMPL_ELSE><img src="images/frev.png" alt="[ First ]" />First</TMPL_IF>&nbsp;
+<TMPL_IF navprev><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=<TMPL_VAR NAME=curpage>&amp;offset=<TMPL_VAR NAME=prevoffs><TMPL_IF id>&amp;id=<TMPL_VAR NAME=id></TMPL_IF><TMPL_IF defrec>&defrec=<TMPL_VAR NAME=defrec></TMPL_IF><TMPL_IF revrec>&amp;revrec=<TMPL_VAR NAME=revrec></TMPL_IF>"><img src="images/rev.png" alt="[ Previous ]" />Previous</a><TMPL_ELSE><img src="images/rev.png" alt="[ Previous ]" />Previous</TMPL_IF>&nbsp;
+<TMPL_IF navnext><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=<TMPL_VAR NAME=curpage>&amp;offset=<TMPL_VAR NAME=nextoffs><TMPL_IF id>&amp;id=<TMPL_VAR NAME=id></TMPL_IF><TMPL_IF defrec>&amp;defrec=<TMPL_VAR NAME=defrec></TMPL_IF><TMPL_IF revrec>&amp;revrec=<TMPL_VAR NAME=revrec></TMPL_IF>">Next<img src="images/fwd.png" alt="[ Next ]" /></a><TMPL_ELSE>Next<img src="images/fwd.png" alt="[ Next ]" /></TMPL_IF>&nbsp;
+<TMPL_IF navlast><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=<TMPL_VAR NAME=curpage>&amp;offset=<TMPL_VAR NAME=lastoffs><TMPL_IF id>&amp;id=<TMPL_VAR NAME=id></TMPL_IF><TMPL_IF defrec>&amp;defrec=<TMPL_VAR NAME=defrec></TMPL_IF><TMPL_IF revrec>&amp;revrec=<TMPL_VAR NAME=revrec></TMPL_IF>">Last<img src="images/ffwd.png" alt="[ Last ]" /></a><TMPL_ELSE>Last<img src="images/ffwd.png" alt="[ Last ]" /></TMPL_IF>&nbsp;
+<TMPL_IF navall><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=<TMPL_VAR NAME=curpage>&amp;offset=all<TMPL_IF id>&amp;id=<TMPL_VAR NAME=id></TMPL_IF><TMPL_IF defrec>&amp;defrec=<TMPL_VAR NAME=defrec></TMPL_IF><TMPL_IF revrec>&amp;revrec=<TMPL_VAR NAME=revrec></TMPL_IF>">All</a><TMPL_ELSE><TMPL_UNLESS onepage><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=<TMPL_VAR NAME=curpage>&amp;offset=0<TMPL_IF id>&amp;id=<TMPL_VAR NAME=id></TMPL_IF><TMPL_IF defrec>&amp;defrec=<TMPL_VAR NAME=defrec></TMPL_IF><TMPL_IF revrec>&amp;revrec=<TMPL_VARNAME=revrec></TMPL_IF>"><TMPL_VAR NAME=perpage> per page</a></TMPL_UNLESS></TMPL_IF>
Index: branches/stable/templates/menu.tmpl
===================================================================
--- branches/stable/templates/menu.tmpl	(revision 535)
+++ branches/stable/templates/menu.tmpl	(revision 544)
@@ -4,7 +4,9 @@
 <hr />
 <a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=domlist">Domains</a><br />
+<TMPL_IF mayrdns><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=revzones">Reverse Zones</a><br /></TMPL_IF>
 <a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=useradmin">Users</a><br />
 <a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=log">Log</a><br />
-<TMPL_IF maydefrec><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=reclist&amp;id=<TMPL_VAR NAME=group>&amp;defrec=y">Default Records</a><br /></TMPL_IF>
+<TMPL_IF maydefrec><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=reclist&amp;id=<TMPL_VAR NAME=group>&amp;defrec=y">Default Records</a><br />
+<TMPL_IF mayrdns><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=reclist&amp;id=<TMPL_VAR NAME=group>&amp;defrec=y&amp;revrec=y">Default Reverse Records</a><br /></TMPL_IF></TMPL_IF>
 <TMPL_IF mayimport><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=axfr">AXFR Import</a><br /></TMPL_IF>
 <TMPL_IF maybulk><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=bulkdomain">Bulk Domain Operations</a><br /></TMPL_IF>
Index: branches/stable/templates/newrevzone.tmpl
===================================================================
--- branches/stable/templates/newrevzone.tmpl	(revision 544)
+++ branches/stable/templates/newrevzone.tmpl	(revision 544)
@@ -0,0 +1,49 @@
+<!-- <TMPL_VAR NAME=sid> -->
+<table class="wholepage"><tr>
+<TMPL_INCLUDE NAME="menu.tmpl">
+
+<td align="center" valign="top">
+
+<form action="dns.cgi">
+<fieldset>
+
+<input type="hidden" name="sid" value="<TMPL_VAR NAME=sid>" />
+<input type="hidden" name="page" value="addrevzone" />
+<input type="hidden" name="newrevzone" value="yes" />
+
+<table class="container" width="450">
+<tr><td>
+    <table border="0" cellspacing="2" cellpadding="2" width="100%">
+<TMPL_IF add_failed>	<tr><td class="errhead" colspan="2">Error adding reverse zone <TMPL_VAR NAME=revzone>: 
+<TMPL_VAR NAME=errmsg></td></tr></TMPL_IF>
+	<tr class="darkrowheader"><td colspan="2" align="center">Add Reverse Zone</td></tr>
+
+	<tr class="datalinelight">
+		<td>CIDR network or reverse zone name:</td>
+		<td align="left"><input type="text" name="revzone" value="<TMPL_VAR NAME=revzone>" /></td>
+	</tr>
+	<tr class="datalinelight">
+		<td>Default reverse hostname pattern:</td>
+		<td align="left"><input type="text" name="revpatt" value="<TMPL_VAR NAME=revpatt>" /></td>
+	</tr>
+	<tr class="datalinelight">
+		<td>Add reverse zone to group:</td>
+		<td><select name="group">
+<TMPL_LOOP name=grouplist>		<option value="<TMPL_VAR NAME=groupval>"<TMPL_IF groupactive> selected="selected"</TMPL_IF>><TMPL_VAR name=groupname></option>
+</TMPL_LOOP>
+		</select></td>
+	</tr>
+	<tr class="datalinelight">
+		<td>Make reverse zone active on next DNS propagation</td><td><input type="checkbox" name="makeactive" checked="checked" /></td>
+	</tr>
+	<tr><td colspan="2" class="tblsubmit"><input type="submit" value="Add reverse zone" /></td></tr>
+    </table>
+    </td>
+</tr>
+</table>
+
+</fieldset>
+</form>
+
+</td></tr>
+</table>
Index: branches/stable/templates/reclist.tmpl
===================================================================
--- branches/stable/templates/reclist.tmpl	(revision 535)
+++ branches/stable/templates/reclist.tmpl	(revision 544)
@@ -8,6 +8,9 @@
 <div class="result"><TMPL_VAR NAME=resultmsg></div>
 </TMPL_IF>
+<TMPL_IF warnmsg>
+<div class="warning"><TMPL_VAR NAME=warnmsg></div>
+</TMPL_IF>
 <TMPL_IF errmsg>
-<div class='errmsg'><TMPL_VAR NAME=errmsg></div>
+<div class="errmsg"><TMPL_VAR NAME=errmsg></div>
 </TMPL_IF>
 
@@ -37,4 +40,5 @@
 		<input type="hidden" name="id" value="<TMPL_VAR NAME=id>" />
 		<input type="hidden" name="defrec" value="<TMPL_VAR NAME=defrec>" />
+		<input type="hidden" name="revrec" value="<TMPL_VAR NAME=revrec>" />
 		<input name="filter"<TMPL_IF filter> value="<TMPL_VAR NAME=filter>"</TMPL_IF> />
 		<input type="submit" value="Filter" />
@@ -46,6 +50,6 @@
 	<td colspan="3">Records</td>
 	<td align="center"><a href="textrecs.cgi?sid=<TMPL_VAR NAME=sid>&amp;id=<TMPL_VAR NAME=id>&amp;defrec=<TMPL_VAR NAME=defrec>">Plain text</a></td>
-<TMPL_IF record_create>	<td align="right"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=record&amp;parentid=<TMPL_VAR NAME=id>&amp;defrec=<TMPL_VAR NAME=defrec>&amp;recact=new">Add record</a></td></TMPL_IF>
-	<td align="right"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=log&amp;id=<TMPL_VAR NAME=id><TMPL_IF logdom>&amp;ltype=dom</TMPL_IF>">View log</a></td>
+<TMPL_IF record_create>	<td align="right"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=record&amp;parentid=<TMPL_VAR NAME=id>&amp;defrec=<TMPL_VAR NAME=defrec>&amp;revrec=<TMPL_VAR NAME=revrec>&amp;recact=new">Add record</a></td></TMPL_IF>
+	<td align="right"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=log&amp;id=<TMPL_VAR NAME=id><TMPL_IF logdom>&amp;ltype=dom</TMPL_IF><TMPL_IF logrdns>&amp;ltype=rdns</TMPL_IF>">View log</a></td>
 </tr>
 
@@ -59,11 +63,13 @@
  NAME=offset>&amp;offset=<TMPL_VAR NAME=offset></TMPL_IF>&amp;sortby=<TMPL_VAR
  NAME=sortby>&amp;order=<TMPL_VAR NAME=order>&amp;id=<TMPL_VAR NAME=id>&amp;defrec=<TMPL_VAR
- NAME=defrec>"><TMPL_VAR NAME=colname></a><TMPL_IF NAME=sortorder>&nbsp;<img alt="<TMPL_VAR
- NAME=sortorder>" src="images/<TMPL_VAR NAME=sortorder>.png" /></TMPL_IF></td></TMPL_LOOP>
+ NAME=defrec>&amp;revrec=<TMPL_VAR NAME=revrec>"><TMPL_VAR NAME=colname></a><TMPL_IF
+ NAME=sortorder>&nbsp;<img alt="<TMPL_VAR NAME=sortorder>" src="images/<TMPL_VAR NAME=sortorder>.png"
+ /></TMPL_IF></td></TMPL_LOOP>
 <TMPL_IF record_delete>	<td>Delete</td></TMPL_IF>
 </tr>
 <TMPL_LOOP NAME=reclist>
 <tr class="row<TMPL_VAR NAME=row>">
-	<td><TMPL_IF record_edit><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=record&amp;parentid=<TMPL_VAR NAME=id>&amp;defrec=<TMPL_VAR NAME=defrec>&amp;recact=edit&amp;id=<TMPL_VAR NAME=record_id>"><TMPL_VAR NAME=host></a><TMPL_ELSE><TMPL_VAR NAME=host></TMPL_IF></td>
+<TMPL_IF fwdzone>
+	<td><TMPL_IF record_edit><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=record&amp;parentid=<TMPL_VAR NAME=id>&amp;defrec=<TMPL_VAR NAME=defrec>&amp;revrec=<TMPL_VAR NAME=revrec>&amp;recact=edit&amp;id=<TMPL_VAR NAME=record_id>"><TMPL_VAR NAME=host></a><TMPL_ELSE><TMPL_VAR NAME=host></TMPL_IF></td>
 	<td><TMPL_VAR NAME=type></td>
 	<td><TMPL_VAR NAME=val></td>
@@ -71,6 +77,11 @@
 	<td><TMPL_VAR NAME=weight></td>
 	<td><TMPL_VAR NAME=port></td>
+<TMPL_ELSE>
+	<td><TMPL_IF record_edit><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=record&amp;parentid=<TMPL_VAR NAME=id>&amp;defrec=<TMPL_VAR NAME=defrec>&amp;revrec=<TMPL_VAR NAME=revrec>&amp;recact=edit&amp;id=<TMPL_VAR NAME=record_id>"><TMPL_VAR NAME=val></a><TMPL_ELSE><TMPL_VAR NAME=val></TMPL_IF></td>
+	<td><TMPL_VAR NAME=type></td>
+	<td><TMPL_VAR NAME=host></td>
+</TMPL_IF>
 	<td><TMPL_VAR NAME=ttl></td>
-<TMPL_IF record_delete>	<td align="center"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=delrec&amp;id=<TMPL_VAR NAME=record_id>&amp;defrec=<TMPL_VAR NAME=defrec>&amp;parentid=<TMPL_VAR NAME=id>"><img src="images/trash2.png" alt="[ Delete ]" /></a></td></TMPL_IF>
+<TMPL_IF record_delete>	<td align="center"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&amp;page=delrec&amp;id=<TMPL_VAR NAME=record_id>&amp;defrec=<TMPL_VAR NAME=defrec>&amp;revrec=<TMPL_VAR NAME=revrec>&amp;parentid=<TMPL_VAR NAME=id>"><img src="images/trash2.png" alt="[ Delete ]" /></a></td></TMPL_IF>
 </tr>
 </TMPL_LOOP>
Index: branches/stable/templates/record.tmpl
===================================================================
--- branches/stable/templates/record.tmpl	(revision 535)
+++ branches/stable/templates/record.tmpl	(revision 544)
@@ -14,4 +14,5 @@
 <input type="hidden" name="page" value="record" />
 <input type="hidden" name="defrec" value="<TMPL_VAR NAME=defrec>" />
+<input type="hidden" name="revrec" value="<TMPL_VAR NAME=revrec>" />
 <input type="hidden" name="sid" value="<TMPL_VAR NAME=sid>" />
 <input type="hidden" name="parentid" value="<TMPL_VAR NAME=parentid>" />
@@ -23,9 +24,14 @@
 
     <table border="0" cellspacing="2" cellpadding="2" width="100%">
-<TMPL_IF failed>	<tr><td class="errhead" colspan="2">Error <TMPL_VAR NAME=wastrying> record: <TMPL_VARNAME=errmsg></td></tr></TMPL_IF>
+<TMPL_IF failed>	<tr><td class="errhead" colspan="2">Error <TMPL_VAR NAME=wastrying> record: <TMPL_VAR NAME=errmsg></td></tr></TMPL_IF>
 	<tr class="tableheader"><td align="center" colspan="2"><TMPL_VAR NAME=todo>: <TMPL_VAR NAME=dohere></td></tr>
 	<tr class="datalinelight">
+<TMPL_IF fwdzone>
 		<td>Hostname</td>
 		<td><input type="text" name="name" value="<TMPL_VAR NAME=name>" /></td>
+<TMPL_ELSE>
+		<td>IP Address</td>
+		<td><input type="text" name="address" value="<TMPL_VAR ESCAPE=HTML NAME=address>" /></td>
+</TMPL_IF>
 	</tr>
 	<tr class="datalinelight">
@@ -38,7 +44,13 @@
 	</tr>
 	<tr class="datalinelight">
+<TMPL_IF fwdzone>
 		<td>Address</td>
 		<td><input type="text" name="address" value="<TMPL_VAR ESCAPE=HTML NAME=address>" /></td>
+<TMPL_ELSE>
+		<td>Hostname</td>
+		<td><input type="text" name="name" value="<TMPL_VAR NAME=name>" /></td>
+</TMPL_IF>
 	</tr>
+<TMPL_IF fwdzone>
 	<tr class="datalinelight">
 		<td>Distance (MX and SRV only)</td>
@@ -53,4 +65,5 @@
 		<td><input type="text" name="port" value="<TMPL_VAR NAME=port>" size="5" maxlength="10" /></td>
 	</tr>
+</TMPL_IF>
 	<tr class="datalinelight">
 		<td>TTL</td>
Index: branches/stable/templates/revzones.tmpl
===================================================================
--- branches/stable/templates/revzones.tmpl	(revision 544)
+++ branches/stable/templates/revzones.tmpl	(revision 544)
@@ -0,0 +1,1 @@
+<TMPL_INCLUDE domlist.tmpl>
