Index: branches/stable/DNSDB.pm
===================================================================
--- branches/stable/DNSDB.pm	(revision 652)
+++ branches/stable/DNSDB.pm	(revision 690)
@@ -517,5 +517,5 @@
     # Fail on "compact" IPv4 variants, because they are not consistent and predictable.
     return ('FAIL',"A record must be a valid IPv4 address")
-	unless ${$args{host}} =~ /^\d+\.\d+\.\d+\.\d+$/;
+	unless ${$args{host}} =~ m{^\d+\.\d+\.\d+\.\d+(/\d+)?$};
     $args{addr} = new NetAddr::IP ${$args{host}};
     return ('FAIL',"A record must be a valid IPv4 address")
@@ -533,5 +533,5 @@
     # or the intended parent domain for live records.
     my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
-    ${$args{host}} =~ s/\.*$/\.$pname/ if (${$args{host}} ne '@' && ${$args{host}} !~ /$pname$/);
+    ${$args{host}} =~ s/\.*$/\.$pname/ if (${$args{host}} ne '@' && ${$args{host}} !~ /$pname$/i);
 
     # Check if it's a proper formal .arpa name for an IP, and renormalize it to the IP
@@ -597,5 +597,5 @@
       # Forcibly append the domain name if the hostname being added does not end with the current domain name
       my $pname = $self->domainName($args{id});
-      ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
+      ${$args{host}} =~ s/\.*$/\.$pname/ if (${$args{host}} ne '@' && ${$args{host}} !~ /$pname$/i);
     }
   } else {
@@ -638,4 +638,5 @@
         my $tmpz = _ZONE($revzone, 'ZONE', 'r', '.');
         return ('FAIL', "The bare zone may not be a CNAME") if $tmphn eq $tmpz;
+##enhance: look up the target name and publish that instead on export
       }
     }
@@ -653,11 +654,14 @@
       if ${$args{val}} =~ /^(?:[\d.]+|[0-9a-fA-F:]+)$/;
 
+    # Make sure target is a well-formed hostname
+    return ('FAIL', $errstr) if ! _check_hostname_form(${$args{val}}, ${$args{rectype}}, $args{defrec}, $args{revrec});
+
     # Forcibly append the domain name if the hostname being added does not end with the current domain name
     my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
-    ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
+    ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/i;
 
     # CNAMEs can not be used for parent nodes;  just leaf nodes with no other record types
     # Enforce this for the zone name
-    return ('FAIL', "The bare zone name may not be a CNAME") if ${$args{host}} eq $pname;
+    return ('FAIL', "The bare zone name may not be a CNAME") if ${$args{host}} eq $pname || ${$args{host}} =~ /^\@/;
 
 ##enhance:  Look up the passed value to see if it exists.  Ooo, fancy.
@@ -738,9 +742,12 @@
       }
       # Validate PTR target for form.
-      return ('FAIL', $errstr) if ! _check_hostname_form(${$args{host}}, ${$args{rectype}}, $args{defrec}, $args{revrec});
+      # %blank% skips the IP when expanding a template record
+      return ('FAIL', $errstr)
+	unless _check_hostname_form(${$args{host}}, ${$args{rectype}}, $args{defrec}, $args{revrec}) ||
+		lc(${$args{host}}) eq '%blank%';
     } else { # revrec ne 'y'
       # Fetch the domain and append if the passed hostname isn't within it.
       my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
-      ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
+      ${$args{host}} =~ s/\.*$/\.$pname/ if (${$args{host}} ne '@' && ${$args{host}} !~ /$pname$/i);
       # Validate hostname and target for form
       return ('FAIL', $errstr) if ! _check_hostname_form(${$args{host}}, ${$args{rectype}}, $args{defrec}, $args{revrec});
@@ -780,6 +787,7 @@
     } else {
       # New record.  Always warn if a PTR exists
+      # Don't warn when a matching A record exists tho
       my ($ptrcount) = $dbh->selectrow_array("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec}).
-	" WHERE $hostcol = ?", undef, ($checkme));
+	" WHERE $hostcol = ? AND (type=12 OR type=65280 OR type=65281)", undef, ($checkme));
       $warnflag .= "PTR record for $checkme already exists;  adding another will probably not do what you want"
 	if $ptrcount;
