Index: branches/stable/DNSDB.pm
===================================================================
--- branches/stable/DNSDB.pm	(revision 546)
+++ branches/stable/DNSDB.pm	(revision 547)
@@ -38,5 +38,5 @@
 @EXPORT_OK	= qw(
 	&initGlobals &login &initActionLog
-	&initPermissions &getPermissions &changePermissions &comparePermissions
+	&getPermissions &changePermissions &comparePermissions
 	&changeGroup
 	&loadConfig &connectDB &finish
@@ -52,18 +52,19 @@
 	&addRec &updateRec &delRec
 	&getLogCount &getLogEntries
+	&getRevPattern
 	&getTypelist
 	&parentID
 	&isParent
-	&zoneStatus &importAXFR
+	&zoneStatus &getZonesByCIDR &importAXFR
 	&export
 	&mailNotify
 	%typemap %reverse_typemap %config
-	%permissions @permtypes $permlist %permchains
+	@permtypes $permlist %permchains
 	);
 
-@EXPORT		= (); # Export nothing by default.
+@EXPORT		= qw(%typemap %reverse_typemap @permtypes $permlist %permchains);
 %EXPORT_TAGS	= ( ALL => [qw(
 		&initGlobals &login &initActionLog
-		&initPermissions &getPermissions &changePermissions &comparePermissions
+		&getPermissions &changePermissions &comparePermissions
 		&changeGroup
 		&loadConfig &connectDB &finish
@@ -79,16 +80,16 @@
 		&addRec &updateRec &delRec
 		&getLogCount &getLogEntries
+		&getRevPattern
 		&getTypelist
 		&parentID
 		&isParent
-		&zoneStatus &importAXFR
+		&zoneStatus &getZonesByCIDR &importAXFR
 		&export
 		&mailNotify
 		%typemap %reverse_typemap %config
-		%permissions @permtypes $permlist %permchains
+		@permtypes $permlist %permchains
 		)]
 	);
 
-our $group = 1;
 our $errstr = '';
 our $resultstr = '';
@@ -135,6 +136,4 @@
 our %reverse_typemap;
 
-our %permissions;
-
 # Prepopulate a basic config.  Note some of these *will* cause errors if left unset.
 # note:  add appropriate stanzas in loadConfig to parse these
@@ -167,4 +166,5 @@
 		log_failures	=> 1,	# log all evarthing by default
 		perpage		=> 15,
+		maxfcgi		=> 100,	# reasonable default?
 	);
 
@@ -174,7 +174,4 @@
 # it relies on visibility flags from the rectypes table in the DB
 my %validators;
-
-# Username, full name, ID - mainly for logging
-my %userdata;
 
 # Entity-relationship reference hashes.
@@ -217,4 +214,30 @@
 
 ##
+## Constructor and destructor
+##
+
+sub new {
+  my $this = shift;
+  my $class = ref($this) || $this;
+  my %args = @_;
+##fixme?  to ponder:  do we do some magic if the caller sets eg dbname to prevent parsing of the config file?
+  if (!loadConfig(basename => $args{configfile})) {
+    warn "Using default configuration;  unable to load custom settings: $errstr\n";
+  }
+  my $self = \%config;
+  $self->{configfile} = $args{configfile};
+  bless $self, $class;
+  $self->{dbh} = connectDB($self->{dbname}, $self->{dbuser}, $self->{dbpass}, $self->{dbhost}) or return;
+  $self->initGlobals();
+
+  return $self;
+}
+
+sub DESTROY {
+  my $self = shift;
+  $self->{dbh}->disconnect;
+}
+
+##
 ## utility functions
 ##
@@ -244,9 +267,10 @@
 ## DNSDB::_ipparent()
 # 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,
+# Takes 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 $self = shift;
+  my $dbh = $self->{dbh};
   my $defrec = shift;
   my $revrec = shift;
@@ -309,8 +333,9 @@
 ## DNSDB::_hostparent()
 # A little different than _ipparent above;  this tries to *find* the parent zone of a hostname
-# Takes a database handle and hostname.
+# Takes a hostname.
 # Returns the domain ID of the parent domain if one was found.
 sub _hostparent {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $hname = shift;
 
@@ -330,5 +355,5 @@
 ## DNSDB::_log()
 # Log an action
-# Takes a database handle and log entry hash containing at least:
+# Takes a log entry hash containing at least:
 #  group_id, log entry
 # and optionally one or more of:
@@ -336,5 +361,6 @@
 # The %userdata hash provides the user ID, username, and fullname
 sub _log {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
 
   my %args = @_;
@@ -348,5 +374,5 @@
 	undef,
 	($args{domain_id}, $args{rdns_id}, $args{group_id}, $args{entry},
-		$userdata{userid}, $userdata{username}, $userdata{fullname}) );
+		$self->{loguserid}, $self->{logusername}, $self->{logfullname}) );
 #  } elsif ($config{log_channel} eq 'file') {
 #  } elsif ($config{log_channel} eq 'syslog') {
@@ -360,5 +386,4 @@
 
 ## All of these subs take substantially the same arguments:
-# a database handle
 # a hash containing at least the following keys:
 #  - defrec (default/live flag)