@@ -817,5 +825,6 @@
     # or the intended parent domain for live records.
     my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
-    ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
+    ${$args{host}} =~ s/\.*$/\.$pname/ if (${$args{host}} ne '@' && ${$args{host}} !~ /$pname$/i);
+    return ('FAIL', $errstr) if ! _check_hostname_form(${$args{host}}, ${$args{rectype}}, $args{defrec}, $args{revrec});
   } else {
     # MX target check - IP addresses not allowed.  Must be a more or less well-formed hostname.
@@ -855,5 +864,5 @@
     # or the intended parent domain for live records.
     my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
-    ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
+    ${$args{host}} =~ s/\.*$/\.$pname/ if (${$args{host}} ne '@' && ${$args{host}} !~ /$pname$/i);
     return ('FAIL', $errstr) if ! _check_hostname_form(${$args{host}}, ${$args{rectype}}, $args{defrec}, $args{revrec});
   } else {
@@ -924,5 +933,5 @@
     # or the intended parent domain for live records.
     my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
-    ${$args{host}} =~ s/\.*$/\.$pname/ if (${$args{host}} ne '@' && ${$args{host}} !~ /$pname$/);
+    ${$args{host}} =~ s/\.*$/\.$pname/ if (${$args{host}} ne '@' && ${$args{host}} !~ /$pname$/i);
 
     # Check if it's a proper formal .arpa name for an IP, and renormalize it to the IP
@@ -956,5 +965,5 @@
 
 # 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', 'Reverse zones cannot contain SRV records') if $args{revrec} eq 'y';
 
   # Key additional record parts.  Always required.
@@ -974,5 +983,5 @@
     # or the intended parent domain for live records.
     my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
-    ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
+    ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/i;
 
 ##enhance:  Rejig so that we can pass back a WARN red flag, instead of
@@ -1042,7 +1051,7 @@
     # but that gets stupid in forward zones, since these records are shared.
     return ('FAIL', "$typemap{${$args{rectype}}} record must be a valid IPv4 address")
-      if ${$args{rectype}} == 65280 && ${$args{val}} !~ /^\d+\.\d+\.\d+\.\d+$/;
+      if ${$args{rectype}} == 65280 && ${$args{val}} !~ m{^\d+\.\d+\.\d+\.\d+(?:/\d+)?$};
     return ('FAIL', "$typemap{${$args{rectype}}} record must be a valid IPv6 address")
-      if ${$args{rectype}} == 65281 && ${$args{val}} !~ /^[a-fA-F0-9:]+$/;
+      if ${$args{rectype}} == 65281 && ${$args{val}} !~ m{^[a-fA-F0-9:]+(?:/\d+)?$};
     # If things are not OK, this should prevent Stupid in the error log.
     $args{addr} = new NetAddr::IP ${$args{val}}
@@ -1115,5 +1124,11 @@
       ${$args{fields}} .= "rdns_id,";
       push @{$args{vallist}}, $revid;
-    }
+
+      # Coerce the hostname back to the domain;  this is so it displays and manipulates
+      # sanely in the reverse zone.
+      if (${$args{host}} eq '@') {
+        ${$args{host}} = $self->domainName($args{id});  # errors?  What errors?
+      }
+    } # revrec ne 'y'
 
   } else {	# defrec eq 'y'
@@ -1243,7 +1258,13 @@
   if ($args{defrec} eq 'n') {
     if ($args{revrec} eq 'n') {
-      ($code,$msg) = $self->_validate_1(%args) if ${$args{rectype}} == 65280;
-      ($code,$msg) = $self->_validate_28(%args) if ${$args{rectype}} == 65281;
-      return ($code,$msg) if $code eq 'FAIL';
+
+      # Coerce all hostnames to end in ".DOMAIN" for group/default records,
+      # or the intended parent domain for live records.
+      my $pname = $self->domainName($args{id});
+      ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/i;
+
+      # check for form;  note this checks both normal and "other" hostnames.
+      return ('FAIL', $errstr)
+	if !_check_hostname_form(${$args{host}}, ${$args{rectype}}, $args{defrec}, $args{revrec});
 
       # Check if the requested reverse zone exists - note, an IP fragment won't
@@ -1295,6 +1316,15 @@
 # AAAA+PTR template record
 # Not sure this can be handled sanely due to the size of IPv6 address space
+# _validate_65283 above should handle v6 template records fine.  It's on export we've got trouble.
 sub _validate_65284 {
-  return ('OK','OK');
+  my $self = shift;
+  my %args = @_;
+
+  # do a quick check on the form of the hostname part;  this is effectively a
+  # "*.0.0.f.ip6.arpa" hostname, not an actual expandable IP template pattern
+  # like with 65283.
+  return ('FAIL', $errstr)
+	if !_check_hostname_form(${$args{host}}, ${$args{rectype}}, $args{defrec}, $args{revrec});
+  return $self->_validate_65283(%args);
 } # done AAAA+PTR template record
 
@@ -1323,5 +1353,5 @@
     } else {
       my $pname = $self->domainName($args{id});
-      ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
+      ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/i;
     }
   } else {
@@ -1573,8 +1603,26 @@
 # really have a sane way to handle this type of expansion at the moment
 # due to the size of the address space.
-# Takes a reference to a template string to be expanded, and an IP to use in the replacement.
+# Takes a reference to a template string to be expanded, an IP to use in the replacement,
+# an optional netblock for the %ngb (net, gw, bcast) expansion, and an optional index
+# number for %c "n'th usable IP in block/range" patterns.
+# Alters the template string referred.
 sub _template4_expand {
+  # ugh pthui
+  my $self;
+  $self = shift if ref($_[0]) eq 'DNSDB';
+
   my $tmpl = shift;
   my $ip = shift;
+  my $subnet = shift;   # for %ngb and %c
+  my $ipindex = shift;  # for %c
+
+  # blank $tmpl on config template_skip_0 or template_skip_255, unless we have a %ngb
+  if ($$tmpl !~ /\%-?n-?g-?b\%/) {
+    if ( ($ip =~ /\.0$/ && $self->{template_skip_0}) ||
+         ($ip =~ /\.255$/ && $self->{template_skip_255}) ) {
+      $$tmpl = '';
+      return;
+    }
+  }
 
   my @ipparts = split /\./, $ip;
@@ -1585,4 +1633,101 @@
     push @ippad, sprintf("%0.3u", $_);
   }
+
+  # Two or three consecutive separator characters (_ or -) should be rare - users that use them
+  # anywhere other than punycoded internationalized domains get to keep the pieces when it breaks.
+  # We clean up the ones that we may inadvertently generate after replacing %c and %ngb%
+  my ($thrsep) = ($$tmpl =~ /[_-]{3}/);
+  my ($twosep) = ($$tmpl =~ /[_-]{2}/);
+
+  # Take the simplest path to pattern substitution;  replace only exactly the %c or %ngb%
+  # patterns as-is.  Then check after to see if we've caused doubled separator characters (- or _)
+  # and eliminate them, but only if the original template didn't have them already.  Also
+  # unconditionally drop separator characters immediately before a dot;  these do not always
+  # strictly make the label invalid but almost always, and any exceptions should never show up
+  # in a template record that expands to "many" real records anyway.
+
+  # %ngb and %c require a netblock
+  if ($subnet) {
+    # extract the fragments
+    my ($ngb,$n,$g,$b) = ($$tmpl =~ /(\%(-?n)(-?g)(-?b)\%)/);
+    my ($c) = ($$tmpl =~ /(\%-?c)/);  my $nld = '';  my $cld = '';
+    $c = '' if !$c;
+    my $skipgw = ($c =~ /\%-c/ ? 0 : 1);
+    my $ipkill = 0;
+
+##fixme: still have one edge case not handled well:
+# %c%n-gb%
+# do we drop the record as per -g, or publish the record with an index of 1 as per %c?
+# arguably this is a "that's a STUPID question!" case
+
+    if ($c) {
+      # "n'th usable IP in the block" pattern.  We need the caller to provide an index
+      # number otherwise we see exponential time growth because we have to iterate over
+      # the whole block to map the IP back to an index.  :/
+      # NetAddr::IP does not have a method for asking "what index is IP <foo> at?"
+
+      # no index, or index == 0, (AKA network address), or IP == broadcast, blank the index fragment
+      if (!$ipindex || ($$subnet->broadcast->addr eq $ip)) {
+        $$tmpl =~ s/$c//;
+      } else {
+        # if we have %c, AKA "skip the gateway", and we're on the nominal gateway IP, blank the index fragment
+        if ($skipgw && $$subnet->first->addr eq $ip) {
+          $$tmpl =~ s/$c//;
+        }
+        # else replace the index fragment with the passed index minus $skipgw, so that we can start the
+        # resulting index at 1 on net+2
+        else {
+          $$tmpl =~ s/$c/($ipindex-$skipgw)/e;
+        }
+      }
+    } # if ($c)
+
+    if ($ngb) {
+      # individually check the network, standard gateway (net+1) IP, and broadcast IP
+      # blank $$tmpl if n, g, or b was prefixed with - (this allows "hiding" net/gw/bcast entries)
+
+      if ($$subnet->network->addr eq $ip) {
+        if ($n eq '-n') {
+          $$tmpl = '';
+        } else {
+          $$tmpl =~ s/$ngb/net/;
+          $ipkill = 1;
+        }
+      } elsif ($$subnet->first->addr eq $ip) {
+        if ($g eq '-g') {
+          $$tmpl = '';
+        } else {
+          $$tmpl =~ s/$ngb/gw/;
+          $ipkill = 1;
+        }
+      } elsif ($$subnet->broadcast->addr eq $ip) {
+        if ($b eq '-b') {
+          $$tmpl = '';
+        } else {
+          $$tmpl =~ s/$ngb/bcast/;
+          $ipkill = 1;
+        }
+      } else {
+        $$tmpl =~ s/$ngb//;
+      }
+    }
+
+    # We don't (usually) want to expand the IP-related patterns on the -net, -gw, or -bcast IPs.
+    # Arguably this is another place for another config knob, or possibly further extension of
+    # the template pattern to control it on a per-subnet basis.
+    if ($ipkill) {
+      # kill common IP patterns
+      $$tmpl =~ s/\%[_.-]?[irdh]//;
+      # kill IP octet patterns
+      $$tmpl =~ s/\%[1234][dh0](?:[_.-]\%[1234][dh0]){0,3}//;
+    }
+
+    # and now clean up to make sure we leave a valid DNS label... mostly.  Should arguably
+    # split on /\./ and process each label separately.
+    $$tmpl =~ s/([_-]){3}/$1/ if !$thrsep;
+    $$tmpl =~ s/([_-]){2}/$1/ if !$twosep;
+    $$tmpl =~ s/[_-]\././;
+
+  } # if ($subnet)
 
   # IP substitutions in template records:
@@ -1620,4 +1765,5 @@
   $$tmpl =~ s/\%([1234])h/$iphex[$1-1]/g;
   $$tmpl =~ s/\%([1234])0/$ippad[$1-1]/g;
+
 } # _template4_expand()
 
@@ -1631,10 +1777,10 @@
     my $tmphost = $hname;
     # we don't actually need to test with the real IP passed;  that saves a bit of fiddling.
-    _template4_expand(\$tmphost, '10.10.10.10');
-    if ($tmphost =~ /\%/) {
+    DNSDB::_template4_expand(\$tmphost, '10.10.10.10');
+    if ($tmphost =~ /\%/ || lc($tmphost) !~ /^(?:\*\.)?(?:[0-9a-z_.-]+)$/) {
       $errstr = "Invalid template $hname";
       return;
     }
-  } elsif ($rectype == $reverse_typemap{CNAME}) {
+  } elsif ($rectype == $reverse_typemap{CNAME} && $revrec eq 'y') {
     # Allow / in reverse CNAME hostnames for sub-/24 delegation
     if (lc($hname) !~ m|^[0-9a-z_./-]+$|) {
@@ -2280,4 +2426,6 @@
   local $dbh->{RaiseError} = 1;
 
+  return ('FAIL', 'Need a zone identifier to look up') if !$zoneid;
+
   my $msg = '';
   my $failmsg = '';
@@ -3989,4 +4137,7 @@
   my %args = @_;
 
+  $args{revrec} = 'n' if !$args{revrec};
+  $args{defrec} = 'n' if !$args{defrec};
+
   # protection against bad or missing arguments
   $args{sortorder} = 'ASC' if !$args{sortorder} || !grep /^$args{sortorder}$/, ('ASC','DESC');
@@ -4000,22 +4151,11 @@
   my $perpage = ($args{nrecs} ? $args{nrecs} : $self->{perpage});
 
-  # sort reverse zones on IP, correctly
-  # do other fiddling with $args{sortby} while we're at it.
-  # whee!  multisort means just passing comma-separated fields in sortby!
-  my $newsort = '';
-  foreach my $sf (split /,/, $args{sortby}) {
-    $sf = "r.$sf";
-    $sf =~ s/r\.val/inetlazy(r.val)/
-	if $args{revrec} eq 'y' && $args{defrec} eq 'n';
-    $sf =~ s/r\.type/t.alphaorder/;
-    $newsort .= ",$sf";
-  }
-  $newsort =~ s/^,//;
-
-  my @bindvars = ($args{id});
-  push @bindvars, ($args{filter},$args{filter}) if $args{filter};
 
 ##fixme:  do we need a knob to twist to switch from unix epoch to postgres time string?
-  my $sql = "SELECT r.record_id,r.host,r.type,r.val,r.ttl";
+  my @bindvars;
+  my $sql = "SELECT r.record_id,";
+  # only include the parent info if we don't already know which parent we're in
+  $sql .= "r.domain_id,r.rdns_id," unless $args{id};
+  $sql .= "r.host,r.type,r.val,r.ttl";
   $sql .= ",l.description AS locname,stamp,r.stamp < now() AS ispast,r.expires,r.stampactive"
 	if $args{defrec} eq 'n';
@@ -4024,14 +4164,46 @@
   $sql .= "INNER JOIN rectypes t ON r.type=t.val ";	# for sorting by type alphabetically
   $sql .= "LEFT JOIN locations l ON r.location=l.location " if $args{defrec} eq 'n';
-  $sql .= "WHERE "._recparent($args{defrec},$args{revrec})." = ?";
-  $sql .= " AND NOT r.type=$reverse_typemap{SOA}";
+  $sql .= "WHERE NOT r.type=$reverse_typemap{SOA}";
+
+  # "normal" record list
+  if ($args{id}) {
+    $sql .= " AND "._recparent($args{defrec},$args{revrec})." = ?";
+    push @bindvars, $args{id};
+  }
+
+  # Filtering on host/val (mainly normal record list)
   if ($args{filter}) {
     $sql .= " AND (r.host ~* ? OR r.val ~* ? OR r.host ~* ? OR r.val ~* ?)";
     my $tmp = join('.',reverse(split(/\./,$args{filter})));
+    push @bindvars, ($args{filter},$args{filter});
     push @bindvars, ($tmp, $tmp);
   }
+
+  # Filtering on other fields
+  foreach (qw(type distance weight port ttl description)) {
+    if ($args{$_}) {
+      $sql .= " AND $_ ~* ?";
+      push @bindvars, $args{$_};
+    }
+  }
+
+  # whee!  multisort means just passing comma-separated fields in sortby!
+  my $newsort = '';
+  foreach my $sf (split /,/, $args{sortby}) {
+    $sf = "r.$sf";
+    # sort on IP, correctly
+    $sf =~ s/r\.val/inetlazy(r.val)/;
+# hmm.  do we really need to limit this?
+#	if $args{revrec} eq 'y' && $args{defrec} eq 'n';
+    $sf =~ s/r\.type/t.alphaorder/;  # subtly different from sorting on rectypes.name
+    $newsort .= ",$sf";
+  }
+  $newsort =~ s/^,//;
+##enhance: pass in ascending/descending sort per-field
   $sql .= " ORDER BY $newsort $args{sortorder}";
   # ensure consistent ordering by sorting on record_id too
   $sql .= ", record_id $args{sortorder}";
+
+  # Offset/pagination
   $sql .= ($args{offset} eq 'all' ? '' : " LIMIT $perpage OFFSET ".$args{offset}*$perpage);
 
@@ -4062,27 +4234,34 @@
   my $self = shift;
   my $dbh = $self->{dbh};
-  my $defrec = shift;
-  my $revrec = shift;
-  my $id = shift;
-  my $filter = shift || '';
-
-  # keep the nasties down, since we can't ?-sub this bit.  :/
-  # note this is chars allowed in DNS hostnames
-  $filter =~ s/[^a-zA-Z0-9_.:-]//g;
-
-  my @bindvars = ($id);
-  push @bindvars, ($filter,$filter) if $filter;
+
+  my %args = @_;
+
+  $args{defrec} = 'n' if !$args{defrec};
+  $args{revrec} = 'n' if !$args{revrec};
+
+  my @bindvars;
   my $sql = "SELECT count(*) FROM ".
-	_rectable($defrec,$revrec).
-	" WHERE "._recparent($defrec,$revrec)."=? ".
-	"AND NOT type=$reverse_typemap{SOA}";
-  if ($filter) {
+	_rectable($args{defrec},$args{revrec}).
+	" r WHERE NOT type=$reverse_typemap{SOA}";
+  if ($args{id}) {
+    $sql .= " AND "._recparent($args{defrec},$args{revrec})." = ?";
+    push @bindvars, $args{id};
+  }
+
+  # Filtering on host/val (mainly normal record list)
+  if ($args{filter}) {
     $sql .= " AND (r.host ~* ? OR r.val ~* ? OR r.host ~* ? OR r.val ~* ?)";
-    my $tmp = join('.',reverse(split(/\./,$filter)));
+    my $tmp = join('.',reverse(split(/\./,$args{filter})));
+    push @bindvars, ($args{filter},$args{filter});
     push @bindvars, ($tmp, $tmp);
   }
-  $sql .= " AND (host ~* ? OR val ~* ? OR host ~* ? OR val ~* ?)" if $filter;
-  my $tmp = join('.',reverse(split(/\./,$filter)));
-  push @bindvars, ($tmp, $tmp) if $filter;
+
+  # Filtering on other fields
+  foreach (qw(type distance weight port ttl description)) {
+    if ($args{$_}) {
+      $sql .= " AND $_ ~* ?";
+      push @bindvars, $args{$_};
+    }
+  }
 
   my ($count) = $dbh->selectrow_array($sql, undef, (@bindvars) );
@@ -4117,9 +4296,13 @@
   $location  = '' if !$location;
 
-  my $expires = shift;
+  my $expires = shift || '';
   $expires = 1 if $expires eq 'until';	# Turn some special values into the appropriate booleans.
   $expires = 0 if $expires eq 'after';
   my $stamp = shift;
   $stamp = '' if !$stamp;	 # Timestamp should be a string at this point.
+
+  # extra safety net - apparently RPC can squeak this by.  O_o
+  return ('FAIL', "host must contain a value") if !$$host;
+  return ('FAIL', "val must contain a value") if !$$val;
 
   # Spaces are evil.
@@ -4249,4 +4432,5 @@
   my $self = shift;
   my $dbh = $self->{dbh};
+
   my $defrec = shift;
   my $revrec = shift;
@@ -4263,5 +4447,5 @@
   $location  = '' if !$location;
 
-  my $expires = shift;
+  my $expires = shift || '';
   $expires = 1 if $expires eq 'until';	# Turn some special values into the appropriate booleans.
   $expires = 0 if $expires eq 'after';
@@ -4481,4 +4665,8 @@
   eval {
     $dbh->do("UPDATE records SET $delpar = ?, type = ? WHERE record_id = ?", undef, @sqlargs);
+    $self->_log(domain_id => $rec->{domain_id}, rdns_id => $rec->{rdns_id},
+	group_id => $self->parentID(id => $rec->{rdns_id}, type => 'revzone', revrec => 'y'),
+	entry => "'$rec->{host} $typemap{$rec->{type}} $rec->{val}' downconverted to ".
+		"'$rec->{host} $typemap{$newtype} $rec->{val}'");
     $dbh->commit;
   };
@@ -4494,4 +4682,5 @@
 ## DNSDB::delRec()
 # Delete a record.  
+# Takes a default/live flag, forward/reverse flag, and the ID of the record to delete.
 sub delRec {
   $errstr = '';
@@ -4514,6 +4703,6 @@
   $logdata{rdns_id} = $oldrec->{rdns_id};
   $logdata{group_id} = $oldrec->{group_id} if $defrec eq 'y';
-  $logdata{group_id} = $self->parentID(id => $oldrec->{domain_id}, type => ($revrec eq 'n' ? 'domain' : 'revzone'),
-	revrec => $revrec)
+  $logdata{group_id} = $self->parentID(id => ($revrec eq 'n' ? $oldrec->{domain_id} : $oldrec->{rdns_id}), 
+	type => 'domain', revrec => $revrec)
 	if $defrec eq 'n';
   $logdata{entry} = "Deleted ".($defrec eq 'y' ? 'default record ' : 'record ').
@@ -4623,5 +4812,5 @@
 
 
-## IPDB::getRevPattern()
+## DNSDB::getRevPattern()
 # Get the narrowest template pattern applicable to a passed CIDR address (may be a netblock or an IP)
 sub getRevPattern {
@@ -4632,6 +4821,6 @@
 
   # for speed!  Casting and comparing even ~7K records takes ~2.5s, so narrow it down to one revzone first.
-  my ($revid) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet >>= ? AND group_id = ?",
-	undef, ($cidr, $group) );
+  my ($revid) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet >>= ?",
+	undef, ($cidr) );
 
 ##fixme?  may need to narrow things down more by octet-chopping and doing text comparisons before casting.
@@ -4641,4 +4830,48 @@
   return $revpatt;
 } # end getRevPattern()
+
+
+## DNSDB::getRevSet()
+# Return the unique per-IP reverse hostnames, if any, for the passed
+# CIDR address (may be a netblock or an IP)
+sub getRevSet {
+  my $self = shift;
+  my $dbh = $self->{dbh};
+  my $cidr = shift;
+  my $group = shift || 1;	# just in case
+
+  # for speed!  Casting and comparing even ~7K records takes ~2.5s, so narrow it down to one revzone first.
+  my ($revid) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet >>= ?",
+	undef, ($cidr) );
+
+  $cidr = new NetAddr::IP $cidr;
+  if ($cidr->num > 256) {	# should also catch v6!
+    # Even reverse entries for a v4 /24 of IPs is a bit much.  I don't expect
+    # there to be a sane reason to retrive more than a /27 at once, really.
+    # v6 is going to be hairy no matter how you slice it.
+    $errstr = "Reverse hostname detail range too large";
+    return;
+  }
+
+  my $sth = $dbh->prepare("SELECT val, host FROM records ".
+	"WHERE (type in (12,65280,65281,65282,65283,65284)) AND rdns_id = ? AND inetlazy(val) = ?");
+
+  my @ret;
+  foreach my $ip (@{$cidr->splitref()}) {
+    $sth->execute($revid, $ip);
+    my @data = $sth->fetchrow_array();
+    my %row;
+    if (@data) {
+      $row{r_ip} = $data[0];
+      $row{iphost} = $data[1];
+    } else {
+      $row{r_ip} = $ip->addr;
+      $row{iphost} = '';
+    }
+    push @ret, \%row;
+  }
+
+  return \@ret;
+} # end getRevSet()
 
 
@@ -4715,13 +4948,15 @@
   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') {
+  # clean up the parent-type.  Set it to group if not set
+  $args{partype} = 'group' if !$args{partype};
+
+  # allow callers to be lazy with type
+  $args{type} = 'revzone' if $args{type} eq 'domain' && $args{revrec} eq 'y';
+
+  if ($par_type{$args{partype}} eq 'domain' || $par_type{$args{partype}} eq 'revzone') {
     # only live records can have a domain/zone parent
     return unless ($args{type} eq 'record' && $args{defrec} eq 'n');
@@ -5679,5 +5914,6 @@
 
           # Check for out-of-zone data
-          if ($host !~ /$dom$/) {
+          $host = $dom if $host eq '@';
+          if ($host !~ /$dom$/i) {
             warn "Not exporting out-of-zone record $host $type $val, $ttl (zone $dom)\n";
             next;
@@ -5825,5 +6061,5 @@
     $tmp = sprintf "%0.4x", $tmp if $srctype eq 'd';	# 0-pad decimal to 4 hex digits
     my @o = ($tmp =~ /^(..)(..)$/);	# split into octets
-    return sprintf "\\%0.3o\\%0.3o", hex($o[0]), hex($o[1]);;
+    return sprintf "\\%0.3o\\%0.3o", hex($o[0]), hex($o[1]);
   }
 
@@ -5847,5 +6083,5 @@
 ##  forked process
   sub __publish_subnet {
-    my $obj = shift;	# *sigh*  need to pass in the DNSDB object so we can read a couple of options
+    my $self = shift;	# *sigh*  need to pass in the DNSDB object so we can read a couple of options
     my $sub = shift;
     my $recflags = shift;
@@ -5862,10 +6098,10 @@
 
     my $iplist = $sub->splitref(32);
+    my $ipindex = -1;
     foreach (@$iplist) {
       my $ip = $_->addr;
+      $ipindex++;
       # make as if we split the non-octet-aligned block into octet-aligned blocks as with SOA
       my $lastoct = (split /\./, $ip)[3];
-      next if $ip =~ /\.0$/ && $obj->{template_skip_0};
-      next if $ip =~ /\.255$/ && $obj->{template_skip_255};
       next if $$recflags{$ip}; # && $self->{skip_bcast_255}
       $$recflags{$ip}++;
@@ -5873,6 +6109,8 @@
       my $rec = $hpat;	# start fresh with the template for each IP
 ##fixme:  there really isn't a good way to handle sub-/24 zones here.  This way at least
-# seems less bad than some alternatives.  
-      _template4_expand(\$rec, $ip);
+# seems less bad than some alternatives.
+      $self->_template4_expand(\$rec, $ip, \$sub, $ipindex);
+      # _template4_expand may blank $rec;  if so, don't publish a record
+      next if !$rec;
       if ($ptronly || $zone->masklen > 24) {
         print $fh "^$lastoct.$arpabase:$rec:$ttl:$stamp:$loc\n" or die $!;
@@ -5885,5 +6123,5 @@
       }
     }
-  }
+  } # __publish_subnet
 
 ## And now the meat.
@@ -6085,6 +6323,9 @@
     # print both;  a dangling record is harmless, and impossible via web
     # UI anyway
-    $self->_printrec_tiny($datafile,$revrec,$recflags,$zone,$host,28,$val,$dist,$weight,$port,$ttl,$loc,$stamp);
-    $self->_printrec_tiny($datafile,$revrec,$recflags,$zone,$host,12,$val,$dist,$weight,$port,$ttl,$loc,$stamp);
+    $self->_printrec_tiny($datafile,$recid,'n',$recflags, $self->domainName($self->_hostparent($host)),
+	$host, 28, $val, $dist, $weight, $port, $ttl, $loc, $stamp);
+    $self->_printrec_tiny($datafile, $recid, 'y', $recflags, $zone,
+	$host, 12, $val, $dist, $weight, $port, $ttl, $loc, $stamp);
+
 ##fixme: add a config flag to indicate use of the patch from http://www.fefe.de/dns/
 # type 6 is for AAAA+PTR, type 3 is for AAAA
@@ -6111,5 +6352,5 @@
     return if $val->{isv6};
 
-    if ($val->masklen <= 16) {
+    if ($val->masklen < 16) {
       foreach my $sub ($val->split(16)) {
         $self->__publish_subnet($sub, $recflags, $host, $datafile, $ttl, $stamp, $loc, $zone, 0);