@@ -374,5 +399,6 @@
 # A record
 sub _validate_1 {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
 
   my %args = @_;
@@ -382,5 +408,5 @@
   # 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}));
+  my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
   ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
 
@@ -399,5 +425,6 @@
 # NS record
 sub _validate_2 {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
 
   my %args = @_;
@@ -409,11 +436,11 @@
     if ($args{revrec} eq 'y') {
       my $tmpip = NetAddr::IP->new(${$args{val}});
-      my $pname = revName($dbh,$args{id});
+      my $pname = $self->revName($args{id});
       return ('FAIL',"${$args{val}} not within $pname")
-	 unless _ipparent($dbh, $args{defrec}, $args{revrec}, $args{val}, $args{id}, \$tmpip);
+	 unless $self->_ipparent($args{defrec}, $args{revrec}, $args{val}, $args{id}, \$tmpip);
       # Sub the returned thing for ZONE?  This could get stupid if you have typos...
       ${$args{val}} =~ s/ZONE/$tmpip->address/;
     } else {
-      my $pname = domainName($dbh,$args{id});
+      my $pname = $self->domainName($args{id});
       ${$args{host}} = $pname if ${$args{host}} !~ /\.$pname$/;
     }
@@ -436,5 +463,6 @@
 # CNAME record
 sub _validate_5 {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
 
   my %args = @_;
@@ -447,5 +475,5 @@
   # 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}));
+  my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
   ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
 
@@ -462,5 +490,6 @@
 # PTR record
 sub _validate_12 {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
 
   my %args = @_;
@@ -468,6 +497,6 @@
   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});
+      return ('FAIL', "IP or IP fragment ${$args{val}} is not within ".$self->revName($args{id}))
+	unless $self->_ipparent($args{defrec}, $args{revrec}, $args{val}, $args{id}, \$args{addr});
       ${$args{val}} = $args{addr}->addr;
     } else {
@@ -542,5 +571,6 @@
 # MX record
 sub _validate_15 {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
 
   my %args = @_;
@@ -558,5 +588,5 @@
   # 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}));
+  my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
   ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
 
@@ -585,5 +615,6 @@
 # AAAA record
 sub _validate_28 {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
 
   my %args = @_;
@@ -593,5 +624,5 @@
   # 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}));
+  my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
   ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
 
@@ -607,5 +638,6 @@
 # SRV record
 sub _validate_33 {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
 
   my %args = @_;
@@ -633,5 +665,5 @@
   # 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}));
+  my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
   ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
 
@@ -643,5 +675,6 @@
 # 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 $self = shift;
+  my $dbh = $self->{dbh};
 
   my %args = @_;
@@ -654,9 +687,16 @@
 
     if ($args{revrec} eq 'y') {
-      ($code,$msg) = _validate_12($dbh, %args);
+      ($code,$msg) = $self->_validate_12(%args);
       return ($code,$msg) if $code eq 'FAIL';
 
+      # check A+PTR is really v4
+      return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv4 address")
+	if ${$args{rectype}} == 65280 && $args{addr}->{isv6};
+      # check AAAA+PTR is really v6
+      return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv6 address")
+	if ${$args{rectype}} == 65281 && !$args{addr}->{isv6};
+
       # Check if the reqested domain exists.  If not, coerce the type down to PTR and warn.
-      if (!(${$args{domid}} = _hostparent($dbh, ${$args{host}}))) {
+      if (!(${$args{domid}} = $self->_hostparent(${$args{host}}))) {
 	my $addmsg = "Record ".($args{update} ? 'updated' : 'added').
 		" as PTR instead of $typemap{${$args{rectype}}};  domain not found for ${$args{host}}";
@@ -672,6 +712,6 @@
 
     } else {
-      ($code,$msg) = _validate_1($dbh, %args) if ${$args{rectype}} == 65280;
-      ($code,$msg) = _validate_28($dbh, %args) if ${$args{rectype}} == 65281;
+      ($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';
 
@@ -722,5 +762,5 @@
   } else {	# defrec eq 'y'
     if ($args{revrec} eq 'y') {
-      ($code,$msg) = _validate_12($dbh, %args);
+      ($code,$msg) = $self->_validate_12(%args);
       return ($code,$msg) if $code eq 'FAIL';
       if (${$args{rectype}} == 65280) {
@@ -754,5 +794,6 @@
 # PTR template record
 sub _validate_65282 {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
 
   my %args = @_;
@@ -761,6 +802,6 @@
   if ($args{revrec} eq 'y') {
     if ($args{defrec} eq 'n') {
-      return ('FAIL', "Template block ${$args{val}} is not within ".revName($dbh, $args{id}))
-	unless _ipparent($dbh, $args{defrec}, $args{revrec}, $args{val}, $args{id}, \$args{addr});
+      return ('FAIL', "Template block ${$args{val}} is not within ".$self->revName($args{id}))
+	unless $self->_ipparent($args{defrec}, $args{revrec}, $args{val}, $args{id}, \$args{addr});
 ##fixme:  warn if $args{val} is not /31 or larger block?
       ${$args{val}} = "$args{addr}";
@@ -821,5 +862,5 @@
       $pcsth->execute($checkme);
       my ($rc) = $pcsth->fetchrow_array;
-      return ('FAIL', "Only one template pseudorecord may exist for a given IP block") if $rc;
+      return ('FAIL', "Only one template pseudorecord may exist for a given IP block") if $rc > 1;
     }
 
@@ -833,5 +874,6 @@
 # A+PTR template record
 sub _validate_65283 {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
 
   my %args = @_;
@@ -843,6 +885,6 @@
   if ($args{defrec} eq 'n') {
     if ($args{revrec} eq 'n') {
-      ($code,$msg) = _validate_1($dbh, %args) if ${$args{rectype}} == 65280;
-      ($code,$msg) = _validate_28($dbh, %args) if ${$args{rectype}} == 65281;
+      ($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';
 
@@ -866,9 +908,9 @@
 
     } else {
-      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});
+      return ('FAIL', "IP or IP fragment ${$args{val}} is not within ".$self->revName($args{id}))
+	unless $self->_ipparent($args{defrec}, $args{revrec}, $args{val}, $args{id}, \$args{addr});
       ${$args{val}} = "$args{addr}";
 
-      if (!(${$args{domid}} = _hostparent($dbh, ${$args{host}}))) {
+      if (!(${$args{domid}} = $self->_hostparent(${$args{host}}))) {
 	my $addmsg = "Record ".($args{update} ? 'updated' : 'added').
 		" as PTR template instead of $typemap{${$args{rectype}}};  domain not found for ${$args{host}}";
@@ -885,5 +927,5 @@
 
   } else {
-    my ($code,$msg) = _validate_65282($dbh, %args);
+    my ($code,$msg) = $self->_validate_65282(%args);
     return ($code, $msg) if $code eq 'FAIL';
     # get domain, check against ${$args{name}}
@@ -902,5 +944,6 @@
 # for delegating IPv4 sub-/24 reverse blocks
 sub _validate_65285 {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
 
   my %args = @_;
@@ -914,11 +957,11 @@
     if ($args{revrec} eq 'y') {
       my $tmpip = NetAddr::IP->new(${$args{val}});
-      my $pname = revName($dbh,$args{id});
+      my $pname = $self->revName($args{id});
       return ('FAIL',"${$args{val}} not within $pname")
-	 unless _ipparent($dbh, $args{defrec}, $args{revrec}, $args{val}, $args{id}, \$tmpip);
+	 unless $self->_ipparent($args{defrec}, $args{revrec}, $args{val}, $args{id}, \$tmpip);
       # Normalize
       ${$args{val}} = "$tmpip";
     } else {
-      my $pname = domainName($dbh,$args{id});
+      my $pname = $self->domainName($args{id});
       ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
     }
@@ -1153,5 +1196,5 @@
   for (@ipparts) {
     push @iphex, sprintf("%x", $_);
-    push @ippad, sprintf("%u.3", $_);
+    push @ippad, sprintf("%0.3u", $_);
   }
 
@@ -1186,5 +1229,5 @@
   $$tmpl =~ s/\%([1234])d/$ipparts[$1-1]/g;
   $$tmpl =~ s/\%([1234])h/$iphex[$1-1]/g;
-  $$tmpl =~ s/\%([1234])h/$ippad[$1-1]/g;
+  $$tmpl =~ s/\%([1234])0/$ippad[$1-1]/g;
 } # _template4_expand()
 
@@ -1193,5 +1236,4 @@
 ## Initialization and cleanup subs
 ##
-
 
 ## DNSDB::loadConfig()
@@ -1200,26 +1242,27 @@
 # Takes an optional hash that may contain:
 #  - basename and config path to look for
-#  - RPC flag (saves parsing the more complex RPC bits if not needed)
 # Populates the %config and %def hashes
 sub loadConfig {
   my %args = @_;
-  $args{basename} = '' if !$args{basename};
-  $args{rpcflag} = '' if !$args{rpcflag};
-##fixme  $args{basename} isn't doing what I think I thought I was trying to do.
+  $args{configfile} = '' if !$args{configfile};
+
+##fixme  this is *intended* to load a system-default config template, and allow
+# overriding on a per-tool or per-web-UI-instance basis with a secondary config
+# file.  The "default" config file can't be deleted in the current form.
 
   my $deferr = '';	# place to put error from default config file in case we can't find either one
 
   my $configroot = "/etc/dnsdb";	##CFG_LEAF##
-  $configroot = '' if $args{basename} =~ m|^/|;
-  $args{basename} .= ".conf" if $args{basename} !~ /\.conf$/;
+  $configroot = '' if $args{configfile} =~ m|^/|;  # allow passed siteconfig to specify an arbitrary absolute path
+  $args{configfile} .= ".conf" if $args{configfile} !~ /\.conf$/;
   my $defconfig = "$configroot/dnsdb.conf";
-  my $siteconfig = "$configroot/$args{basename}";
+  my $siteconfig = "$configroot/$args{configfile}";
 
   # System defaults
-  __cfgload("$defconfig", $args{rpcflag}) or $deferr = $errstr;
+  __cfgload("$defconfig") or $deferr = $errstr;
 
   # Per-site-ish settings.
-  if ($args{basename} ne '.conf') {
-    unless (__cfgload("$siteconfig"), $args{rpcflag}) {
+  if ($args{configfile} ne '.conf') {
+    unless (__cfgload("$siteconfig")) {
       $errstr = ($deferr ? "Error opening default config file $defconfig: $deferr\n" : '').
 	"Error opening site config file $siteconfig";
@@ -1253,9 +1296,8 @@
 ## DNSDB::__cfgload()
 # Private sub to parse a config file and load it into %config
-# Takes a file handle on an open config file
+# Takes a filename
 sub __cfgload {
   $errstr = '';
   my $cfgfile = shift;
-  my $rpcflag = shift;
 
   if (open CFG, "<$cfgfile") {
@@ -1296,10 +1338,10 @@
       $config{exportcache}	= $1 if m{^exportcache\s*=\s*([a-z0-9/_.-]+)}i;
       # RPC options
-      if ($rpcflag && /^rpc/) {
-        if (my ($tmp) = /^rpc_iplist\s*=\s*(.+)/i) {
-          my @ips = split /[,\s]+/, $tmp;
-          my $rpcsys = shift @ips;
-          push @{$config{rpcacl}{$rpcsys}}, @ips;
-        }
+      $config{rpcmode}		= $1 if /^rpc_mode\s*=\s*(socket|HTTP|XMLRPC)\s*$/i;
+      $config{maxfcgi}		= $1 if /^max_fcgi_requests\s*=\s*(\d+)\s*$/i;
+      if (my ($tmp) = /^rpc_iplist\s*=\s*(.+)/i) {
+        my @ips = split /[,\s]+/, $tmp;
+        my $rpcsys = shift @ips;
+        push @{$config{rpcacl}{$rpcsys}}, @ips;
       }
     }
@@ -1316,7 +1358,8 @@
 # Creates connection to DNS database.
 # Requires the database name, username, and password.
-# Returns a handle to the db.
+# Returns a handle to the db or undef on failure.
 # Set up for a PostgreSQL db;  could be any transactional DBMS with the
 # right changes.
+# Called by new();  not intended to be called publicly.
 sub connectDB {
   $errstr = '';
@@ -1335,12 +1378,19 @@
 	AutoCommit => 1,
 	PrintError => 0
-	})
-    or return (undef, $DBI::errstr) if(!$dbh);
-
+	});
+  if (!$dbh) {
+    $errstr = $DBI::errstr;
+    return;
+  }
+#) if(!$dbh);
+
+  local $dbh->{RaiseError} = 1;
+
+  eval {
 ##fixme:  initialize the DB if we can't find the table (since, by definition, there's
 # nothing there if we can't select from it...)
-  my $tblsth = $dbh->prepare("SELECT count(*) FROM pg_catalog.pg_class WHERE relkind='r' AND relname=?");
-  my ($tblcount) = $dbh->selectrow_array($tblsth, undef, ('misc'));
-  return (undef,$DBI::errstr) if $dbh->err;
+    my $tblsth = $dbh->prepare("SELECT count(*) FROM pg_catalog.pg_class WHERE relkind='r' AND relname=?");
+    my ($tblcount) = $dbh->selectrow_array($tblsth, undef, ('misc'));
+#  return (undef,$DBI::errstr) if $dbh->err;
 
 #if ($tblcount == 0) {
@@ -1349,10 +1399,9 @@
 #}
 
-
 # Return here if we can't select.
 # This should retrieve the dbversion key.
-  my $sth = $dbh->prepare("SELECT key,value FROM misc WHERE misc_id=1");
-  $sth->execute();
-  return (undef,$DBI::errstr) if ($sth->err);
+    my $sth = $dbh->prepare("SELECT key,value FROM misc WHERE misc_id=1");
+    $sth->execute();
+#  return (undef,$DBI::errstr) if ($sth->err);
 
 ##fixme:  do stuff to the DB on version mismatch
@@ -1363,11 +1412,17 @@
 # See if the select returned anything (or null data).  This should
 # succeed if the select executed, but...
-  $sth->fetchrow();
-  return (undef,$DBI::errstr)  if ($sth->err);
-
-  $sth->finish;
+    $sth->fetchrow();
+#  return (undef,$DBI::errstr)  if ($sth->err);
+
+    $sth->finish;
+
+  }; # wrapped DB checks
+  if ($@) {
+    $errstr = $@;
+    return;
+  }
 
 # If we get here, we should be OK.
-  return ($dbh,"DB connection OK");
+  return $dbh;
 } # end connectDB
 
@@ -1377,6 +1432,6 @@
 # Requires a database handle
 sub finish {
-  my $dbh = $_[0];
-  $dbh->disconnect;
+  my $self = shift;
+  $self->{dbh}->disconnect;
 } # end finish
 
@@ -1385,7 +1440,7 @@
 # Initialize global variables
 # NB: this does NOT include web-specific session variables!
-# Requires a database handle
 sub initGlobals {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
 
 # load record types from database
@@ -1408,10 +1463,11 @@
 
 ## DNSDB::initRPC()
-# Takes a database handle, remote username, and remote fullname.
+# Takes a remote username and remote fullname.
 # Sets up the RPC logging-pseudouser if needed.
 # Sets the %userdata hash for logging.
 # Returns undef on failure
 sub initRPC {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my %args  = @_;
 
@@ -1429,7 +1485,8 @@
 	" FROM users WHERE username=?", undef, ($args{username}) );
   }
-  %userdata = %{$tmpuser};
-  $userdata{lastname} = '' if !$userdata{lastname};
-  $userdata{fullname} = "$userdata{firstname} $userdata{lastname} ($args{rpcsys})";
+  $tmpuser->{lastname} = '' if !$tmpuser->{lastname};
+  $self->{loguserid} = $tmpuser->{userid};
+  $self->{logusername} = $tmpuser->{username};
+  $self->{logfullname} = "$tmpuser->{firstname} $tmpuser->{lastname} ($args{rpcsys})";
   return 1 if $tmpuser;
 } # end initRPC()
@@ -1442,5 +1499,6 @@
 # Returns undef otherwise
 sub login {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $user = shift;
   my $pass = shift;
@@ -1476,5 +1534,6 @@
 # See https://secure.deepnet.cx/trac/dnsadmin/ticket/21
 sub initActionLog {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $uid = shift;
 
@@ -1488,7 +1547,7 @@
 ##fixme: errors are unpossible!
 
-  $userdata{username} = $username;
-  $userdata{userid} = $uid;
-  $userdata{fullname} = $fullname;
+  $self->{logusername} = $username;
+  $self->{loguserid} = $uid;
+  $self->{logfullname} = $fullname;
 
   # convert to real check once we have other logging channels
@@ -1500,22 +1559,11 @@
 
 
-## DNSDB::initPermissions()
-# Set up permissions global
-# Takes database handle and UID
-sub initPermissions {
-  my $dbh = shift;
-  my $uid = shift;
-
-#  %permissions = $(getPermissions($dbh,'user',$uid));
-  getPermissions($dbh, 'user', $uid, \%permissions);
-
-} # end initPermissions()
-
-
 ## DNSDB::getPermissions()
 # Get permissions from DB
 # Requires DB handle, group or user flag, ID, and hashref.
 sub getPermissions {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
+
   my $type = shift;
   my $id = shift;
@@ -1567,5 +1615,6 @@
 # Takes a db handle, type, owner-id, and hashref for the changed permissions.
 sub changePermissions {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $type = shift;
   my $id = shift;
@@ -1632,5 +1681,5 @@
       $resultmsg = "Updated default permissions for group $name";
     }
-    _log($dbh, (group_id => ($type eq 'user' ? $parid : $id), entry => $resultmsg));
+    $self->_log(group_id => ($type eq 'user' ? $parid : $id), entry => $resultmsg);
     $dbh->commit;
   }; # end eval
@@ -1681,5 +1730,6 @@
 # Takes a database handle, entity type, entity ID, and new group ID
 sub changeGroup {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $type = shift;
   my $id = shift;
@@ -1695,17 +1745,17 @@
   my $entname;
   if ($type eq 'domain') {
-    $entname = domainName($dbh, $id);
+    $entname = $self->domainName($id);
   } elsif ($type eq 'revzone') {
-    $entname = revName($dbh, $id);
+    $entname = $self->revName($id);
   } elsif ($type eq 'user') {
-    $entname = userFullName($dbh, $id, '%u');
+    $entname = $self->userFullName($id, '%u');
   } elsif ($type eq 'group') {
-    $entname = groupName($dbh, $id);
+    $entname = $self->groupName($id);
   }
 
   my ($oldgid) = $dbh->selectrow_array("SELECT group_id FROM $par_tbl{$type} WHERE $id_col{$type}=?",
 	undef, ($id));
-  my $oldgname = groupName($dbh, $oldgid);
-  my $newgname = groupName($dbh, $newgrp);
+  my $oldgname = $self->groupName($oldgid);
+  my $newgname = $self->groupName($newgrp);
 
   return ('FAIL', "Can't move things into a group that doesn't exist") if !$newgname;
@@ -1721,6 +1771,6 @@
     $dbh->do("UPDATE $par_tbl{$type} SET group_id=? WHERE $id_col{$type}=?", undef, ($newgrp, $id));
     # Log the change in both the old and new groups
-    _log($dbh, (group_id => $oldgid, entry => "Moved $type $entname from $oldgname to $newgname"));
-    _log($dbh, (group_id => $newgrp, entry => "Moved $type $entname from $oldgname to $newgname"));
+    $self->_log(group_id => $oldgid, entry => "Moved $type $entname from $oldgname to $newgname");
+    $self->_log(group_id => $newgrp, entry => "Moved $type $entname from $oldgname to $newgname");
     $dbh->commit;
   };
@@ -1729,5 +1779,5 @@
     eval { $dbh->rollback; };
     if ($config{log_failures}) {
-      _log($dbh, (group_id => $oldgid, entry => "Error moving $type $entname to $newgname: $msg"));
+      $self->_log(group_id => $oldgid, entry => "Error moving $type $entname to $newgname: $msg");
       $dbh->commit;	# since we enabled transactions earlier
     }
@@ -1750,5 +1800,6 @@
 sub addDomain {
   $errstr = '';
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   return ('FAIL',"Need database handle") if !$dbh;
   my $domain = shift;
@@ -1791,6 +1842,6 @@
 	undef, ($domain));
 
-    _log($dbh, (domain_id => $dom_id, group_id => $group,
-	entry => "Added ".($state ? 'active' : 'inactive')." domain $domain"));
+    $self->_log(domain_id => $dom_id, group_id => $group,
+	entry => "Added ".($state ? 'active' : 'inactive')." domain $domain");
 
     # ... and now we construct the standard records from the default set.  NB:  group should be variable.
@@ -1806,13 +1857,13 @@
 	my @tmp1 = split /:/, $host;
 	my @tmp2 = split /:/, $val;
-	_log($dbh, (domain_id => $dom_id, group_id => $group,
+	$self->_log(domain_id => $dom_id, group_id => $group,
 		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, (domain_id => $dom_id, group_id => $group,
-		entry => $logentry." $val', TTL $ttl"));
+	$self->_log(domain_id => $dom_id, group_id => $group,
+		entry => $logentry." $val', TTL $ttl");
       }
     }
@@ -1825,5 +1876,5 @@
     my $msg = $@;
     eval { $dbh->rollback; };
-    _log($dbh, (group_id => $group, entry => "Failed adding domain $domain ($msg)"))
+    $self->_log(group_id => $group, entry => "Failed adding domain $domain ($msg)")
 	if $config{log_failures};
     $dbh->commit;	# since we enabled transactions earlier
@@ -1841,5 +1892,6 @@
 # later we may want to archive it in some way instead (status code 2, for example?)
 sub delZone {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $zoneid = shift;
   my $revrec = shift;
@@ -1852,5 +1904,5 @@
   my $msg = '';
   my $failmsg = '';
-  my $zone = ($revrec eq 'n' ? domainName($dbh, $zoneid) : revName($dbh, $zoneid));
+  my $zone = ($revrec eq 'n' ? $self->domainName($zoneid) : $self->revName($zoneid));
 
   return ('FAIL', ($revrec eq 'n' ? 'Domain' : 'Reverse zone')." ID $zoneid doesn't exist") if !$zone;
@@ -1860,6 +1912,6 @@
   $loghash{domain_id} = $zoneid if $revrec eq 'n';
   $loghash{rdns_id} = $zoneid if $revrec eq 'y';
-  $loghash{group_id} = parentID($dbh,
-	(id => $zoneid, type => ($revrec eq 'n' ? 'domain' : 'revzone'), revrec => $revrec) );
+  $loghash{group_id} = $self->parentID(
+	id => $zoneid, type => ($revrec eq 'n' ? 'domain' : 'revzone'), revrec => $revrec);
 
   # Wrap all the SQL in a transaction
@@ -1894,5 +1946,5 @@
     $msg = "Deleted ".($revrec eq 'n' ? 'domain' : 'reverse zone')." $zone";
     $loghash{entry} = $msg;
-    _log($dbh, %loghash);
+    $self->_log(%loghash);
 
     # once we get here, we should have suceeded.
@@ -1905,5 +1957,5 @@
     $loghash{entry} = "Error deleting $zone: $msg ($failmsg)";
     if ($config{log_failures}) {
-      _log($dbh, %loghash);
+      $self->_log(%loghash);
       $dbh->commit;	# since we enabled transactions earlier
     }
@@ -1922,5 +1974,6 @@
 sub domainName {
   $errstr = '';
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $domid = shift;
   my ($domname) = $dbh->selectrow_array("SELECT domain FROM domains WHERE domain_id=?", undef, ($domid) );
@@ -1936,5 +1989,6 @@
 sub revName {
   $errstr = '';
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $revid = shift;
   my ($revname) = $dbh->selectrow_array("SELECT revnet FROM revzones WHERE rdns_id=?", undef, ($revid) );
@@ -1949,5 +2003,6 @@
 sub domainID {
   $errstr = '';
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $domain = shift;
   my ($domid) = $dbh->selectrow_array("SELECT domain_id FROM domains WHERE lower(domain) = lower(?)",
@@ -1963,5 +2018,6 @@
 sub revID {
   $errstr = '';
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $revzone = shift;
   my ($revid) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet=?", undef, ($revzone) );
@@ -1977,10 +2033,13 @@
 # Returns a status code and message
 sub addRDNS {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $zone = NetAddr::IP->new(shift);
+
   return ('FAIL',"Zone name must be a valid CIDR netblock") unless ($zone && $zone->addr !~ /^0/);
   my $revpatt = shift;	# construct a custom (A/AAAA+)? PTR template record
   my $group = shift;
   my $state = shift;
+  my $defloc = shift || '';
 
   $state = 1 if $state =~ /^active$/;
@@ -2007,17 +2066,18 @@
   # 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));
+    # insert the zone...
+    $dbh->do("INSERT INTO revzones (revnet,group_id,status,default_location) VALUES (?,?,?,?)", undef,
+	($zone, $group, $state, $defloc) );
 
     # get the ID...
     ($rdns_id) = $dbh->selectrow_array("SELECT currval('revzones_rdns_id_seq')");
 
-    _log($dbh, (rdns_id => $rdns_id, group_id => $group,
-	entry => "Added ".($state ? 'active' : 'inactive')." reverse zone $zone"));
+    $self->_log(rdns_id => $rdns_id, group_id => $group,
+	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,domain_id,host,type,val,ttl)".
-	" VALUES ($rdns_id,?,?,?,?,?)");
+    my $sth_in = $dbh->prepare("INSERT INTO records (rdns_id,domain_id,host,type,val,ttl,location)".
+	" VALUES ($rdns_id,?,?,?,?,?,?)");
     $sth->execute($group);
     while (my ($host,$type,$val,$ttl) = $sth->fetchrow_array()) {
@@ -2035,5 +2095,6 @@
       # While we're at it, we substitute $zone for ZONE in the value.
       if ($val eq 'ZONE') {
-	next if $revpatt;	# If we've got a pattern, we skip the default record version.
+	# If we've got a pattern, we skip the default record version on (A+)PTR-template types
+	next if $revpatt && ($type == 65282 || $type == 65283);	
 ##fixme?  do we care if we have multiple whole-zone templates?
 	$val = $zone->network;
@@ -2049,5 +2110,5 @@
 	}
 	my $addr;
-	if (_ipparent($dbh, 'n', 'y', \$tmpval, $rdns_id, \$addr)) {
+	if ($self->_ipparent('n', 'y', \$tmpval, $rdns_id, \$addr)) {
 	  $val = $addr->addr;
 	} else {
@@ -2065,5 +2126,5 @@
       my $domid = 0;
       if ($type >= 65280) {
-	if (!($domid = _hostparent($dbh, $host))) {
+	if (!($domid = $self->_hostparent($host))) {
 	  $warnstr .= "\nRecord added as PTR instead of $typemap{$type};  domain not found for $host";
 	  $type = $reverse_typemap{PTR};
@@ -2072,17 +2133,17 @@
       }
 
-      $sth_in->execute($domid,$host,$type,$val,$ttl);
+      $sth_in->execute($domid,$host,$type,$val,$ttl,$defloc);
 
       if ($typemap{$type} eq 'SOA') {
 	my @tmp1 = split /:/, $host;
 	my @tmp2 = split /:/, $val;
-	_log($dbh, (rdns_id => $rdns_id, group_id => $group,
+	$self->_log(rdns_id => $rdns_id, group_id => $group,
 		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"));
+		"[refresh $tmp2[0]] [retry $tmp2[1]] [expire $tmp2[2]] [minttl $tmp2[3]], TTL $ttl");
 	$defttl = $tmp2[3];
       } else {
-	my $logentry = "[new $zone] Added record '$host $typemap{$type}";
-	_log($dbh, (rdns_id => $rdns_id, domain_id => $domid, group_id => $group,
-		entry => $logentry." $val', TTL $ttl"));
+	my $logentry = "[new $zone] Added record '$host $typemap{$type} $val', TTL $ttl";
+	$logentry .= ", default location ".$self->getLoc($defloc)->{description} if $defloc;
+	$self->_log(rdns_id => $rdns_id, domain_id => $domid, group_id => $group, entry => $logentry);
       }
     }
@@ -2098,5 +2159,5 @@
 
       my $domid = 0;
-      if (!($domid = _hostparent($dbh, $host))) {
+      if (!($domid = $self->_hostparent($host))) {
 	$warnstr .= "\nDefault pattern added as PTR template instead of $typemap{$type};  domain not found for $host";
 	$type = 65282;
@@ -2104,12 +2165,12 @@
       }
 
-      $sth_in->execute($domid,$host,$type,$val,$defttl);
+      $sth_in->execute($domid,$host,$type,$val,$defttl,$defloc);
       my $logentry = "[new $zone] Added record '$host $typemap{$type}";
-      _log($dbh, (rdns_id => $rdns_id, domain_id => $domid, group_id => $group,
-	entry => $logentry." $val', TTL $defttl from pattern"));
+      $self->_log(rdns_id => $rdns_id, domain_id => $domid, group_id => $group,
+	entry => $logentry." $val', TTL $defttl from pattern");
     }
 
     # If there are warnings (presumably about default records skipped for cause) log them
-    _log($dbh, (rdns_id => $rdns_id, group_id => $group, entry => "Warning(s) adding $zone:$warnstr"))
+    $self->_log(rdns_id => $rdns_id, group_id => $group, entry => "Warning(s) adding $zone:$warnstr")
 	if $warnstr;
 
@@ -2121,5 +2182,5 @@
     my $msg = $@;
     eval { $dbh->rollback; };
-    _log($dbh, (group_id => $group, entry => "Failed adding reverse zone $zone ($msg)"))
+    $self->_log(group_id => $group, entry => "Failed adding reverse zone $zone ($msg)")
 	if $config{log_failures};
     $dbh->commit;	# since we enabled transactions earlier
@@ -2146,5 +2207,6 @@
 # Returns an integer count of the resulting zone list.
 sub getZoneCount {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
 
   my %args = @_;
@@ -2179,5 +2241,6 @@
 # Returns a reference to an array of hashrefs suitable for feeding to HTML::Template
 sub getZoneList {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
 
   my %args = @_;
@@ -2185,5 +2248,5 @@
   my @zonelist;
 
-  $args{sortorder} = 'ASC' if !grep /^$args{sortorder}$/, ('ASC','DESC');
+  $args{sortorder} = 'ASC' if !$args{sortorder} || !grep /^$args{sortorder}$/, ('ASC','DESC');
   $args{offset} = 0 if !$args{offset} || $args{offset} !~ /^(?:all|\d+)$/;
 
@@ -2197,5 +2260,5 @@
   # 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}$/, ('domain','group','status');
+    $args{sortby} = 'domain' if !$args{sortby} || !grep /^$args{sortby}$/, ('domain','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".
@@ -2205,5 +2268,5 @@
   } else {
 ##fixme:  arguably startwith here is irrelevant.  depends on the UI though.
-    $args{sortby} = 'revnet' if !grep /^$args{sortby}$/, ('revnet','group','status');
+    $args{sortby} = 'revnet' if !$args{sortby} || !grep /^$args{sortby}$/, ('revnet','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".
@@ -2237,5 +2300,6 @@
 # Takes a database handle, forward/reverse flag, and zone ID
 sub getZoneLocation {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $revrec = shift;
   my $zoneid = shift;
@@ -2255,5 +2319,6 @@
 sub addGroup {
   $errstr = '';
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $groupname = shift;
   my $pargroup = shift;
@@ -2330,5 +2395,5 @@
     }
 
-    _log($dbh, (group_id => $pargroup, entry => "Added group $groupname") );
+    $self->_log(group_id => $pargroup, entry => "Added group $groupname");
 
     # once we get here, we should have suceeded.
@@ -2340,5 +2405,5 @@
     eval { $dbh->rollback; };
     if ($config{log_failures}) {
-      _log($dbh, (group_id => $pargroup, entry => "Failed to add group $groupname: $msg") );
+      $self->_log(group_id => $pargroup, entry => "Failed to add group $groupname: $msg");
       $dbh->commit;
     }
@@ -2355,5 +2420,6 @@
 # Returns a status code and message
 sub delGroup {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $groupid = shift;
 
@@ -2371,6 +2437,6 @@
 
   # collect some pieces for logging and error messages
-  my $groupname = groupName($dbh,$groupid);
-  my $parid = parentID($dbh, (id => $groupid, type => 'group'));
+  my $groupname = $self->groupName($groupid);
+  my $parid = $self->parentID(id => $groupid, type => 'group');
 
   # Wrap all the SQL in a transaction
@@ -2392,5 +2458,5 @@
     $dbh->do("DELETE from groups WHERE group_id=?", undef, ($groupid));
 
-    _log($dbh, (group_id => $parid, entry => "Deleted group $groupname"));
+    $self->_log(group_id => $parid, entry => "Deleted group $groupname");
     $resultmsg = "Deleted group $groupname";
 
@@ -2403,5 +2469,5 @@
     eval { $dbh->rollback; };
     if ($config{log_failures}) {
-      _log($dbh, (group_id => $parid, entry => "$failmsg: $msg"));
+      $self->_log(group_id => $parid, entry => "$failmsg: $msg");
       $dbh->commit;	# since we enabled transactions earlier
     }
@@ -2421,5 +2487,6 @@
 sub getChildren {
   $errstr = '';
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $rootgroup = shift;
   my $groupdest = shift;
@@ -2442,5 +2509,5 @@
     while (my ($group) = $sth->fetchrow_array) {
       push @$groupdest, $group;
-      getChildren($dbh,$group,$groupdest) if $immed eq 'all';
+      $self->getChildren($group, $groupdest) if $immed eq 'all';
     }
   }
@@ -2454,5 +2521,6 @@
 sub groupName {
   $errstr = '';
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $groupid = shift;
   my $sth = $dbh->prepare("SELECT group_name FROM groups WHERE group_id=?");
@@ -2472,5 +2540,6 @@
 # Returns an integer count of the resulting group list.
 sub getGroupCount {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
 
   my %args = @_;
@@ -2497,5 +2566,6 @@
 # Returns an arrayref containing hashrefs suitable for feeding straight to HTML::Template
 sub getGroupList {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
 
   my %args = @_;
@@ -2557,5 +2627,6 @@
 sub groupID {
   $errstr = '';
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $group = shift;
   my ($grpid) = $dbh->selectrow_array("SELECT group_id FROM groups WHERE group_name=?", undef, ($group) );
@@ -2581,5 +2652,6 @@
 sub addUser {
   $errstr = '';
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $username = shift;
   my $group = shift;
@@ -2669,5 +2741,5 @@
 ##fixme: add another table to hold name/email for log table?
 
-    _log($dbh, (group_id => $group, entry => "Added user $username ($fname $lname)"));
+    $self->_log(group_id => $group, entry => "Added user $username ($fname $lname)");
     # once we get here, we should have suceeded.
     $dbh->commit;
@@ -2678,5 +2750,5 @@
     eval { $dbh->rollback; };
     if ($config{log_failures}) {
-      _log($dbh, (group_id => $group, entry => "Error adding user $username: $msg"));
+      $self->_log(group_id => $group, entry => "Error adding user $username: $msg");
       $dbh->commit;	# since we enabled transactions earlier
     }
@@ -2695,5 +2767,6 @@
 # - a "Starts with" string
 sub getUserCount {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
 
   my %args = @_;
@@ -2723,5 +2796,6 @@
 # - offset/return-all-everything flag (defaults to $perpage records)
 sub getUserList {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
 
   my %args = @_;
@@ -2763,5 +2837,6 @@
 # Returns a reference to a list of hashrefs suitable to feeding to HTML::Template
 sub getUserDropdown {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $grp = shift;
   my $sel = shift || 0;
@@ -2783,31 +2858,9 @@
 
 
-## DNSDB::checkUser()
-# Check user/pass combo on login
-sub checkUser {
-  my $dbh = shift;
-  my $user = shift;
-  my $inpass = shift;
-
-  my $sth = $dbh->prepare("SELECT user_id,group_id,password,firstname,lastname FROM users WHERE username=?");
-  $sth->execute($user);
-  my ($uid,$gid,$pass,$fname,$lname) = $sth->fetchrow_array;
-  my $loginfailed = 1 if !defined($uid);
-
-  if ($pass =~ m|^\$1\$([A-Za-z0-9/.]+)\$|) {
-    $loginfailed = 1 if $pass ne unix_md5_crypt($inpass,$1);
-  } else {
-    $loginfailed = 1 if $pass ne $inpass;
-  }
-
-  # nnnngggg
-  return ($uid, $gid);
-} # end checkUser
-
-
 ## DNSDB:: updateUser()
 # Update general data about user
 sub updateUser {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
 
 ##fixme:  tweak calling convention so that we can update any given bit of data
@@ -2849,5 +2902,5 @@
 	" WHERE user_id=?", undef, ($username, $pass, $fname, $lname, $phone, $type, $state, $uid));
     $resultmsg = "Updated user info for $username ($fname $lname)";
-    _log($dbh, group_id => $group, entry => $resultmsg);
+    $self->_log(group_id => $group, entry => $resultmsg);
     $dbh->commit;
   };
@@ -2856,5 +2909,5 @@
     eval { $dbh->rollback; };
     if ($config{log_failures}) {
-      _log($dbh, (group_id => $group, entry => "Error updating user $username: $msg"));
+      $self->_log(group_id => $group, entry => "Error updating user $username: $msg");
       $dbh->commit;	# since we enabled transactions earlier
     }
@@ -2871,10 +2924,11 @@
 # Returns a success/failure code and matching message
 sub delUser {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $userid = shift;
 
   return ('FAIL',"Bad userid") if !defined($userid);
 
-  my $userdata = getUserData($dbh, $userid);
+  my $userdata = $self->getUserData($userid);
 
   # Allow transactions, and raise an exception on errors so we can catch it later.
@@ -2885,7 +2939,7 @@
   eval {
     $dbh->do("DELETE FROM users WHERE user_id=?", undef, ($userid));
-    _log($dbh, (group_id => $userdata->{group_id},
+    $self->_log(group_id => $userdata->{group_id},
 	entry => "Deleted user ID $userid/".$userdata->{username}.
-		" (".$userdata->{firstname}." ".$userdata->{lastname}.")") );
+		" (".$userdata->{firstname}." ".$userdata->{lastname}.")");
     $dbh->commit;
   };
@@ -2894,6 +2948,6 @@
     eval { $dbh->rollback; };
     if ($config{log_failures}) {
-      _log($dbh, (group_id => $userdata->{group_id}, entry => "Error deleting user ID ".
-	"$userid/".$userdata->{username}.": $msg") );
+      $self->_log(group_id => $userdata->{group_id}, entry => "Error deleting user ID ".
+	"$userid/".$userdata->{username}.": $msg");
       $dbh->commit;
     }
@@ -2915,5 +2969,6 @@
 sub userFullName {
   $errstr = '';
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $userid = shift;
   my $fullformat = shift || '%f %l (%u)';
@@ -2936,5 +2991,6 @@
 # Returns undef on errors.
 sub userStatus {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $id = shift;
   my $newstatus = shift || 'mu';
@@ -2942,5 +2998,5 @@
   return undef if $id !~ /^\d+$/;
 
-  my $userdata = getUserData($dbh, $id);
+  my $userdata = $self->getUserData($id);
 
   # Allow transactions, and raise an exception on errors so we can catch it later.
@@ -2960,7 +3016,7 @@
 
       my %loghash;
-      $loghash{group_id} = parentID($dbh, (id => $id, type => 'user'));
+      $loghash{group_id} = $self->parentID(id => $id, type => 'user');
       $loghash{entry} = $resultstr;
-      _log($dbh, %loghash);
+      $self->_log(%loghash);
 
       $dbh->commit;
@@ -2984,5 +3040,6 @@
 # Get misc user data for display
 sub getUserData {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $uid = shift;
 
@@ -3000,5 +3057,6 @@
 # Returns ('OK',<location>) on success, ('FAIL',<failmsg>) on failure
 sub addLoc {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $grp = shift;
   my $shdesc = shift;
@@ -3038,6 +3096,7 @@
     # Get the "last" location.  Note this is the only use for loc_id, because selecting on location Does Funky Things
     ($loc) = $dbh->selectrow_array("SELECT location FROM locations ORDER BY loc_id DESC LIMIT 1");
-    ($loc) = ($loc =~ /^(..)/);
+    ($loc) = ($loc =~ /^(..)/) if $loc;
     my $origloc = $loc;
+    $loc = 'aa' if !$loc;	
     # Make a change...
     $loc++;
@@ -3055,5 +3114,5 @@
     $dbh->do("INSERT INTO locations (location, group_id, iplist, description, comments) VALUES (?,?,?,?,?)", 
 	undef, ($loc, $grp, $iplist, $shdesc, $comments) );
-    _log($dbh, entry => "Added location ($shdesc, '$iplist')");
+    $self->_log(entry => "Added location ($shdesc, '$iplist')");
     $dbh->commit;
   };
@@ -3063,5 +3122,5 @@
     if ($config{log_failures}) {
       $shdesc = $loc if !$shdesc;
-      _log($dbh, (entry => "Failed adding location ($shdesc, '$iplist'): $msg"));
+      $self->_log(entry => "Failed adding location ($shdesc, '$iplist'): $msg");
       $dbh->commit;
     }
@@ -3079,5 +3138,6 @@
 # Returns a result code and message
 sub updateLoc {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $loc = shift;
   my $grp = shift;
@@ -3095,5 +3155,5 @@
   local $dbh->{RaiseError} = 1;
 
-  my $oldloc = getLoc($dbh, $loc);
+  my $oldloc = $self->getLoc($loc);
   my $okmsg = "Updated location (".$oldloc->{description}.", '".$oldloc->{iplist}."') to ($shdesc, '$iplist')";
 
@@ -3101,5 +3161,5 @@
     $dbh->do("UPDATE locations SET group_id=?,iplist=?,description=?,comments=? WHERE location=?",
 	undef, ($grp, $iplist, $shdesc, $comments, $loc) );
-    _log($dbh, entry => $okmsg);
+    $self->_log(entry => $okmsg);
     $dbh->commit;
   };
@@ -3109,5 +3169,5 @@
     if ($config{log_failures}) {
       $shdesc = $loc if !$shdesc;
-      _log($dbh, (entry => "Failed updating location ($shdesc, '$iplist'): $msg"));
+      $self->_log(entry => "Failed updating location ($shdesc, '$iplist'): $msg");
       $dbh->commit;
     }
@@ -3121,5 +3181,6 @@
 ## DNSDB::delLoc()
 sub delLoc {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $loc = shift;
 
@@ -3129,5 +3190,5 @@
   local $dbh->{RaiseError} = 1;
 
-  my $oldloc = getLoc($dbh, $loc);
+  my $oldloc = $self->getLoc($loc);
   my $olddesc = ($oldloc->{description} ? $oldloc->{description} : $loc);
   my $okmsg = "Deleted location ($olddesc, '".$oldloc->{iplist}."')";
@@ -3139,5 +3200,5 @@
     die "Records still exist in location $olddesc\n" if $r;
     $dbh->do("DELETE FROM locations WHERE location=?", undef, ($loc) );
-    _log($dbh, entry => $okmsg);
+    $self->_log(entry => $okmsg);
     $dbh->commit;
   };
@@ -3146,5 +3207,5 @@
     eval { $dbh->rollback; };
     if ($config{log_failures}) {
-      _log($dbh, (entry => "Failed to delete location ($olddesc, '$oldloc->{iplist}'): $msg"));
+      $self->_log(entry => "Failed to delete location ($olddesc, '$oldloc->{iplist}'): $msg");
       $dbh->commit;
     }
@@ -3161,5 +3222,6 @@
 # Returns a reference to a hash containing the group ID, IP list, description, and comments/notes
 sub getLoc {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $loc = shift;
 
@@ -3177,5 +3239,6 @@
 # - a "Starts with" string
 sub getLocCount {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
 
   my %args = @_;
@@ -3200,5 +3263,6 @@
 ## DNSDB::getLocList()
 sub getLocList {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
 
   my %args = @_;
@@ -3239,5 +3303,6 @@
 # Returns a reference to a list of hashrefs suitable to feeding to HTML::Template
 sub getLocDropdown {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $grp = shift;
   my $sel = shift || '';
@@ -3251,5 +3316,5 @@
 
   my @loclist;
-  push @loclist, { locname => "(None/public)", loc => '', selected => ($sel ? 0 : ($sel eq '' ? 1 : 0)) };
+  push @loclist, { locname => "(Default/All)", loc => '', selected => ($sel ? 0 : ($sel eq '' ? 1 : 0)) };
   while (my ($locname, $loc) = $sth->fetchrow_array) {
     my %row = (
@@ -3269,5 +3334,6 @@
 sub getSOA {
   $errstr = '';
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $def = shift;
   my $rev = shift;
@@ -3297,5 +3363,6 @@
 # Returns a two-element list with a result code and message
 sub updateSOA {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $defrec = shift;
   my $revrec = shift;
@@ -3303,5 +3370,5 @@
   my %soa = @_;
 
-  my $oldsoa = getSOA($dbh, $defrec, $revrec, $soa{id});
+  my $oldsoa = $self->getSOA($defrec, $revrec, $soa{id});
 
   my $msg;
@@ -3310,11 +3377,11 @@
     $logdata{domain_id} = $soa{id} if $revrec eq 'n';
     $logdata{rdns_id} = $soa{id} if $revrec eq 'y';
-    $logdata{group_id} = parentID($dbh, (id => $soa{id}, revrec => $revrec,
-	type => ($revrec eq 'n' ? 'domain' : 'revzone') ) );
+    $logdata{group_id} = $self->parentID(id => $soa{id}, revrec => $revrec,
+	type => ($revrec eq 'n' ? 'domain' : 'revzone') );
   } else {
     $logdata{group_id} = $soa{id};
   }
-  my $parname = ($defrec eq 'y' ? groupName($dbh, $soa{id}) :
-		($revrec eq 'n' ? domainName($dbh, $soa{id}) : revName($dbh, $soa{id})) );
+  my $parname = ($defrec eq 'y' ? $self->groupName($soa{id}) :
+		($revrec eq 'n' ? $self->domainName($soa{id}) : $self->revName($soa{id})) );
 
   # Allow transactions, and raise an exception on errors so we can catch it later.
@@ -3335,5 +3402,5 @@
 
     $logdata{entry} = $msg;
-    _log($dbh, %logdata);
+    $self->_log(%logdata);
 
     $dbh->commit;
@@ -3345,5 +3412,5 @@
 	"SOA record for $parname: $msg";
     if ($config{log_failures}) {
-      _log($dbh, %logdata);
+      $self->_log(%logdata);
       $dbh->commit;
     }
@@ -3360,5 +3427,6 @@
 sub getRecLine {
   $errstr = '';
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $defrec = shift;
   my $revrec = shift;
@@ -3390,4 +3458,5 @@
     $ret->{secid} = (($revrec eq 'y') ? $ret->{domain_id} : $ret->{rdns_id}) if $ret->{type} > 65279;
   }
+  $ret->{address} = $ret->{val};	# because.
 
   return $ret;
@@ -3403,5 +3472,6 @@
 sub getDomRecs {
   $errstr = '';
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
 
   my %args = @_;
@@ -3462,5 +3532,6 @@
 # Returns the count
 sub getRecCount {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $defrec = shift;
   my $revrec = shift;
@@ -3496,5 +3567,6 @@
 sub addRec {
   $errstr = '';
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $defrec = shift;
   my $revrec = shift;
@@ -3554,14 +3626,20 @@
 
   # Call the validation sub for the type requested.
-  ($retcode,$retmsg) = $validators{$$rectype}($dbh, (defrec => $defrec, revrec => $revrec, id => $id,
+  ($retcode,$retmsg) = $validators{$$rectype}($self, defrec => $defrec, revrec => $revrec, id => $id,
 	host => $host, rectype => $rectype, val => $val, addr => $addr,
 	dist => \$dist, port => \$port, weight => \$weight,
-	fields => \$fields, vallist => \@vallist) );
+	fields => \$fields, vallist => \@vallist);
 
   return ($retcode,$retmsg) if $retcode eq 'FAIL';
 
   # Set up database fields and bind parameters
-  $fields .= "host,type,val,ttl,location,"._recparent($defrec,$revrec);
-  push @vallist, ($$host,$$rectype,$$val,$ttl,$location,$id);
+  $fields .= "host,type,val,ttl,"._recparent($defrec,$revrec);
+  push @vallist, ($$host,$$rectype,$$val,$ttl,$id);
+
+  # locations are not for default records, silly coder!
+  if ($defrec eq 'n') {
+    $fields .= ",location";
+    push @vallist, $location;
+  }
   my $vallen = '?'.(',?'x$#vallist);
 
@@ -3576,6 +3654,5 @@
   }
   $logdata{group_id} = $id if $defrec eq 'y';
-  $logdata{group_id} = parentID($dbh,
-		(id => $id, type => ($revrec eq 'n' ? 'domain' : 'revzone'), revrec => $revrec) )
+  $logdata{group_id} = $self->parentID(id => $id, type => ($revrec eq 'n' ? 'domain' : 'revzone'), revrec => $revrec)
 	if $defrec eq 'n';
   $logdata{entry} = "Added ".($defrec eq 'y' ? 'default record' : 'record');
@@ -3591,5 +3668,5 @@
 	if $typemap{$$rectype} eq 'SRV';
   $logdata{entry} .= "', TTL $ttl";
-  $logdata{entry} .= ", location ".getLoc($dbh, $location)->{description} if $location;
+  $logdata{entry} .= ", location ".$self->getLoc($location)->{description} if $location;
 
   # Allow transactions, and raise an exception on errors so we can catch it later.
@@ -3601,5 +3678,5 @@
     $dbh->do("INSERT INTO "._rectable($defrec, $revrec)." ($fields) VALUES ($vallen)",
 	undef, @vallist);
-    _log($dbh, %logdata);
+    $self->_log(%logdata);
     $dbh->commit;
   };
@@ -3610,5 +3687,5 @@
       $logdata{entry} = "Failed adding ".($defrec eq 'y' ? 'default ' : '').
 	"record '$$host $typemap{$$rectype} $$val', TTL $ttl ($msg)";
-      _log($dbh, %logdata);
+      $self->_log(%logdata);
       $dbh->commit;
     }
@@ -3629,5 +3706,6 @@
   $errstr = '';
 
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $defrec = shift;
   my $revrec = shift;
@@ -3683,14 +3761,14 @@
   # get old record data so we have the right parent ID
   # and for logging (eventually)
-  my $oldrec = getRecLine($dbh, $defrec, $revrec, $id);
+  my $oldrec = $self->getRecLine($defrec, $revrec, $id);
 
   # Call the validation sub for the type requested.
   # Note the ID to pass here is the *parent*, not the record
-  ($retcode,$retmsg) = $validators{$$rectype}($dbh, (defrec => $defrec, revrec => $revrec,
+  ($retcode,$retmsg) = $validators{$$rectype}($self, defrec => $defrec, revrec => $revrec,
 	id => ($defrec eq 'y' ? $oldrec->{group_id} : ($revrec eq 'n' ? $oldrec->{domain_id} : $oldrec->{rdns_id})),
 	host => $host, rectype => $rectype, val => $val, addr => $addr,
 	dist => \$dist, port => \$port, weight => \$weight,
 	fields => \$fields, vallist => \@vallist,
-	update => $id) );
+	update => $id);
 
   return ($retcode,$retmsg) if $retcode eq 'FAIL';
@@ -3698,7 +3776,13 @@
   # Set up database fields and bind parameters.  Note only the optional fields
   # (distance, weight, port, secondary parent ID) are added in the validation call above
-  $fields .= "host,type,val,ttl,location,"._recparent($defrec,$revrec);
-  push @vallist, ($$host,$$rectype,$$val,$ttl,$location,
+  $fields .= "host,type,val,ttl,"._recparent($defrec,$revrec);
+  push @vallist, ($$host,$$rectype,$$val,$ttl,
 	($defrec eq 'y' ? $oldrec->{group_id} : ($revrec eq 'n' ? $oldrec->{domain_id} : $oldrec->{rdns_id})) );
+
+  # locations are not for default records, silly coder!
+  if ($defrec eq 'n') {
+    $fields .= ",location";
+    push @vallist, $location;
+  }
 
   # hack hack PTHUI
@@ -3739,6 +3823,5 @@
   }
   $logdata{group_id} = $parid if $defrec eq 'y';
-  $logdata{group_id} = parentID($dbh,
-		(id => $parid, type => ($revrec eq 'n' ? 'domain' : 'revzone'), revrec => $revrec) )
+  $logdata{group_id} = $self->parentID(id => $parid, type => ($revrec eq 'n' ? 'domain' : 'revzone'), revrec => $revrec)
 	if $defrec eq 'n';
   $logdata{entry} = "Updated ".($defrec eq 'y' ? 'default record' : 'record')." from\n";
@@ -3753,5 +3836,5 @@
 	if $typemap{$oldrec->{type}} eq 'SRV';
   $logdata{entry} .= "', TTL $oldrec->{ttl}";
-  $logdata{entry} .= ", location ".getLoc($dbh, $oldrec->{location})->{description} if $oldrec->{location};
+  $logdata{entry} .= ", location ".$self->getLoc($oldrec->{location})->{description} if $oldrec->{location};
   $logdata{entry} .= "\nto\n";
   # More NS special
@@ -3764,5 +3847,5 @@
   $logdata{entry} .= " [priority $dist] [weight $weight] [port $port]" if $typemap{$$rectype} eq 'SRV';
   $logdata{entry} .= "', TTL $ttl";
-  $logdata{entry} .= ", location ".getLoc($dbh, $location)->{description} if $location;
+  $logdata{entry} .= ", location ".$self->getLoc($location)->{description} if $location;
 
   local $dbh->{AutoCommit} = 0;
@@ -3775,5 +3858,5 @@
   eval {
     $dbh->do("UPDATE "._rectable($defrec,$revrec)." SET $fields WHERE record_id=?", undef, (@vallist, $id) );
-    _log($dbh, %logdata);
+    $self->_log(%logdata);
     $dbh->commit;
   };
@@ -3784,5 +3867,5 @@
       $logdata{entry} = "Failed updating ".($defrec eq 'y' ? 'default ' : '').
 	"record '$oldrec->{host} $typemap{$oldrec->{type}} $oldrec->{val}', TTL $oldrec->{ttl} ($msg)";
-      _log($dbh, %logdata);
+      $self->_log(%logdata);
       $dbh->commit;
     }
@@ -3793,4 +3876,60 @@
   return ($retcode, $retmsg);
 } # end updateRec()
+
+
+## DNSDB::downconvert()
+# A mostly internal (not exported) semiutilty sub to downconvert from pseudotype <x>
+# to a compatible component type.  Only a handful of operations are valid, anything
+# else is a null-op.
+# Takes the record ID and the new type.  Returns boolean.
+sub downconvert {
+  my $self = shift;
+  my $dbh = $self->{dbh};
+  my $recid = shift;
+  my $newtype = shift;
+
+  # also, only work on live records;  little to no value trying to do this on default records.
+  my $rec = $self->getRecLine('n', 'y', $recid);
+
+  # hm?
+  #return 1 if !$rec;
+
+  return 1 if $rec->{type} < 65000;	# Only the reverse-record pseudotypes can be downconverted
+  return 1 if $rec->{type} == 65282;	# Nowhere to go
+
+  my $delpar;
+  my @sqlargs;
+  if ($rec->{type} == 65280) {
+    return 1 if $newtype != 1 && $newtype != 12;
+    $delpar = ($newtype == 1 ? 'rdns_id' : 'domain_id');
+    push @sqlargs, 0, $newtype, $recid;
+  } elsif ($rec->{type} == 65281) {
+    return 1 if $newtype != 28 && $newtype != 12;
+    $delpar = ($newtype == 28 ? 'rdns_id' : 'domain_id');
+    push @sqlargs, 0, $newtype, $recid;
+  } elsif ($rec->{type} == 65283) {
+    return 1 if $newtype != 65282;
+    $delpar = 'rdns_id';
+  } elsif ($rec->{type} == 65284) {
+    return 1 if $newtype != 65282;
+    $delpar = 'rdns_id';
+  } else {
+    # Your llama is on fire.
+  }
+
+  local $dbh->{AutoCommit} = 0;
+  local $dbh->{RaiseError} = 1;
+
+  eval {
+    $dbh->do("UPDATE records SET $delpar = ?, type = ? WHERE record_id = ?", undef, @sqlargs);
+    $dbh->commit;
+  };
+  if ($@) {
+    $errstr = $@;
+    eval { $dbh->rollback; };
+    return 0;
+  }
+  return 1;
+} # end downconvert()
 
 
@@ -3799,10 +3938,11 @@
 sub delRec {
   $errstr = '';
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $defrec = shift;
   my $revrec = shift;
   my $id = shift;
 
-  my $oldrec = getRecLine($dbh, $defrec, $revrec, $id);
+  my $oldrec = $self->getRecLine($defrec, $revrec, $id);
 
   # Allow transactions, and raise an exception on errors so we can catch it later.
@@ -3816,6 +3956,6 @@
   $logdata{rdns_id} = $oldrec->{rdns_id};
   $logdata{group_id} = $oldrec->{group_id} if $defrec eq 'y';
-  $logdata{group_id} = parentID($dbh,
-		(id => $oldrec->{domain_id}, type => ($revrec eq 'n' ? 'domain' : 'revzone'), revrec => $revrec) )
+  $logdata{group_id} = $self->parentID(id => $oldrec->{domain_id}, type => ($revrec eq 'n' ? 'domain' : 'revzone'),
+	revrec => $revrec)
 	if $defrec eq 'n';
   $logdata{entry} = "Deleted ".($defrec eq 'y' ? 'default record ' : 'record ').
@@ -3825,9 +3965,9 @@
 	if $typemap{$oldrec->{type}} eq 'SRV';
   $logdata{entry} .= "', TTL $oldrec->{ttl}";
-  $logdata{entry} .= ", location ".getLoc($dbh, $oldrec->{location})->{description} if $oldrec->{location};
+  $logdata{entry} .= ", location ".$self->getLoc($oldrec->{location})->{description} if $oldrec->{location};
 
   eval {
     my $sth = $dbh->do("DELETE FROM "._rectable($defrec,$revrec)." WHERE record_id=?", undef, ($id));
-    _log($dbh, %logdata);
+    $self->_log(%logdata);
     $dbh->commit;
   };
@@ -3838,5 +3978,5 @@
       $logdata{entry} = "Error deleting ".($defrec eq 'y' ? 'default record' : 'record').
 	" '$oldrec->{host} $typemap{$oldrec->{type}} $oldrec->{val}', TTL $oldrec->{ttl} ($msg)";
-      _log($dbh, %logdata);
+      $self->_log(%logdata);
       $dbh->commit;
     }
@@ -3853,5 +3993,6 @@
 # - Entity ID and entity type as the primary log "slice"
 sub getLogCount {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
 
   my %args = @_;
@@ -3889,5 +4030,6 @@
 # - offset for pagination
 sub getLogEntries {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
 
   my %args = @_;
@@ -3925,4 +4067,24 @@
 
 
+## IPDB::getRevPattern()
+# Get the narrowest template pattern applicable to a passed CIDR address (may be a netblock or an IP)
+sub getRevPattern {
+  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 >>= ? AND group_id = ?",
+	undef, ($cidr, $group) );
+
+##fixme?  may need to narrow things down more by octet-chopping and doing text comparisons before casting.
+  my ($revpatt) = $dbh->selectrow_array("SELECT host FROM records ".
+	"WHERE (type in (12,65280,65281,65282,65283,65284)) AND rdns_id = ? AND CAST (val AS inet) >>= ? ".
+	"ORDER BY CAST (val AS inet) DESC LIMIT 1", undef, ($revid, $cidr) );
+  return $revpatt;
+} # end getRevPattern()
+
+
 ## DNSDB::getTypelist()
 # Get a list of record types for various UI dropdowns
@@ -3930,5 +4092,6 @@
 # Returns an arrayref to list of hashrefs perfect for HTML::Template
 sub getTypelist {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $recgroup = shift;
   my $type = shift || $reverse_typemap{A};
@@ -3977,5 +4140,6 @@
 # Returns the ID or undef on failure
 sub parentID {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
 
   my %args = @_;
@@ -4019,5 +4183,6 @@
 # Returns true if $id1 is a parent of $id2, false otherwise
 sub isParent {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $id1 = shift;
   my $type1 = shift; 
@@ -4063,6 +4228,6 @@
     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');
+    return 1 if $dom && $self->isParent($id1, $type1, $dom, 'domain');
+    return 1 if $rdns && $self->isParent($id1, $type1, $rdns, 'revzone');
     # exit here since we've executed the loop below by proxy in the above recursive calls.
     return 0;
@@ -4104,5 +4269,6 @@
 # Returns status, or undef on errors.
 sub zoneStatus {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $id = shift;
   my $revrec = shift;
@@ -4125,5 +4291,5 @@
 
 ##fixme  switch to more consise "Enabled <domain"/"Disabled <domain>" as with users?
-      $resultstr = "Changed ".($revrec eq 'n' ? domainName($dbh, $id) : revName($dbh, $id)).
+      $resultstr = "Changed ".($revrec eq 'n' ? $self->domainName($id) : $self->revName($id)).
 	" state to ".($newstatus ? 'active' : 'inactive');
 
@@ -4131,8 +4297,7 @@
       $loghash{domain_id} = $id if $revrec eq 'n';
       $loghash{rdns_id} = $id if $revrec eq 'y';
-      $loghash{group_id} = parentID($dbh,
-	(id => $id, type => ($revrec eq 'n' ? 'domain' : 'revzone'), revrec => $revrec) );
+      $loghash{group_id} = $self->parentID(id => $id, type => ($revrec eq 'n' ? 'domain' : 'revzone'), revrec => $revrec);
       $loghash{entry} = $resultstr;
-      _log($dbh, %loghash);
+      $self->_log(%loghash);
 
       $dbh->commit;
@@ -4154,28 +4319,41 @@
 
 
+## DNSDB::getZonesByCIDR()
+# Get a list of zone names and IDs that records for a passed CIDR block are within.
+sub getZonesByCIDR {
+  my $self = shift;
+  my $dbh = $self->{dbh};
+  my %args = @_;
+
+  my $result = $dbh->selectall_arrayref("SELECT rdns_id,revnet FROM revzones WHERE revnet >>= ? OR revnet <<= ?",
+	{ Slice => {} }, ($args{cidr}, $args{cidr}) );
+  return $result;
+} # end getZonesByCIDR()
+
+
 ## DNSDB::importAXFR
 # Import a domain via AXFR
 # Takes AXFR host, domain to transfer, group to put the domain in,
-# and optionally:
-#  - active/inactive state flag (defaults to active)
-#  - overwrite-SOA flag (defaults to off)
-#  - overwrite-NS flag (defaults to off, doesn't affect subdomain NS records)
+# and an optional hash containing:
+#  status - active/inactive state flag (defaults to active)
+#  rwsoa - overwrite-SOA flag (defaults to off)
+#  rwns - overwrite-NS flag (defaults to off, doesn't affect subdomain NS records)
+#  merge - flag to automerge A or AAAA records with matching PTR records
 # Returns a status code (OK, WARN, or FAIL) and message - message should be blank
 # if status is OK, but WARN includes conditions that are not fatal but should
 # really be reported.
 sub importAXFR {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $ifrom_in = shift;
   my $zone = shift;
   my $group = shift;
-  my $status = shift;
-  $status = (defined($status) ? $status : 0);	# force sane semantics, and allow passing "null" (inactive) status
-  my $rwsoa = shift || 0;
-  my $rwns = shift || 0;
-  my $newttl = shift;
-
-  my $merge = shift || 0;	# do we attempt to merge A/AAAA and PTR records whenever possible?
-				# do we overload this with the fixme below?
+
+  my %args = @_;
+
 ##fixme:  add mode to delete&replace, merge+overwrite, merge new?
+
+  $args{status} = (defined($args{status}) ? $args{status} : 0);
+  $args{status} = 1 if $args{status} eq 'on'; 
 
   my $nrecs = 0;
@@ -4283,18 +4461,20 @@
     if ($rev eq 'n') {
 ##fixme:  serial
-      $dbh->do("INSERT INTO domains (domain,group_id,status) VALUES (?,?,?)", undef, ($zone,$group,$status) );
+      $dbh->do("INSERT INTO domains (domain,group_id,status) VALUES (?,?,?)", undef,
+        ($zone, $group, $args{status}) );
       # get domain id so we can do the records
       ($zone_id) = $dbh->selectrow_array("SELECT currval('domains_domain_id_seq')");
       $domain_id = $zone_id;
-      _log($dbh, (group_id => $group, domain_id => $domain_id,
-		entry => "[Added ".($status ? 'active' : 'inactive')." domain $zone via AXFR]") );
+      $self->_log(group_id => $group, domain_id => $domain_id,
+		entry => "[Added ".($args{status} ? 'active' : 'inactive')." domain $zone via AXFR]");
     } else {
 ##fixme:  serial
-      $dbh->do("INSERT INTO revzones (revnet,group_id,status) VALUES (?,?,?)", undef, ($cidr,$group,$status) );
+      $dbh->do("INSERT INTO revzones (revnet,group_id,status) VALUES (?,?,?)", undef,
+        ($cidr,$group,$args{status}) );
       # get revzone id so we can do the records
       ($zone_id) = $dbh->selectrow_array("SELECT currval('revzones_rdns_id_seq')");
       $rdns_id = $zone_id;
-      _log($dbh, (group_id => $group, rdns_id => $rdns_id,
-		entry => "[Added ".($status ? 'active' : 'inactive')." reverse zone $cidr via AXFR]") );
+      $self->_log(group_id => $group, rdns_id => $rdns_id,
+		entry => "[Added ".($args{status} ? 'active' : 'inactive')." reverse zone $cidr via AXFR]");
     }
 
@@ -4331,4 +4511,5 @@
       my $ttl = ($newttl ? $newttl : $rr->ttl);	# allow force-override TTLs
       my $host = $rr->name;
+      my $ttl = ($args{newttl} ? $args{newttl} : $rr->ttl);	# allow force-override TTLs
 
       $soaflag = 1 if $type eq 'SOA';
@@ -4360,5 +4541,5 @@
       } elsif ($type eq 'NS') {
 # hmm.  should we warn here if subdomain NS'es are left alone?
-	next if ($rwns && ($rr->name eq $zone));
+	next if ($args{rwns} && ($rr->name eq $zone));
 	if ($rev eq 'y') {
 	  # revzones have records more or less reversed from forward zones.
@@ -4396,5 +4577,5 @@
 	}
       } elsif ($type eq 'SOA') {
-	next if $rwsoa;
+	next if $args{rwsoa};
 	$host = $rr->rname.":".$rr->mname;
 	$val = $rr->refresh.":".$rr->retry.":".$rr->expire.":".$rr->minimum;
@@ -4444,5 +4625,5 @@
       my $logentry = "[AXFR ".($rev eq 'n' ? $zone : $cidr)."] ";
 
-      if ($merge) {
+      if ($args{merge}) {
 	if ($rev eq 'n') {
 	  # importing a domain;  we have A and AAAA records that could be merged with matching PTR records
@@ -4464,5 +4645,5 @@
 		($domain_id, $ettl, $etype, $erid));
 	    $nrecs++;
-	    _log($dbh, (group_id => $group, domain_id => $domain_id, rdns_id => $erdns, entry => $logentry) );
+	    $self->_log(group_id => $group, domain_id => $domain_id, rdns_id => $erdns, entry => $logentry);
 	    next;	# while axfr_next
 	  }
@@ -4486,9 +4667,9 @@
 		($rdns_id, $ettl, $etype, $erid));
 	    $nrecs++;
-	    _log($dbh, (group_id => $group, domain_id => $domid, rdns_id => $rdns_id, entry => $logentry) );
+	    $self->_log(group_id => $group, domain_id => $domid, rdns_id => $rdns_id, entry => $logentry);
 	    next;	# while axfr_next
 	  }
 	} # $rev eq 'y'
-      } # if $merge
+      } # if $args{merge}
 
       # Insert the new record
@@ -4499,5 +4680,5 @@
 
       if ($type eq 'SOA') {
-	# also !$rwsoa, but if that's set, it should be impossible to get here.
+	# also !$args{rwsoa}, but if that's set, it should be impossible to get here.
 	my @tmp1 = split /:/, $host;
 	my @tmp2 = split /:/, $val;
@@ -4513,5 +4694,5 @@
 	$logentry .= " $val', TTL $ttl";
       }
-      _log($dbh, (group_id => $group, domain_id => $domain_id, rdns_id => $rdns_id, entry => $logentry) );
+      $self->_log(group_id => $group, domain_id => $domain_id, rdns_id => $rdns_id, entry => $logentry);
 
     } # while axfr_next
@@ -4525,5 +4706,5 @@
 
     # Overwrite SOA record
-    if ($rwsoa) {
+    if ($args{rwsoa}) {
       $soaflag = 1;
       my $sthgetsoa = $dbh->prepare("SELECT host,val,ttl FROM default_records WHERE group_id=? AND type=?");
@@ -4538,5 +4719,5 @@
 
     # Overwrite NS records
-    if ($rwns) {
+    if ($args{rwns}) {
       $nsflag = 1;
       my $sthgetns = $dbh->prepare("SELECT host,val,ttl FROM default_records WHERE group_id=? AND type=?");
@@ -4584,15 +4765,15 @@
 ## DNSDB::export()
 # Export the DNS database, or a part of it
-# Takes database handle, export type, optional arguments depending on type
+# Takes a string indicating the export type, plus optional arguments depending on type
 # Writes zone data to targets as appropriate for type
 sub export {
-  my $dbh = shift;
+  my $self = shift;
   my $target = shift;
 
   if ($target eq 'tiny') {
-    __export_tiny($dbh,@_);
+    $self->__export_tiny(@_);
   }
 # elsif ($target eq 'foo') {
-#   __export_foo($dbh,@_);
+#   __export_foo(@_);
 #}
 # etc
@@ -4603,8 +4784,9 @@
 ## DNSDB::__export_tiny
 # Internal sub to implement tinyDNS (compatible) export
-# Takes database handle, filehandle to write export to, optional argument(s)
+# Takes filehandle to write export to, optional argument(s)
 # to determine which data gets exported
 sub __export_tiny {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my $datafile = shift;
 
@@ -4625,5 +4807,31 @@
     foreach my $ipprefix (split /[,\s]+/, $lochash->{$location}{iplist}) {
       $ipprefix =~ s/\s+//g;
-      print $datafile "%$location:$ipprefix\n";
+      $ipprefix = new NetAddr::IP $ipprefix;
+##fixme:  how to handle IPv6?
+next if $ipprefix->{isv6};
+      # have to account for /nn CIDR entries.  tinydns only speaks octet-sliced prefix.
+      if ($ipprefix->masklen <= 8) {
+        foreach ($ipprefix->split(8)) {
+          my $tmp = $_->addr;
+          $tmp =~ s/\.\d+\.\d+\.\d+$//;
+          print $datafile "%$location:$tmp\n";
+        }
+      } elsif ($ipprefix->masklen <= 16) {
+        foreach ($ipprefix->split(16)) {
+          my $tmp = $_->addr;
+          $tmp =~ s/\.\d+\.\d+$//;
+          print $datafile "%$location:$tmp\n";
+        }
+      } elsif ($ipprefix->masklen <= 24) {
+        foreach ($ipprefix->split(24)) {
+          my $tmp = $_->addr;
+          $tmp =~ s/\.\d+$//;
+          print $datafile "%$location:$tmp\n";
+        }
+      } else {
+        foreach ($ipprefix->split(32)) {
+          print $datafile "%$location:".$_->addr."\n";
+        }
+      }
     }
     print $datafile "%$location\n" if !$lochash->{$location}{iplist};
@@ -4667,10 +4875,17 @@
 	$ttl = '' if $ttl == '0';
 
-	_printrec_tiny($datafile, 'n', \%recflags,
-		$dom, $host, $type, $val, $dist, $weight, $port, $ttl, $loc, $stamp);
+	# Spaces are evil.
+	$host =~ s/^\s+//;
+	$host =~ s/\s+$//;
+	if ($typemap{$type} ne 'TXT') {
+	  # Leading or trailng spaces could be legit in TXT records.
+	  $val =~ s/^\s+//;
+	  $val =~ s/\s+$//;
+	}
 
 	_printrec_tiny(*ZONECACHE, 'n', \%recflags,
 		$dom, $host, $type, $val, $dist, $weight, $port, $ttl, $loc, $stamp)
 		if *ZONECACHE;
+
 	# in case the zone shrunk, get rid of garbage at the end of the file.
 	truncate(ZONECACHE, tell(ZONECACHE));
@@ -4678,8 +4893,7 @@
 	$recflags{$recid} = 1;
       } # while ($recsth)
-    } else {
-      # domain not changed, stream from cache
-      print $datafile $_ while <ZONECACHE>;
-    }
+    }
+    # stream from cache, whether freshly created or existing
+    print $datafile $_ while <ZONECACHE>;
     close ZONECACHE;
     # mark domain as unmodified
@@ -4723,5 +4937,5 @@
       $soasth->execute($revid);
       my (@zsoa) = $soasth->fetchrow_array();
-      _printrec_tiny($datafile,'y',\%recflags,$revzone,
+      _printrec_tiny(*ZONECACHE,'y',\%recflags,$revzone,
 	$zsoa[0],$zsoa[1],$zsoa[2],$zsoa[3],$zsoa[4],$zsoa[5],$zsoa[6],$zsoa[8],'');
 
@@ -4745,20 +4959,18 @@
 	$ttl = '' if $ttl == '0';
 
-	_printrec_tiny($datafile, 'y', \%recflags, $revzone,
-		$host, $type, $val, $dist, $weight, $port, $ttl, $loc, $stamp);
 	_printrec_tiny(*ZONECACHE, 'y', \%recflags, $revzone,
 		$host, $type, $val, $dist, $weight, $port, $ttl, $loc, $stamp)
 		if *ZONECACHE;
-	# in case the zone shrunk, get rid of garbage at the end of the file.
-	truncate(ZONECACHE, tell(ZONECACHE));
+
+        # in case the zone shrunk, get rid of garbage at the end of the file.
+        truncate(ZONECACHE, tell(ZONECACHE));
 
 	$recflags{$recid} = 1;
       } # while ($recsth)
-    } else {
-      # zone not changed, stream from cache
-      print $datafile $_ while <ZONECACHE>;
-    }
+    }
+    # stream from cache, whether freshly created or existing
+    print $datafile $_ while <ZONECACHE>;
     close ZONECACHE;
-    # mark domain as unmodified
+    # mark zone as unmodified
     $zonesth->execute($revid);
   } # while ($domsth)
@@ -4806,4 +5018,5 @@
       next if $$recflags{$ip};
       $$recflags{$ip}++;
+      next if $hpat eq '%blank%';	# Allows blanking a subnet so no records are published.
       my $rec = $hpat;	# start fresh with the template for each IP
       _template4_expand(\$rec, $ip);
@@ -5092,5 +5305,6 @@
 # Sends notification mail to recipients regarding a DNSDB operation
 sub mailNotify {
-  my $dbh = shift;
+  my $self = shift;
+  my $dbh = $self->{dbh};
   my ($subj,$message) = @_;
 
