Ignore:
Timestamp:
12/11/13 16:01:18 (10 years ago)
Author:
Kris Deugau
Message:

/branches/stable

Merge reverse DNS work and object conversion from /trunk, part 5

Includes changes through r543 with a few more minor conflicts.

Location:
branches/stable
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • branches/stable

  • branches/stable/DNSDB.pm

    r547 r548  
    33##
    44# $Id$
    5 # Copyright 2008-2012 Kris Deugau <kdeugau@deepnet.cx>
     5# Copyright 2008-2013 Kris Deugau <kdeugau@deepnet.cx>
    66#
    77#    This program is free software: you can redistribute it and/or modify
     
    2828use Crypt::PasswdMD5;
    2929use Net::SMTP;
    30 use NetAddr::IP qw(:lower);
     30use NetAddr::IP 4.027 qw(:lower);
    3131use POSIX;
    3232use Fcntl qw(:flock);
     33use Time::TAI64 qw(:tai64);
    3334
    3435use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
     
    4041        &getPermissions &changePermissions &comparePermissions
    4142        &changeGroup
    42         &loadConfig &connectDB &finish
     43        &connectDB &finish
    4344        &addDomain &delZone &domainName &revName &domainID &revID &addRDNS
    4445        &getZoneCount &getZoneList &getZoneLocation
     
    4950        &addLoc &updateLoc &delLoc &getLoc
    5051        &getLocCount &getLocList &getLocDropdown
    51         &getSOA &updateSOA &getRecLine &getDomRecs &getRecCount
     52        &getSOA &updateSOA &getRecLine &getRecList &getRecCount
    5253        &addRec &updateRec &delRec
    5354        &getLogCount &getLogEntries
     
    5960        &export
    6061        &mailNotify
    61         %typemap %reverse_typemap %config
     62        %typemap %reverse_typemap
    6263        @permtypes $permlist %permchains
    6364        );
     
    6869                &getPermissions &changePermissions &comparePermissions
    6970                &changeGroup
    70                 &loadConfig &connectDB &finish
     71                &connectDB &finish
    7172                &addDomain &delZone &domainName &revName &domainID &revID &addRDNS
    7273                &getZoneCount &getZoneList &getZoneLocation
     
    7778                &addLoc &updateLoc &delLoc &getLoc
    7879                &getLocCount &getLocList &getLocDropdown
    79                 &getSOA &updateSOA &getRecLine &getDomRecs &getRecCount
     80                &getSOA &updateSOA &getRecLine &getRecList &getRecCount
    8081                &addRec &updateRec &delRec
    8182                &getLogCount &getLogEntries
     
    8788                &export
    8889                &mailNotify
    89                 %typemap %reverse_typemap %config
     90                %typemap %reverse_typemap
    9091                @permtypes $permlist %permchains
    9192                )]
     
    9495our $errstr = '';
    9596our $resultstr = '';
    96 
    97 # Halfway sane defaults for SOA, TTL, etc.
    98 # serial defaults to 0 for convenience.
    99 # value will be either YYYYMMDDNN for BIND/etc, or auto-internal for tinydns
    100 our %def = qw (
    101         contact hostmaster.DOMAIN
    102         prins   ns1.myserver.com
    103         serial  0
    104         soattl  86400
    105         refresh 10800
    106         retry   3600
    107         expire  604800
    108         minttl  10800
    109         ttl     10800
    110 );
    11197
    11298# Arguably defined wholly in the db, but little reason to change without supporting code changes
     
    135121our %typemap;
    136122our %reverse_typemap;
    137 
    138 # Prepopulate a basic config.  Note some of these *will* cause errors if left unset.
    139 # note:  add appropriate stanzas in loadConfig to parse these
    140 our %config = (
    141                 # Database connection info
    142                 dbname  => 'dnsdb',
    143                 dbuser  => 'dnsdb',
    144                 dbpass  => 'secret',
    145                 dbhost  => '',
    146 
    147                 # Email notice settings
    148                 mailhost        => 'smtp.example.com',
    149                 mailnotify      => 'dnsdb@example.com', # to
    150                 mailsender      => 'dnsdb@example.com', # from
    151                 mailname        => 'DNS Administration',
    152                 orgname         => 'Example Corp',
    153                 domain          => 'example.com',
    154 
    155                 # Template directory
    156                 templatedir     => 'templates/',
    157 # fmeh.  this is a real web path, not a logical internal one.  hm..
    158 #               cssdir  => 'templates/',
    159                 sessiondir      => 'session/',
    160                 exportcache     => 'cache/',
    161 
    162                 # Session params
    163                 timeout         => '3600',      # 1 hour default
    164 
    165                 # Other miscellanea
    166                 log_failures    => 1,   # log all evarthing by default
    167                 perpage         => 15,
    168                 maxfcgi         => 100, # reasonable default?
    169         );
    170123
    171124## (Semi)private variables
     
    221174  my $class = ref($this) || $this;
    222175  my %args = @_;
    223 ##fixme?  to ponder:  do we do some magic if the caller sets eg dbname to prevent parsing of the config file?
    224   if (!loadConfig(basename => $args{configfile})) {
    225     warn "Using default configuration;  unable to load custom settings: $errstr\n";
    226   }
    227   my $self = \%config;
    228   $self->{configfile} = $args{configfile};
     176
     177  # Prepopulate a basic config.  Note some of these *will* cause errors if left unset.
     178  # note:  add appropriate stanzas in __cfgload() to parse these
     179  my %defconfig = (
     180                # The only configuration options not loadable from a config file.
     181                configfile => "/etc/dnsdb/dnsdb.conf",  ##CFG_LEAF##
     182
     183                # Database connection info
     184                dbname  => 'dnsdb',
     185                dbuser  => 'dnsdb',
     186                dbpass  => 'secret',
     187                dbhost  => '',
     188
     189                # Email notice settings
     190                mailhost        => 'smtp.example.com',
     191                mailnotify      => 'dnsdb@example.com', # to
     192                mailsender      => 'dnsdb@example.com', # from
     193                mailname        => 'DNS Administration',
     194                orgname         => 'Example Corp',
     195                domain          => 'example.com',
     196
     197                # Template directory
     198                templatedir     => 'templates/',
     199# fmeh.  this is a real web path, not a logical internal one.  hm..
     200#               cssdir  => 'templates/',
     201                sessiondir      => 'session/',
     202                exportcache     => 'cache/',
     203
     204                # Session params
     205                timeout         => '1h',        # passed as-is to CGI::Session
     206
     207                # Other miscellanea
     208                log_failures    => 1,   # log all evarthing by default
     209                perpage         => 15,
     210                max_fcgi_requests => 100,       # reasonable default?
     211                force_refresh   => 1,
     212                lowercase       => 0,   # mangle as little as possible by default
     213        );
     214
     215  # Config file parse calls.
     216  # If we are passed a blank argument for $args{configfile},
     217  #   we should NOT parse the default config file - we will
     218  #   rely on hardcoded defaults OR caller-specified values.
     219  # If we are passed a non-blank argument, parse that file.
     220  # If no config file is specified, parse the default one.
     221  my %siteconfig;
     222  if (defined($args{configfile})) {
     223    if ($args{configfile}) {
     224      return if !__cfgload($args{configfile}, \%siteconfig);
     225    }
     226  } else {
     227    return if !__cfgload($defconfig{configfile}, \%siteconfig);
     228  }
     229
     230  # Assemble the object.  Apply configuration hashes in order of precedence.
     231  my $self = {
     232        # Hardcoded defaults
     233        %defconfig,
     234        # Default config file OR caller-specified one, loaded above
     235        %siteconfig,
     236        # Caller-specified arguments
     237        %args
     238        };
    229239  bless $self, $class;
     240
     241  # Several settings are booleans.  Handle multiple possible ways of setting them.
     242  for my $boolopt ('log_failures', 'force_refresh', 'lowercase') {
     243    if ($self->{$boolopt} ne '1' && $self->{$boolopt} ne '0') {
     244      # true/false, on/off, yes/no all valid.
     245      if ($self->{$boolopt} =~ /^(?:true|false|t|f|on|off|yes|no)$/) {
     246        if ($self->{$boolopt} =~ /(?:true|t|on|yes)/) {
     247         $self->{$boolopt} = 1;
     248        } else {
     249         $self->{$boolopt} = 0;
     250        }
     251      } else {
     252        warn "Bad $boolopt setting $self->{$boolopt}\n";
     253        $self->{$boolopt} = 1;
     254      }
     255    }
     256  }
     257
     258  # Try to connect to the DB, and initialize a number of handy globals.
    230259  $self->{dbh} = connectDB($self->{dbname}, $self->{dbuser}, $self->{dbpass}, $self->{dbhost}) or return;
    231260  $self->initGlobals();
     
    236265sub DESTROY {
    237266  my $self = shift;
    238   $self->{dbh}->disconnect;
     267  $self->{dbh}->disconnect if $self->{dbh};
    239268}
     269
     270sub errstr { $DNSDB::errstr; }
    240271
    241272##
     
    370401
    371402##fixme:  farm out the actual logging to different subs for file, syslog, internal, etc based on config
    372 #  if ($config{log_channel} eq 'sql') {
     403#  if ($self->{log_channel} eq 'sql') {
    373404  $dbh->do("INSERT INTO log (domain_id,rdns_id,group_id,entry,user_id,email,name) VALUES (?,?,?,?,?,?,?)",
    374405        undef,
    375406        ($args{domain_id}, $args{rdns_id}, $args{group_id}, $args{entry},
    376407                $self->{loguserid}, $self->{logusername}, $self->{logfullname}) );
    377 #  } elsif ($config{log_channel} eq 'file') {
    378 #  } elsif ($config{log_channel} eq 'syslog') {
     408#  } elsif ($self->{log_channel} eq 'file') {
     409#  } elsif ($self->{log_channel} eq 'syslog') {
    379410#  }
    380411} # end _log
     
    409440  # or the intended parent domain for live records.
    410441  my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
    411   ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
     442  ${$args{host}} =~ s/\.*$/\.$pname/ if (${$args{host}} ne '@' && ${$args{host}} !~ /$pname$/);
    412443
    413444  # Check IP is well-formed, and that it's a v4 address
     
    526557        ${$args{val}} = "ZONE,${$args{val}}" unless ${$args{val}} =~ /^ZONE/;
    527558      }
    528       ${$args{host}} =~ s/\.*$/\.$config{domain}/ if ${$args{host}} !~ /(?:$config{domain}|ADMINDOMAIN)$/;
     559      ${$args{host}} =~ s/\.*$/\.$self->{domain}/ if ${$args{host}} !~ /(?:$self->{domain}|ADMINDOMAIN)$/;
    529560    }
    530561
     
    12021233  #major patterns:
    12031234  #dashed IP, forward and reverse
     1235  #underscoreed IP, forward and reverse
    12041236  #dotted IP, forward and reverse (even if forward is... dumb)
    1205   # -> %r for reverse, %i for forward, leading - or . to indicate separator, defaults to -
     1237  # -> %r for reverse, %i for forward, leading -, _, or . to indicate separator, defaults to -
    12061238  # %r or %-r   => %4d-%3d-%2d-%1d
     1239  # %_r         => %4d_%3d_%2d_%1d
    12071240  # %.r         => %4d.%3d.%2d.%1d
    12081241  # %i or %-i   => %1d-%2d-%3d-%4d
     1242  # %_i         => %1d_%2d_%3d_%4d
    12091243  # %.i         => %1d.%2d.%3d.%4d
    12101244  $$tmpl =~ s/\%r/\%4d-\%3d-\%2d-\%1d/g;
    1211   $$tmpl =~ s/\%([-.])r/\%4d$1\%3d$1\%2d$1\%1d/g;
     1245  $$tmpl =~ s/\%([-._])r/\%4d$1\%3d$1\%2d$1\%1d/g;
    12121246  $$tmpl =~ s/\%i/\%1d-\%2d-\%3d-\%4d/g;
    1213   $$tmpl =~ s/\%([-.])i/\%1d$1\%2d$1\%3d$1\%4d/g;
     1247  $$tmpl =~ s/\%([-._])i/\%1d$1\%2d$1\%3d$1\%4d/g;
    12141248
    12151249  #hex-coded IP
     
    12321266} # _template4_expand()
    12331267
     1268# Broad syntactic check on the hostname.  Checks for valid characters, correctly-expandable template patterns.
     1269# Takes the hostname, type, and live/default and forward/reverse flags
     1270# Returns true/false, sets errstr on failures
     1271sub _check_hostname_form {
     1272  my ($hname,$rectype,$defrec,$revrec) = @_;
     1273
     1274  if ($hname =~ /\%/ && ($rectype == 65282 || $rectype == 65283) ) {
     1275    my $tmphost = $hname;
     1276    # we don't actually need to test with the real IP passed;  that saves a bit of fiddling.
     1277    _template4_expand(\$tmphost, '10.10.10.10');
     1278    if ($tmphost =~ /\%/) {
     1279      $errstr = "Invalid template $hname";
     1280      return;
     1281    }
     1282  } elsif ($revrec eq 'y') {
     1283    # Reverse zones don't support @ in hostnames
     1284    # Also skip failure on revzone TXT records;  the hostname contains the TXT content in that case.
     1285    if ($rectype != $reverse_typemap{TXT} && lc($hname) !~ /^[0-9a-z_.-]+$/) {
     1286      $errstr = "Hostnames may not contain anything other than (0-9 a-z . _)";
     1287      return;
     1288    }
     1289  } else {
     1290    if (lc($hname) !~ /^(?:[0-9a-z_.-]+|@)$/) {
     1291      # Don't mention @, because it would be far too wordy to explain the nuance of @
     1292      $errstr = "Hostnames may not contain anything other than (0-9 a-z . _)";
     1293      return;
     1294    }
     1295  }
     1296  return 1;
     1297} # _check_hostname_form()
     1298
    12341299
    12351300##
     
    12371302##
    12381303
    1239 ## DNSDB::loadConfig()
    1240 # Load the minimum required initial state (DB connect info) from a config file
    1241 # Load misc other bits while we're at it.
    1242 # Takes an optional hash that may contain:
    1243 #  - basename and config path to look for
    1244 # Populates the %config and %def hashes
    1245 sub loadConfig {
    1246   my %args = @_;
    1247   $args{configfile} = '' if !$args{configfile};
    1248 
    1249 ##fixme  this is *intended* to load a system-default config template, and allow
    1250 # overriding on a per-tool or per-web-UI-instance basis with a secondary config
    1251 # file.  The "default" config file can't be deleted in the current form.
    1252 
    1253   my $deferr = '';      # place to put error from default config file in case we can't find either one
    1254 
    1255   my $configroot = "/etc/dnsdb";        ##CFG_LEAF##
    1256   $configroot = '' if $args{configfile} =~ m|^/|;  # allow passed siteconfig to specify an arbitrary absolute path
    1257   $args{configfile} .= ".conf" if $args{configfile} !~ /\.conf$/;
    1258   my $defconfig = "$configroot/dnsdb.conf";
    1259   my $siteconfig = "$configroot/$args{configfile}";
    1260 
    1261   # System defaults
    1262   __cfgload("$defconfig") or $deferr = $errstr;
    1263 
    1264   # Per-site-ish settings.
    1265   if ($args{configfile} ne '.conf') {
    1266     unless (__cfgload("$siteconfig")) {
    1267       $errstr = ($deferr ? "Error opening default config file $defconfig: $deferr\n" : '').
    1268         "Error opening site config file $siteconfig";
    1269       return;
    1270     }
    1271   }
    1272 
    1273   # Munge log_failures.
    1274   if ($config{log_failures} ne '1' && $config{log_failures} ne '0') {
    1275     # true/false, on/off, yes/no all valid.
    1276     if ($config{log_failures} =~ /^(?:true|false|on|off|yes|no)$/) {
    1277       if ($config{log_failures} =~ /(?:true|on|yes)/) {
    1278         $config{log_failures} = 1;
    1279       } else {
    1280         $config{log_failures} = 0;
    1281       }
    1282     } else {
    1283       $errstr = "Bad log_failures setting $config{log_failures}";
    1284       $config{log_failures} = 1;
    1285       # Bad setting shouldn't be fatal.
    1286       # return 2;
    1287     }
    1288   }
    1289 
    1290   # All good, clear the error and go home.
    1291   $errstr = '';
    1292   return 1;
    1293 } # end loadConfig()
    1294 
    1295 
    12961304## DNSDB::__cfgload()
    12971305# Private sub to parse a config file and load it into %config
    1298 # Takes a filename
     1306# Takes a filename and a hashref to put the parsed entries in
    12991307sub __cfgload {
    13001308  $errstr = '';
    13011309  my $cfgfile = shift;
     1310  my $cfg = shift;
    13021311
    13031312  if (open CFG, "<$cfgfile") {
     
    13101319#    $mode = $1 if /^\[(a-z)+]/;
    13111320    # DB connect info
    1312       $config{dbname}   = $1 if /^dbname\s*=\s*([a-z0-9_.-]+)/i;
    1313       $config{dbuser}   = $1 if /^dbuser\s*=\s*([a-z0-9_.-]+)/i;
    1314       $config{dbpass}   = $1 if /^dbpass\s*=\s*([a-z0-9_.-]+)/i;
    1315       $config{dbhost}   = $1 if /^dbhost\s*=\s*([a-z0-9_.-]+)/i;
    1316       # SOA defaults
    1317       $def{contact}     = $1 if /^contact\s*=\s*([a-z0-9_.-]+)/i;
    1318       $def{prins}       = $1 if /^prins\s*=\s*([a-z0-9_.-]+)/i;
    1319       $def{soattl}      = $1 if /^soattl\s*=\s*(\d+)/i;
    1320       $def{refresh}     = $1 if /^refresh\s*=\s*(\d+)/i;
    1321       $def{retry}       = $1 if /^retry\s*=\s*(\d+)/i;
    1322       $def{expire}      = $1 if /^expire\s*=\s*(\d+)/i;
    1323       $def{minttl}      = $1 if /^minttl\s*=\s*(\d+)/i;
    1324       $def{ttl}         = $1 if /^ttl\s*=\s*(\d+)/i;
     1321      $cfg->{dbname}    = $1 if /^dbname\s*=\s*([a-z0-9_.-]+)/i;
     1322      $cfg->{dbuser}    = $1 if /^dbuser\s*=\s*([a-z0-9_.-]+)/i;
     1323      $cfg->{dbpass}    = $1 if /^dbpass\s*=\s*([a-z0-9_.-]+)/i;
     1324      $cfg->{dbhost}    = $1 if /^dbhost\s*=\s*([a-z0-9_.-]+)/i;
    13251325      # Mail settings
    1326       $config{mailhost}         = $1 if /^mailhost\s*=\s*([a-z0-9_.-]+)/i;
    1327       $config{mailnotify}       = $1 if /^mailnotify\s*=\s*([a-z0-9_.\@-]+)/i;
    1328       $config{mailsender}       = $1 if /^mailsender\s*=\s*([a-z0-9_.\@-]+)/i;
    1329       $config{mailname}         = $1 if /^mailname\s*=\s*([a-z0-9\s_.-]+)/i;
    1330       $config{orgname}          = $1 if /^orgname\s*=\s*([a-z0-9\s_.,'-]+)/i;
    1331       $config{domain}           = $1 if /^domain\s*=\s*([a-z0-9_.-]+)/i;
     1326      $cfg->{mailhost}          = $1 if /^mailhost\s*=\s*([a-z0-9_.-]+)/i;
     1327      $cfg->{mailnotify}        = $1 if /^mailnotify\s*=\s*([a-z0-9_.\@-]+)/i;
     1328      $cfg->{mailsender}        = $1 if /^mailsender\s*=\s*([a-z0-9_.\@-]+)/i;
     1329      $cfg->{mailname}          = $1 if /^mailname\s*=\s*([a-z0-9\s_.-]+)/i;
     1330      $cfg->{orgname}           = $1 if /^orgname\s*=\s*([a-z0-9\s_.,'-]+)/i;
     1331      $cfg->{domain}            = $1 if /^domain\s*=\s*([a-z0-9_.-]+)/i;
    13321332      # session - note this is fed directly to CGI::Session
    1333       $config{timeout}          = $1 if /^[tT][iI][mM][eE][oO][uU][tT]\s*=\s*(\d+[smhdwMy]?)/;
    1334       $config{sessiondir}       = $1 if m{^sessiondir\s*=\s*([a-z0-9/_.-]+)}i;
     1333      $cfg->{timeout}           = $1 if /^[tT][iI][mM][eE][oO][uU][tT]\s*=\s*(\d+[smhdwMy]?)/;
     1334      $cfg->{sessiondir}        = $1 if m{^sessiondir\s*=\s*([a-z0-9/_.-]+)}i;
    13351335      # misc
    1336       $config{log_failures}     = $1 if /^log_failures\s*=\s*([a-z01]+)/i;
    1337       $config{perpage}          = $1 if /^perpage\s*=\s*(\d+)/i;
    1338       $config{exportcache}      = $1 if m{^exportcache\s*=\s*([a-z0-9/_.-]+)}i;
     1336      $cfg->{log_failures}      = $1 if /^log_failures\s*=\s*([a-z01]+)/i;
     1337      $cfg->{perpage}           = $1 if /^perpage\s*=\s*(\d+)/i;
     1338      $cfg->{exportcache}       = $1 if m{^exportcache\s*=\s*([a-z0-9/_.-]+)}i;
     1339      $cfg->{lowercase}         = $1 if /^lowercase\s*=\s*([a-z01]+)/i;
     1340# not supported in dns.cgi yet
     1341#      $cfg->{templatedir}      = $1 if m{^templatedir\s*=\s*([a-z0-9/_.-]+)}i;
     1342#      $cfg->{templateoverride} = $1 if m{^templateoverride\s*=\s*([a-z0-9/_.-]+)}i;
    13391343      # RPC options
    1340       $config{rpcmode}          = $1 if /^rpc_mode\s*=\s*(socket|HTTP|XMLRPC)\s*$/i;
    1341       $config{maxfcgi}          = $1 if /^max_fcgi_requests\s*=\s*(\d+)\s*$/i;
     1344      $cfg->{rpcmode}           = $1 if /^rpc_mode\s*=\s*(socket|HTTP|XMLRPC)\s*$/i;
     1345      $cfg->{maxfcgi}           = $1 if /^max_fcgi_requests\s*=\s*(\d+)\s*$/i;
     1346      $cfg->{force_refresh}     = $1 if /^force_refresh\s*=\s*([a-z01]+)/i;
    13421347      if (my ($tmp) = /^rpc_iplist\s*=\s*(.+)/i) {
    13431348        my @ips = split /[,\s]+/, $tmp;
    13441349        my $rpcsys = shift @ips;
    1345         push @{$config{rpcacl}{$rpcsys}}, @ips;
     1350        push @{$cfg->{rpcacl}{$rpcsys}}, @ips;
    13461351      }
    13471352    }
    13481353    close CFG;
    13491354  } else {
    1350     $errstr = $!;
     1355    $errstr = "Couldn't load configuration file $cfgfile: $!";
    13511356    return;
    13521357  }
     
    15311536# about having to open a file or a syslog channel
    15321537##fixme Need to call _initActionLog_blah() for various logging channels, configured
    1533 # via dnsdb.conf, in $config{log_channel} or something
     1538# via dnsdb.conf, in $self->{log_channel} or something
    15341539# See https://secure.deepnet.cx/trac/dnsadmin/ticket/21
    15351540sub initActionLog {
     
    15521557
    15531558  # convert to real check once we have other logging channels
    1554   # if ($config{log_channel} eq 'sql') {
     1559  # if ($self->{log_channel} eq 'sql') {
    15551560  #   Open Log, Sez Me!
    15561561  # }
     
    15941599  my $sth = $dbh->prepare($sql);
    15951600
    1596   $sth->execute($id) or die "argh: ".$sth->errstr;
     1601##fixme?  we don't trap other plain SELECT errors
     1602  $sth->execute($id);
    15971603
    15981604#  my $permref = $sth->fetchrow_hashref;
     
    17781784    my $msg = $@;
    17791785    eval { $dbh->rollback; };
    1780     if ($config{log_failures}) {
     1786    if ($self->{log_failures}) {
    17811787      $self->_log(group_id => $oldgid, entry => "Error moving $type $entname to $newgname: $msg");
    17821788      $dbh->commit;     # since we enabled transactions earlier
     
    18021808  my $self = shift;
    18031809  my $dbh = $self->{dbh};
    1804   return ('FAIL',"Need database handle") if !$dbh;
    18051810  my $domain = shift;
    1806   return ('FAIL',"Domain must not be blank") if !$domain;
     1811  return ('FAIL',"Domain must not be blank\n") if !$domain;
    18071812  my $group = shift;
    1808   return ('FAIL',"Need group") if !defined($group);
     1813  return ('FAIL',"Group must be specified\n") if !defined($group);
    18091814  my $state = shift;
    1810   return ('FAIL',"Need domain status") if !defined($state);
     1815  return ('FAIL',"Domain status must be specified\n") if !defined($state);
     1816  my $defloc = shift || '';
    18111817
    18121818  $state = 1 if $state =~ /^active$/;
     
    18171823  return ('FAIL',"Invalid domain status") if $state !~ /^\d+$/;
    18181824
     1825  $domain = lc($domain) if $self->{lowercase};
     1826
    18191827  return ('FAIL', "Invalid characters in domain") if $domain !~ /^[a-zA-Z0-9_.-]+$/;
    18201828
     
    18361844  eval {
    18371845    # insert the domain...
    1838     $dbh->do("INSERT INTO domains (domain,group_id,status) VALUES (?,?,?)", undef, ($domain, $group, $state));
     1846    $dbh->do("INSERT INTO domains (domain,group_id,status,default_location) VALUES (?,?,?,?)", undef,
     1847        ($domain, $group, $state, $defloc));
    18391848
    18401849    # get the ID...
     
    18471856    # ... and now we construct the standard records from the default set.  NB:  group should be variable.
    18481857    my $sth = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl FROM default_records WHERE group_id=?");
    1849     my $sth_in = $dbh->prepare("INSERT INTO records (domain_id,host,type,val,distance,weight,port,ttl)".
    1850         " VALUES ($dom_id,?,?,?,?,?,?,?)");
     1858    my $sth_in = $dbh->prepare("INSERT INTO records (domain_id,host,type,val,distance,weight,port,ttl,location)".
     1859        " VALUES ($dom_id,?,?,?,?,?,?,?,?)");
    18511860    $sth->execute($group);
    1852     while (my ($host,$type,$val,$dist,$weight,$port,$ttl) = $sth->fetchrow_array()) {
     1861    while (my ($host, $type, $val, $dist, $weight, $port, $ttl) = $sth->fetchrow_array()) {
    18531862      $host =~ s/DOMAIN/$domain/g;
    18541863      $val =~ s/DOMAIN/$domain/g;
    1855       $sth_in->execute($host,$type,$val,$dist,$weight,$port,$ttl);
     1864      $sth_in->execute($host, $type, $val, $dist, $weight, $port, $ttl, $defloc);
    18561865      if ($typemap{$type} eq 'SOA') {
    18571866        my @tmp1 = split /:/, $host;
     
    18771886    eval { $dbh->rollback; };
    18781887    $self->_log(group_id => $group, entry => "Failed adding domain $domain ($msg)")
    1879         if $config{log_failures};
     1888        if $self->{log_failures};
    18801889    $dbh->commit;       # since we enabled transactions earlier
    18811890    return ('FAIL',$msg);
     
    19081917  return ('FAIL', ($revrec eq 'n' ? 'Domain' : 'Reverse zone')." ID $zoneid doesn't exist") if !$zone;
    19091918
    1910   # Set this up here since we may use if if $config{log_failures} is enabled
     1919  # Set this up here since we may use if if $self->{log_failures} is enabled
    19111920  my %loghash;
    19121921  $loghash{domain_id} = $zoneid if $revrec eq 'n';
     
    19561965    eval { $dbh->rollback; };
    19571966    $loghash{entry} = "Error deleting $zone: $msg ($failmsg)";
    1958     if ($config{log_failures}) {
     1967    if ($self->{log_failures}) {
    19591968      $self->_log(%loghash);
    19601969      $dbh->commit;     # since we enabled transactions earlier
     
    20082017  my ($domid) = $dbh->selectrow_array("SELECT domain_id FROM domains WHERE lower(domain) = lower(?)",
    20092018        undef, ($domain) );
    2010   $errstr = $DBI::errstr if !$domid;
     2019  if (!$domid) {
     2020    if ($dbh->err) {
     2021      $errstr = $DBI::errstr;
     2022    } else {
     2023      $errstr = "Domain $domain not present";
     2024    }
     2025  }
    20112026  return $domid if $domid;
    20122027} # end domainID()
     
    20222037  my $revzone = shift;
    20232038  my ($revid) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet=?", undef, ($revzone) );
    2024   $errstr = $DBI::errstr if !$revid;
     2039  if (!$revid) {
     2040    if ($dbh->err) {
     2041      $errstr = $DBI::errstr;
     2042    } else {
     2043      $errstr = "Reverse zone $revzone not present";
     2044    }
     2045  }
    20252046  return $revid if $revid;
    20262047} # end revID()
     
    20892110      }
    20902111
    2091       $host =~ s/ADMINDOMAIN/$config{domain}/g;
     2112      $host =~ s/ADMINDOMAIN/$self->{domain}/g;
    20922113
    20932114      # Check to make sure the IP stubs will fit in the zone.  Under most usage failures here should be rare.
     
    21832204    eval { $dbh->rollback; };
    21842205    $self->_log(group_id => $group, entry => "Failed adding reverse zone $zone ($msg)")
    2185         if $config{log_failures};
     2206        if $self->{log_failures};
    21862207    $dbh->commit;       # since we enabled transactions earlier
    21872208    return ('FAIL',$msg);
     
    22112232
    22122233  my %args = @_;
     2234
     2235  # Fail on bad curgroup argument.  There's no sane fallback on this one.
     2236  if (!$args{curgroup} || $args{curgroup} !~ /^\d+$/) {
     2237    $errstr = "Bad or missing curgroup argument";
     2238    return;
     2239  }
     2240  # Fail on bad childlist argument.  This could be sanely ignored if bad, maybe.
     2241  if ($args{childlist} && $args{childlist} !~ /^[\d,]+$/) {
     2242    $errstr = "Bad childlist argument";
     2243    return;
     2244  }
    22132245
    22142246  my @filterargs;
     
    22502282  $args{sortorder} = 'ASC' if !$args{sortorder} || !grep /^$args{sortorder}$/, ('ASC','DESC');
    22512283  $args{offset} = 0 if !$args{offset} || $args{offset} !~ /^(?:all|\d+)$/;
     2284
     2285  # Fail on bad curgroup argument.  There's no sane fallback on this one.
     2286  if (!$args{curgroup} || $args{curgroup} !~ /^\d+$/) {
     2287    $errstr = "Bad or missing curgroup argument";
     2288    return;
     2289  }
     2290  # Fail on bad childlist argument.  This could be sanely ignored if bad, maybe.
     2291  if ($args{childlist} && $args{childlist} !~ /^[\d,]+$/) {
     2292    $errstr = "Bad childlist argument";
     2293    return;
     2294  }
    22522295
    22532296  my @filterargs;
     
    22772320  # A common tail.
    22782321  $sql .= " ORDER BY ".($args{sortby} eq 'group' ? 'groups.group_name' : $args{sortby})." $args{sortorder} ".
    2279         ($args{offset} eq 'all' ? '' : " LIMIT $config{perpage}".
    2280         " OFFSET ".$args{offset}*$config{perpage});
     2322        ($args{offset} eq 'all' ? '' : " LIMIT $self->{perpage}".
     2323        " OFFSET ".$args{offset}*$self->{perpage});
    22812324  my $sth = $dbh->prepare($sql);
    22822325  $sth->execute(@filterargs);
     
    24042447    my $msg = $@;
    24052448    eval { $dbh->rollback; };
    2406     if ($config{log_failures}) {
     2449    if ($self->{log_failures}) {
    24072450      $self->_log(group_id => $pargroup, entry => "Failed to add group $groupname: $msg");
    24082451      $dbh->commit;
     
    24482491    my ($domcnt) = $dbh->selectrow_array("SELECT count(*) FROM domains WHERE group_id=?", undef, ($groupid));
    24492492    die "$domcnt domains still in group\n" if $domcnt;
     2493    my ($revcnt) = $dbh->selectrow_array("SELECT count(*) FROM revzones WHERE group_id=?", undef, ($groupid));
     2494    die "$revcnt reverse zones still in group\n" if $revcnt;
    24502495    my ($usercnt) = $dbh->selectrow_array("SELECT count(*) FROM users WHERE group_id=?", undef, ($groupid));
    24512496    die "$usercnt users still in group\n" if $usercnt;
     
    24682513    my $msg = $@;
    24692514    eval { $dbh->rollback; };
    2470     if ($config{log_failures}) {
     2515    if ($self->{log_failures}) {
    24712516      $self->_log(group_id => $parid, entry => "$failmsg: $msg");
    24722517      $dbh->commit;     # since we enabled transactions earlier
     
    25452590  my %args = @_;
    25462591
     2592  # Fail on bad curgroup argument.  There's no sane fallback on this one.
     2593  if (!$args{curgroup} || $args{curgroup} !~ /^\d+$/) {
     2594    $errstr = "Bad or missing curgroup argument";
     2595    return;
     2596  }
     2597  # Fail on bad childlist argument.  This could be sanely ignored if bad, maybe.
     2598  if ($args{childlist} && $args{childlist} !~ /^[\d,]+$/) {
     2599    $errstr = "Bad childlist argument";
     2600    return;
     2601  }
     2602
    25472603  my @filterargs;
    2548 
    25492604  $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/;
    25502605  push @filterargs, "^$args{startwith}" if $args{startwith};
     
    25712626  my %args = @_;
    25722627
     2628  # Fail on bad curgroup argument.  There's no sane fallback on this one.
     2629  if (!$args{curgroup} || $args{curgroup} !~ /^\d+$/) {
     2630    $errstr = "Bad or missing curgroup argument";
     2631    return;
     2632  }
     2633  # Fail on bad childlist argument.  This could be sanely ignored if bad, maybe.
     2634  if ($args{childlist} && $args{childlist} !~ /^[\d,]+$/) {
     2635    $errstr = "Bad childlist argument";
     2636    return;
     2637  }
     2638
    25732639  my @filterargs;
    2574 
    25752640  $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/;
    25762641  push @filterargs, "^$args{startwith}" if $args{startwith};
     
    25782643
    25792644  # protection against bad or missing arguments
    2580   $args{sortorder} = 'ASC' if !$args{sortorder};
     2645  $args{sortorder} = 'ASC' if !$args{sortorder} || !grep /^$args{sortorder}$/, ('ASC','DESC');
     2646  $args{sortby} = 'group' if !$args{sortby} || $args{sortby} !~ /^[\w_.]+$/;
    25812647  $args{offset} = 0 if !$args{offset} || $args{offset} !~ /^(?:all|\d+)$/;
    25822648
     
    25942660        " GROUP BY g.group_id, g.group_name, g2.group_name ".
    25952661        " ORDER BY $args{sortby} $args{sortorder} ".
    2596         ($args{offset} eq 'all' ? '' : " LIMIT $config{perpage} OFFSET ".$args{offset}*$config{perpage});
     2662        ($args{offset} eq 'all' ? '' : " LIMIT $self->{perpage} OFFSET ".$args{offset}*$self->{perpage});
    25972663  my $glist = $dbh->selectall_arrayref($sql, { Slice => {} }, (@filterargs) );
    25982664  $errstr = $dbh->errstr if !$glist;
     
    27492815    my $msg = $@;
    27502816    eval { $dbh->rollback; };
    2751     if ($config{log_failures}) {
     2817    if ($self->{log_failures}) {
    27522818      $self->_log(group_id => $group, entry => "Error adding user $username: $msg");
    27532819      $dbh->commit;     # since we enabled transactions earlier
     
    27722838  my %args = @_;
    27732839
     2840  # Fail on bad curgroup argument.  There's no sane fallback on this one.
     2841  if (!$args{curgroup} || $args{curgroup} !~ /^\d+$/) {
     2842    $errstr = "Bad or missing curgroup argument";
     2843    return;
     2844  }
     2845  # Fail on bad childlist argument.  This could be sanely ignored if bad, maybe.
     2846  if ($args{childlist} && $args{childlist} !~ /^[\d,]+$/) {
     2847    $errstr = "Bad childlist argument";
     2848    return;
     2849  }
     2850
    27742851  my @filterargs;
    2775 
    27762852  $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/;
    27772853  push @filterargs, "^$args{startwith}" if $args{startwith};
    27782854  push @filterargs, $args{filter} if $args{filter};
    2779 
    27802855
    27812856  my $sql = "SELECT count(*) FROM users ".
     
    28012876  my %args = @_;
    28022877
     2878  # Fail on bad curgroup argument.  There's no sane fallback on this one.
     2879  if (!$args{curgroup} || $args{curgroup} !~ /^\d+$/) {
     2880    $errstr = "Bad or missing curgroup argument";
     2881    return;
     2882  }
     2883  # Fail on bad childlist argument.  This could be sanely ignored if bad, maybe.
     2884  if ($args{childlist} && $args{childlist} !~ /^[\d,]+$/) {
     2885    $errstr = "Bad childlist argument";
     2886    return;
     2887  }
     2888
    28032889  my @filterargs;
    2804 
    28052890  $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/;
    28062891  push @filterargs, "^$args{startwith}" if $args{startwith};
     
    28132898
    28142899  # protection against bad or missing arguments
    2815   $args{sortorder} = 'ASC' if !$args{sortorder};
    2816   $args{sortby} = 'u.username' if !$args{sortby};
     2900  $args{sortorder} = 'ASC' if !$args{sortorder} || !grep /^$args{sortorder}$/, ('ASC','DESC');
     2901  $args{sortby} = 'u.username' if !$args{sortby} || $args{sortby} !~ /^[\w_.]+$/;
    28172902  $args{offset} = 0 if !$args{offset} || $args{offset} !~ /^(?:all|\d+)$/;
    28182903
     
    28252910        " AND NOT u.type = 'R' ".
    28262911        " ORDER BY $args{sortby} $args{sortorder} ".
    2827         ($args{offset} eq 'all' ? '' : " LIMIT $config{perpage} OFFSET ".$args{offset}*$config{perpage});
     2912        ($args{offset} eq 'all' ? '' : " LIMIT $self->{perpage} OFFSET ".$args{offset}*$self->{perpage});
    28282913  my $ulist = $dbh->selectall_arrayref($sql, { Slice => {} }, (@filterargs) );
    28292914  $errstr = $dbh->errstr if !$ulist;
     
    29082993    my $msg = $@;
    29092994    eval { $dbh->rollback; };
    2910     if ($config{log_failures}) {
     2995    if ($self->{log_failures}) {
    29112996      $self->_log(group_id => $group, entry => "Error updating user $username: $msg");
    29122997      $dbh->commit;     # since we enabled transactions earlier
     
    29473032    my $msg = $@;
    29483033    eval { $dbh->rollback; };
    2949     if ($config{log_failures}) {
     3034    if ($self->{log_failures}) {
    29503035      $self->_log(group_id => $userdata->{group_id}, entry => "Error deleting user ID ".
    29513036        "$userid/".$userdata->{username}.": $msg");
     
    31203205    my $msg = $@;
    31213206    eval { $dbh->rollback; };
    3122     if ($config{log_failures}) {
     3207    if ($self->{log_failures}) {
    31233208      $shdesc = $loc if !$shdesc;
    31243209      $self->_log(entry => "Failed adding location ($shdesc, '$iplist'): $msg");
     
    31673252    my $msg = $@;
    31683253    eval { $dbh->rollback; };
    3169     if ($config{log_failures}) {
     3254    if ($self->{log_failures}) {
    31703255      $shdesc = $loc if !$shdesc;
    31713256      $self->_log(entry => "Failed updating location ($shdesc, '$iplist'): $msg");
     
    32063291    my $msg = $@;
    32073292    eval { $dbh->rollback; };
    3208     if ($config{log_failures}) {
     3293    if ($self->{log_failures}) {
    32093294      $self->_log(entry => "Failed to delete location ($olddesc, '$oldloc->{iplist}'): $msg");
    32103295      $dbh->commit;
     
    32443329  my %args = @_;
    32453330
     3331  # Fail on bad curgroup argument.  There's no sane fallback on this one.
     3332  if (!$args{curgroup} || $args{curgroup} !~ /^\d+$/) {
     3333    $errstr = "Bad or missing curgroup argument";
     3334    return;
     3335  }
     3336  # Fail on bad childlist argument.  This could be sanely ignored if bad, maybe.
     3337  if ($args{childlist} && $args{childlist} !~ /^[\d,]+$/) {
     3338    $errstr = "Bad childlist argument";
     3339    return;
     3340  }
     3341
    32463342  my @filterargs;
    3247 
    32483343  $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/;
    32493344  push @filterargs, "^$args{startwith}" if $args{startwith};
    32503345  push @filterargs, $args{filter} if $args{filter};
    3251 
    32523346
    32533347  my $sql = "SELECT count(*) FROM locations ".
     
    32683362  my %args = @_;
    32693363
     3364  # Fail on bad curgroup argument.  There's no sane fallback on this one.
     3365  if (!$args{curgroup} || $args{curgroup} !~ /^\d+$/) {
     3366    $errstr = "Bad or missing curgroup argument";
     3367    return;
     3368  }
     3369  # Fail on bad childlist argument.  This could be sanely ignored if bad, maybe.
     3370  if ($args{childlist} && $args{childlist} !~ /^[\d,]+$/) {
     3371    $errstr = "Bad childlist argument";
     3372    return;
     3373  }
     3374
    32703375  my @filterargs;
    3271 
    32723376  $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/;
    32733377  push @filterargs, "^$args{startwith}" if $args{startwith};
     
    32803384
    32813385  # protection against bad or missing arguments
    3282   $args{sortorder} = 'ASC' if !$args{sortorder};
    3283   $args{sortby} = 'l.description' if !$args{sortby};
     3386  $args{sortorder} = 'ASC' if !$args{sortorder} || !grep /^$args{sortorder}$/, ('ASC','DESC');
     3387  $args{sortby} = 'l.description' if !$args{sortby} || $args{sortby} !~ /^[\w_.]+$/;
    32843388  $args{offset} = 0 if !$args{offset} || $args{offset} !~ /^(?:all|\d+)$/;
    32853389
     
    32913395        ($args{filter} ? " AND l.description ~* ?" : '').
    32923396        " ORDER BY $args{sortby} $args{sortorder} ".
    3293         ($args{offset} eq 'all' ? '' : " LIMIT $config{perpage} OFFSET ".$args{offset}*$config{perpage});
     3397        ($args{offset} eq 'all' ? '' : " LIMIT $self->{perpage} OFFSET ".$args{offset}*$self->{perpage});
    32943398  my $ulist = $dbh->selectall_arrayref($sql, { Slice => {} }, (@filterargs) );
    32953399  $errstr = $dbh->errstr if !$ulist;
     
    34113515    $logdata{entry} = "Error updating ".($defrec eq 'y' ? ($revrec eq 'y' ? 'default reverse zone ' : 'default ') : '').
    34123516        "SOA record for $parname: $msg";
    3413     if ($config{log_failures}) {
     3517    if ($self->{log_failures}) {
    34143518      $self->_log(%logdata);
    34153519      $dbh->commit;
     
    34333537  my $id = shift;
    34343538
     3539##fixme: do we need a knob to twist to switch between unix epoch and postgres time string?
    34353540  my $sql = "SELECT record_id,host,type,val,ttl".
    34363541        ($defrec eq 'n' ? ',location' : '').
    34373542        ($revrec eq 'n' ? ',distance,weight,port' : '').
    3438         (($defrec eq 'y') ? ',group_id FROM ' : ',domain_id,rdns_id FROM ').
     3543        (($defrec eq 'y') ? ',group_id FROM ' : ',domain_id,rdns_id,stamp,stamp < now() AS ispast,expires,stampactive FROM ').
    34393544        _rectable($defrec,$revrec)." WHERE record_id=?";
    34403545  my $ret = $dbh->selectrow_hashref($sql, undef, ($id) );
     
    34653570
    34663571##fixme: should use above (getRecLine()) to get lines for below?
    3467 ## DNSDB::getDomRecs()
    3468 # Return records for a domain
    3469 # Takes a database handle, default/live flag, group/domain ID, start,
     3572## DNSDB::getRecList()
     3573# Return records for a group or zone
     3574# Takes a default/live flag, group or zone ID, start,
    34703575# number of records, sort field, and sort order
    34713576# Returns a reference to an array of hashes
    3472 sub getDomRecs {
     3577sub getRecList {
    34733578  $errstr = '';
    34743579  my $self = shift;
     
    34823587
    34833588  # protection against bad or missing arguments
    3484   $args{sortorder} = 'ASC' if !$args{sortorder};
    3485   $args{sortby} = 'host' if !$args{sortby} && $args{revrec} eq 'n';     # default sort by host on domain record list
    3486   $args{sortby} = 'val' if !$args{sortby} && $args{revrec} eq 'y';      # default sort by IP on revzone record list
     3589  $args{sortorder} = 'ASC' if !$args{sortorder} || !grep /^$args{sortorder}$/, ('ASC','DESC');
     3590  my $defsort;
     3591  $defsort = 'host' if $args{revrec} eq 'n';     # default sort by host on domain record list
     3592  $defsort = 'val' if $args{revrec} eq 'y';      # default sort by IP on revzone record list
     3593  $args{sortby} = '' if !$args{sortby};
     3594  $args{sortby} = $defsort if !$args{revrec};
     3595  $args{sortby} = $defsort if $args{sortby} !~ /^[\w_,.]+$/;
    34873596  $args{offset} = 0 if !$args{offset} || $args{offset} !~ /^(?:all|\d+)$/; 
     3597  my $perpage = ($args{nrecs} ? $args{nrecs} : $self->{perpage});
    34883598
    34893599  # sort reverse zones on IP, correctly
     
    35083618  $newsort =~ s/^,//;
    35093619
     3620##fixme:  do we need a knob to twist to switch from unix epoch to postgres time string?
     3621  my $sql = "SELECT r.record_id,r.host,r.type,r.val,r.ttl";
     3622  $sql .= ",l.description AS locname,stamp,r.stamp < now() AS ispast,r.expires,r.stampactive"
     3623        if $args{defrec} eq 'n';
     3624  $sql .= ",r.distance,r.weight,r.port" if $args{revrec} eq 'n';
     3625  $sql .= " FROM "._rectable($args{defrec},$args{revrec})." r ";
    35103626  $sql .= "INNER JOIN rectypes t ON r.type=t.val ";     # for sorting by type alphabetically
    35113627  $sql .= "LEFT JOIN locations l ON r.location=l.location " if $args{defrec} eq 'n';
     
    35163632  # ensure consistent ordering by sorting on record_id too
    35173633  $sql .= ", record_id $args{sortorder}";
    3518   $sql .= ($args{offset} eq 'all' ? '' : " LIMIT $config{perpage} OFFSET ".$args{offset}*$config{perpage});
     3634  $sql .= ($args{offset} eq 'all' ? '' : " LIMIT $perpage OFFSET ".$args{offset}*$perpage);
    35193635
    35203636  my @bindvars = ($args{id});
     
    35223638
    35233639  my $ret = $dbh->selectall_arrayref($sql, { Slice => {} }, (@bindvars) );
     3640  $errstr = "Error retrieving records: ".$dbh->errstr if !$ret;
     3641
    35243642  return $ret;
    3525 } # end getDomRecs()
     3643} # end getRecList()
    35263644
    35273645
     
    35813699  $location  = '' if !$location;
    35823700
     3701  my $expires = shift;
     3702  $expires = 1 if $expires eq 'until';  # Turn some special values into the appropriate booleans.
     3703  $expires = 0 if $expires eq 'after';
     3704  my $stamp = shift;
     3705  $stamp = '' if !$stamp;        # Timestamp should be a string at this point.
     3706
    35833707  # Spaces are evil.
    35843708  $host =~ s/^\s+//;
     
    35903714  }
    35913715
    3592   # Validation
    3593   my $addr = NetAddr::IP->new($val);
    3594   if ($rectype == $reverse_typemap{A}) {
    3595     return ('FAIL',$typemap{$rectype}." record must be a valid IPv4 address")
    3596         unless $addr && !$addr->{isv6};
    3597   }
    3598   if ($rectype == $reverse_typemap{AAAA}) {
    3599     return ('FAIL',$typemap{$rectype}." record must be a valid IPv6 address")
    3600         unless $addr && $addr->{isv6};
    3601   }
     3716  if ($self->{lowercase}) {
     3717    if ($typemap{$$rectype} ne 'TXT') {
     3718      $$host = lc($$host);
     3719      $$val = lc($$val);
     3720    } else {
     3721      # TXT records should preserve user entry in the string.
     3722      if ($revrec eq 'n') {
     3723        $$host = lc($$host);
     3724      } else {
     3725        $$val = lc($$val);
     3726      }
     3727    }
     3728  }
     3729
     3730  # prep for validation
     3731  my $addr = NetAddr::IP->new($$val);
     3732  $$host =~ s/\.+$//;   # FQDNs ending in . are an internal detail, and really shouldn't be exposed in the UI.
    36023733
    36033734  my $domid = 0;
     
    36103741  return ('FAIL', "TTL must be numeric") unless $ttl =~ /^\d+$/;
    36113742
    3612   # Quick check on hostname parts.  Note the regex is more forgiving than the error message;
    3613   # domain names technically are case-insensitive, and we use printf-like % codes for a couple
    3614   # of types.  Other things may also be added to validate default records of several flavours.
    3615   return ('FAIL', "Hostnames may not contain anything other than (0-9 a-z . _)")
    3616         if $defrec eq 'n' && ($revrec eq 'y' ? $$rectype != $reverse_typemap{TXT} : 1) &&
    3617                 $$host !~ /^[0-9a-z_%.-]+$/i;
     3743  # Quick check on hostname parts.  There are enough variations to justify a sub now.
     3744  return ('FAIL', $errstr) if ! _check_hostname_form($$host, $$rectype, $defrec, $revrec);
    36183745
    36193746  # Collect these even if we're only doing a simple A record so we can call *any* validation sub
     
    36373764  push @vallist, ($$host,$$rectype,$$val,$ttl,$id);
    36383765
    3639   # locations are not for default records, silly coder!
    36403766  if ($defrec eq 'n') {
     3767    # locations are not for default records, silly coder!
    36413768    $fields .= ",location";
    36423769    push @vallist, $location;
    3643   }
     3770    # timestamps are rare.
     3771    if ($stamp) {
     3772      $fields .= ",stamp,expires,stampactive";
     3773      push @vallist, $stamp, $expires, 'y';
     3774    } else {
     3775      $fields .= ",stampactive";
     3776      push @vallist, 'n';
     3777    }
     3778  }
     3779
     3780  # a little magic to get the right number of ? placeholders based on how many values we're providing
    36443781  my $vallen = '?'.(',?'x$#vallist);
    36453782
     
    36693806  $logdata{entry} .= "', TTL $ttl";
    36703807  $logdata{entry} .= ", location ".$self->getLoc($location)->{description} if $location;
     3808  $logdata{entry} .= ($expires eq 'after' ? ', valid after ' : ', expires at ').$stamp if $stamp;
    36713809
    36723810  # Allow transactions, and raise an exception on errors so we can catch it later.
     
    36843822    my $msg = $@;
    36853823    eval { $dbh->rollback; };
    3686     if ($config{log_failures}) {
     3824    if ($self->{log_failures}) {
    36873825      $logdata{entry} = "Failed adding ".($defrec eq 'y' ? 'default ' : '').
    36883826        "record '$$host $typemap{$$rectype} $$val', TTL $ttl ($msg)";
     
    37223860  $location  = '' if !$location;
    37233861
    3724   # prep for validation
    3725   my $addr = NetAddr::IP->new($$val);
    3726   $$host =~ s/\.+$//;   # FQDNs ending in . are an internal detail, and really shouldn't be exposed in the UI.
     3862  my $expires = shift;
     3863  $expires = 1 if $expires eq 'until';  # Turn some special values into the appropriate booleans.
     3864  $expires = 0 if $expires eq 'after';
     3865  my $stamp = shift;
     3866  $stamp = '' if !$stamp;        # Timestamp should be a string at this point.
     3867
     3868  # just set it to an empty string;  failures will be caught later.
     3869  $$host = '' if !$$host;
    37273870
    37283871  # Spaces are evil.
     
    37353878  }
    37363879
     3880  if ($self->{lowercase}) {
     3881    if ($typemap{$$rectype} ne 'TXT') {
     3882      $$host = lc($$host);
     3883      $$val = lc($$val);
     3884    } else {
     3885      # TXT records should preserve user entry in the string.
     3886      if ($revrec eq 'n') {
     3887        $$host = lc($$host);
     3888      } else {
     3889        $$val = lc($$val);
     3890      }
     3891    }
     3892  }
     3893
     3894  # prep for validation
     3895  my $addr = NetAddr::IP->new($$val);
     3896  $$host =~ s/\.+$//;   # FQDNs ending in . are an internal detail, and really shouldn't be exposed in the UI.
     3897
    37373898  my $domid = 0;
    37383899  my $revid = 0;
     
    37443905  return ('FAIL', "TTL must be numeric") unless $ttl =~ /^\d+$/;
    37453906
    3746   # Quick check on hostname parts.  Note the regex is more forgiving than the error message;
    3747   # domain names technically are case-insensitive, and we use printf-like % codes for a couple
    3748   # of types.  Other things may also be added to validate default records of several flavours.
    3749   return ('FAIL', "Hostnames may not contain anything other than (0-9 a-z - . _)")
    3750         if $defrec eq 'n' && ($revrec eq 'y' ? $$rectype != $reverse_typemap{TXT} : 1) &&
    3751                 $$host !~ /^[0-9a-z_%.-]+$/i;
     3907  # Quick check on hostname parts.  There are enough variations to justify a sub now.
     3908  return ('FAIL', $errstr) if ! _check_hostname_form($$host, $$rectype, $defrec, $revrec);
    37523909
    37533910  # only MX and SRV will use these
     
    37803937        ($defrec eq 'y' ? $oldrec->{group_id} : ($revrec eq 'n' ? $oldrec->{domain_id} : $oldrec->{rdns_id})) );
    37813938
    3782   # locations are not for default records, silly coder!
    37833939  if ($defrec eq 'n') {
     3940    # locations are not for default records, silly coder!
    37843941    $fields .= ",location";
    37853942    push @vallist, $location;
     3943    # timestamps are rare.
     3944    if ($stamp) {
     3945      $fields .= ",stamp,expires,stampactive";
     3946      push @vallist, $stamp, $expires, 'y';
     3947    } else {
     3948      $fields .= ",stampactive";
     3949      push @vallist, 'n';
     3950    }
    37863951  }
    37873952
     
    38374002  $logdata{entry} .= "', TTL $oldrec->{ttl}";
    38384003  $logdata{entry} .= ", location ".$self->getLoc($oldrec->{location})->{description} if $oldrec->{location};
     4004  $logdata{entry} .= ($oldrec->{expires} ? ', expires at ' : ', valid after ').$oldrec->{stamp}
     4005        if $oldrec->{stampactive};
    38394006  $logdata{entry} .= "\nto\n";
    38404007  # More NS special
     
    38484015  $logdata{entry} .= "', TTL $ttl";
    38494016  $logdata{entry} .= ", location ".$self->getLoc($location)->{description} if $location;
     4017  $logdata{entry} .= ($expires eq 'after' ? ', valid after ' : ', expires at ').$stamp if $stamp;
    38504018
    38514019  local $dbh->{AutoCommit} = 0;
     
    38644032    my $msg = $@;
    38654033    eval { $dbh->rollback; };
    3866     if ($config{log_failures}) {
     4034    if ($self->{log_failures}) {
    38674035      $logdata{entry} = "Failed updating ".($defrec eq 'y' ? 'default ' : '').
    38684036        "record '$oldrec->{host} $typemap{$oldrec->{type}} $oldrec->{val}', TTL $oldrec->{ttl} ($msg)";
     
    39754143    my $msg = $@;
    39764144    eval { $dbh->rollback; };
    3977     if ($config{log_failures}) {
     4145    if ($self->{log_failures}) {
    39784146      $logdata{entry} = "Error deleting ".($defrec eq 'y' ? 'default record' : 'record').
    39794147        " '$oldrec->{host} $typemap{$oldrec->{type}} $oldrec->{val}', TTL $oldrec->{ttl} ($msg)";
     
    40114179  $args{logtype} = 'domain' if $args{logtype} eq 'dom';         # hack pthui
    40124180  return if !grep /^$args{logtype}$/, ('group', 'domain', 'revzone', 'user');
    4013 
    4014   $args{logtype} = 'revzone' if $args{logtype} eq 'rdns';       # hack pthui
    40154181
    40164182  my $sql = "SELECT count(*) FROM log ".
     
    40474213
    40484214  # Sorting defaults
    4049   $args{sortby} = 'stamp' if !$args{sortby};
    4050   $args{sortorder} = 'DESC' if !$args{sortorder};
     4215  $args{sortorder} = 'DESC' if !$args{sortorder} || !grep /^$args{sortorder}$/, ('ASC','DESC');
     4216  $args{sortby} = 'stamp' if !$args{sortby} || $args{sortby} !~ /^[\w_.]+$/;
    40514217  $args{offset} = 0 if !$args{offset} || $args{offset} !~ /^(?:all|\d+)$/;
    40524218
     
    40604226        ($args{filter} ? " AND entry ~* ?" : '').
    40614227        " ORDER BY $args{sortby} $args{sortorder}, log_id $args{sortorder}".
    4062         ($args{offset} eq 'all' ? '' : " LIMIT $config{perpage} OFFSET ".$args{offset}*$config{perpage});
     4228        ($args{offset} eq 'all' ? '' : " LIMIT $self->{perpage} OFFSET ".$args{offset}*$self->{perpage});
    40634229  my $loglist = $dbh->selectall_arrayref($sql, { Slice => {} }, ($args{id}, @filterargs) );
    40644230  $errstr = $dbh->errstr if !$loglist;
     
    41114277    # default;  forward zone types.  technically $type eq 'f' but not worth the error message.
    41124278    $sql .= "stdflag=1 OR stdflag=2";
     4279    $sql .= " AND val < 65280" if $recgroup eq 'fo';  # An extra flag to trim off the pseudotypes as well.
    41134280  }
    41144281  $sql .= " ORDER BY listorder";
     
    42434410      $limiter++;
    42444411##fixme:  how often will this happen on a live site?  fail at max limiter <n>?
     4412# 2013/10/22 only seems to happen when you request an entity that doesn't exist.
    42454413      warn "no results looking for $sql with id $id (depth $limiter)\n";
    42464414      last;
     
    48415009  my %recflags;
    48425010
    4843   my $domsth = $dbh->prepare("SELECT domain_id,domain,status,changed FROM domains WHERE status=1");
    4844   my $recsth = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl,record_id,location ".
    4845         "FROM records WHERE domain_id=? AND type < 65280");     # Just exclude all types relating to rDNS
    4846   my $zonesth = $dbh->prepare("UPDATE domains SET changed='n' WHERE domain_id=?");
    4847   $domsth->execute();
    4848   while (my ($domid,$dom,$domstat,$changed) = $domsth->fetchrow_array) {
    4849 ##fixme: need to find a way to block opening symlinked files without introducing a race.
    4850 #       O_NOFOLLOW
    4851 #              If  pathname  is a symbolic link, then the open fails.  This is a FreeBSD extension, which was
    4852 #              added to Linux in version 2.1.126.  Symbolic links in earlier components of the pathname  will
    4853 #              still be followed.
    4854 # but that doesn't help other platforms.  :/
    4855     sysopen(ZONECACHE, "$config{exportcache}/$dom", O_RDWR|O_CREAT);
    4856     flock(ZONECACHE, LOCK_EX);
    4857     if ($changed || -s "$config{exportcache}/$dom" == 0) {
    4858       $recsth->execute($domid);
    4859       while (my ($host,$type,$val,$dist,$weight,$port,$ttl,$recid,$loc) = $recsth->fetchrow_array) {
    4860         next if $recflags{$recid};
    4861 
    4862         $loc = '' if !$loc;     # de-nullify - just in case
    4863 ##fixme:  handle case of record-with-location-that-doesn't-exist better.
    4864 # note this currently fails safe (tested) - records with a location that
    4865 # doesn't exist will not be sent to any client
    4866 #       $loc = '' if !$lochash->{$loc};
    4867 
    4868 ##fixme:  record validity timestamp. tinydns supports fiddling with timestamps.
    4869 # note $ttl must be set to 0 if we want to use tinydns's auto-expiring timestamps.
    4870 # timestamps are TAI64
    4871 # ~~ 2^62 + time()
    4872         my $stamp = '';
    4873 
    4874         # support tinydns' auto-TTL
    4875         $ttl = '' if $ttl == '0';
    4876 
    4877         # Spaces are evil.
    4878         $host =~ s/^\s+//;
    4879         $host =~ s/\s+$//;
    4880         if ($typemap{$type} ne 'TXT') {
    4881           # Leading or trailng spaces could be legit in TXT records.
    4882           $val =~ s/^\s+//;
    4883           $val =~ s/\s+$//;
    4884         }
    4885 
    4886         _printrec_tiny(*ZONECACHE, 'n', \%recflags,
    4887                 $dom, $host, $type, $val, $dist, $weight, $port, $ttl, $loc, $stamp)
    4888                 if *ZONECACHE;
    4889 
    4890         # in case the zone shrunk, get rid of garbage at the end of the file.
    4891         truncate(ZONECACHE, tell(ZONECACHE));
    4892 
    4893         $recflags{$recid} = 1;
    4894       } # while ($recsth)
    4895     }
    4896     # stream from cache, whether freshly created or existing
    4897     print $datafile $_ while <ZONECACHE>;
    4898     close ZONECACHE;
    4899     # mark domain as unmodified
    4900     $zonesth->execute($domid);
    4901   } # while ($domsth)
    4902 
    4903   my $revsth = $dbh->prepare("SELECT rdns_id,revnet,status,changed FROM revzones WHERE status=1 ".
    4904         "ORDER BY masklen(revnet) DESC");
    4905 
    49065011# For reasons unknown, we can't sanely UNION these statements.  Feh.
    49075012# Supposedly it should work though (note last 3 lines):
     
    49185023  my $soasth = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl,record_id,location ".
    49195024        "FROM records WHERE rdns_id=? AND type=6");
    4920   $recsth = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl,record_id,location ".
     5025  my $recsth = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl,record_id,location,extract(epoch from stamp),expires,stampactive ".
    49215026        "FROM records WHERE rdns_id=? AND not type=6 ".
    49225027        "ORDER BY masklen(CAST(val AS inet)) DESC, CAST(val AS inet)");
    4923   $zonesth = $dbh->prepare("UPDATE revzones SET changed='n' WHERE rdns_id=?");
     5028  my $revsth = $dbh->prepare("SELECT rdns_id,revnet,status,changed FROM revzones WHERE status=1 ".
     5029        "ORDER BY masklen(revnet) DESC");
     5030  my $zonesth = $dbh->prepare("UPDATE revzones SET changed='n' WHERE rdns_id=?");
    49245031  $revsth->execute();
    49255032  while (my ($revid,$revzone,$revstat,$changed) = $revsth->fetchrow_array) {
     
    49315038# but that doesn't help other platforms.  :/
    49325039    my $tmpzone = NetAddr::IP->new($revzone);
    4933     sysopen(ZONECACHE, "$config{exportcache}/".$tmpzone->network->addr, O_RDWR|O_CREAT);
    4934     flock(ZONECACHE, LOCK_EX);
    4935     if ($changed || -s "$config{exportcache}/".$tmpzone->network->addr == 0) {
    4936       # need to fetch this separately since the rest of the records all (should) have real IPs in val
    4937       $soasth->execute($revid);
    4938       my (@zsoa) = $soasth->fetchrow_array();
    4939       _printrec_tiny(*ZONECACHE,'y',\%recflags,$revzone,
    4940         $zsoa[0],$zsoa[1],$zsoa[2],$zsoa[3],$zsoa[4],$zsoa[5],$zsoa[6],$zsoa[8],'');
    4941 
    4942       $recsth->execute($revid);
    4943       while (my ($host,$type,$val,$dist,$weight,$port,$ttl,$recid,$loc) = $recsth->fetchrow_array) {
    4944         next if $recflags{$recid};
    4945 
    4946         $loc = '' if !$loc;     # de-nullify - just in case
     5040##fixme:  locations/views?  subnet mask?  need to avoid possible collisions with zone/superzone
     5041##        (eg /20 vs /24, starting on .0.0)
     5042    my $cz = $tmpzone->network->addr."-".$tmpzone->masklen;
     5043    my $cachefile = "$self->{exportcache}/$cz";
     5044    my $tmpcache = "$self->{exportcache}/tmp.$cz.$$";
     5045    eval {
     5046
     5047      # only update the cache file if the zone has changed, or if the cache file has nothing in it.
     5048      if ($self->{force_refresh} || $changed || !-e $cachefile || -z $cachefile) {
     5049        open ZONECACHE, ">$tmpcache" or die "Error creating temporary file $tmpcache: $!\n";
     5050
     5051        # need to fetch this separately since the rest of the records all (should) have real IPs in val
     5052        $soasth->execute($revid);
     5053        my (@zsoa) = $soasth->fetchrow_array();
     5054        _printrec_tiny(*ZONECACHE,'y',\%recflags,$revzone,
     5055          $zsoa[0],$zsoa[1],$zsoa[2],$zsoa[3],$zsoa[4],$zsoa[5],$zsoa[6],$zsoa[8],'');
     5056
     5057        $recsth->execute($revid);
     5058        while (my ($host,$type,$val,$dist,$weight,$port,$ttl,$recid,$loc,$stamp,$expires,$stampactive) = $recsth->fetchrow_array) {
     5059          next if $recflags{$recid};
     5060
     5061# not sure this is necessary for revzones.
     5062#         # Spaces are evil.
     5063#         $val =~ s/^\s+//;
     5064#         $val =~ s/\s+$//;
     5065#         if ($typemap{$type} ne 'TXT') {
     5066#           # Leading or trailng spaces could be legit in TXT records.
     5067#           $host =~ s/^\s+//;
     5068#           $host =~ s/\s+$//;
     5069#         }
     5070
     5071          _printrec_tiny(*ZONECACHE, 'y', \%recflags, $revzone,
     5072            $host, $type, $val, $dist, $weight, $port, $ttl, $loc, $stamp, $expires, $stampactive)
     5073                if *ZONECACHE;
     5074
     5075          $recflags{$recid} = 1;
     5076
     5077        } # while ($recsth)
     5078
     5079        close ZONECACHE; # force the file to be written
     5080
     5081        # catch obvious write errors that leave an empty temp file
     5082        if (-s $tmpcache) {
     5083          rename $tmpcache, $cachefile
     5084            or die "Error overwriting cache file $cachefile with temporary file: $!\n";
     5085        }
     5086
     5087      } # if $changed or cache filesize is 0
     5088
     5089    };
     5090    if ($@) {
     5091      print "error writing new data for $revzone: $@\n";
     5092      # error!  something borked, and we should be able to fall back on the old cache file
     5093      # report the error, somehow.
     5094    } else {
     5095      # mark zone as unmodified.  Only do this if no errors, that way
     5096      # export failures should recover a little more automatically.
     5097      $zonesth->execute($revid);
     5098    }
     5099    # Always stream the cache (even if stale or obsolete due to errors creating the new cache)
     5100    open CACHE, "<$cachefile";
     5101    print $datafile $_ while <CACHE>;
     5102    close CACHE;
     5103
     5104  } # while ($revsth)
     5105
     5106  my $domsth = $dbh->prepare("SELECT domain_id,domain,status,changed FROM domains WHERE status=1");
     5107  $recsth = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl,record_id,location,extract(epoch from stamp),expires,stampactive ".
     5108        "FROM records WHERE domain_id=?");      # Just exclude all types relating to rDNS
     5109#       "FROM records WHERE domain_id=? AND type < 65280");     # Just exclude all types relating to rDNS
     5110  $zonesth = $dbh->prepare("UPDATE domains SET changed='n' WHERE domain_id=?");
     5111  $domsth->execute();
     5112  while (my ($domid,$dom,$domstat,$changed) = $domsth->fetchrow_array) {
     5113##fixme: need to find a way to block opening symlinked files without introducing a race.
     5114#       O_NOFOLLOW
     5115#              If  pathname  is a symbolic link, then the open fails.  This is a FreeBSD extension, which was
     5116#              added to Linux in version 2.1.126.  Symbolic links in earlier components of the pathname  will
     5117#              still be followed.
     5118# but that doesn't help other platforms.  :/
     5119    my $cachefile = "$self->{exportcache}/$dom";
     5120    my $tmpcache = "$self->{exportcache}/tmp.$dom.$$";
     5121    eval {
     5122
     5123      # only update the cache file if the zone has changed, or if the cache file has nothing in it.
     5124      if ($self->{force_refresh} || $changed || !-e $cachefile || -z $cachefile) {
     5125        open ZONECACHE, ">$tmpcache" or die "Error creating temporary file $tmpcache: $!\n";
     5126
     5127        $recsth->execute($domid);
     5128        while (my ($host,$type,$val,$dist,$weight,$port,$ttl,$recid,$loc,$stamp,$expires,$stampactive) = $recsth->fetchrow_array) {
     5129          next if $recflags{$recid};
     5130
     5131          # Spaces are evil.
     5132          $host =~ s/^\s+//;
     5133          $host =~ s/\s+$//;
     5134          if ($typemap{$type} ne 'TXT') {
     5135            # Leading or trailng spaces could be legit in TXT records.
     5136            $val =~ s/^\s+//;
     5137            $val =~ s/\s+$//;
     5138          }
     5139
     5140          _printrec_tiny(*ZONECACHE, 'n', \%recflags,
     5141                $dom, $host, $type, $val, $dist, $weight, $port, $ttl, $loc, $stamp, $expires, $stampactive)
     5142                if *ZONECACHE;
     5143
     5144          $recflags{$recid} = 1;
     5145
     5146        } # while ($recsth)
     5147
     5148        close ZONECACHE; # force the file to be written
     5149
     5150        # catch obvious write errors that leave an empty temp file
     5151        if (-s $tmpcache) {
     5152          rename $tmpcache, $cachefile
     5153            or die "Error overwriting cache file $cachefile with temporary file: $!\n";
     5154        }
     5155
     5156      } # if $changed or cache filesize is 0
     5157
     5158    };
     5159    if ($@) {
     5160      print "error writing new data for $dom: $@\n";
     5161      # error!  something borked, and we should be able to fall back on the old cache file
     5162      # report the error, somehow.
     5163    } else {
     5164      # mark domain as unmodified.  Only do this if no errors, that way
     5165      # export failures should recover a little more automatically.
     5166      $zonesth->execute($domid);
     5167    }
     5168    # Always stream the cache (even if stale or obsolete due to errors creating the new cache)
     5169    open CACHE, "<$cachefile";
     5170    print $datafile $_ while <CACHE>;
     5171    close CACHE;
     5172
     5173  } # while ($domsth)
     5174
     5175} # end __export_tiny()
     5176
     5177
     5178# Utility sub for __export_tiny above
     5179sub _printrec_tiny {
     5180  my ($datafile,$revrec,$recflags,$zone,$host,$type,$val,$dist,$weight,$port,$ttl,$loc,$stamp,$expires,$stampactive) = @_;
     5181
     5182  $loc = '' if !$loc;   # de-nullify - just in case
    49475183##fixme:  handle case of record-with-location-that-doesn't-exist better.
    49485184# note this currently fails safe (tested) - records with a location that
     
    49505186#       $loc = '' if !$lochash->{$loc};
    49515187
    4952 ##fixme:  record validity timestamp. tinydns supports fiddling with timestamps.
    4953 # note $ttl must be set to 0 if we want to use tinydns's auto-expiring timestamps.
    4954 # timestamps are TAI64
    4955 # ~~ 2^62 + time()
    4956         my $stamp = '';
    4957 
    4958         # support tinydns' auto-TTL
    4959         $ttl = '' if $ttl == '0';
    4960 
    4961         _printrec_tiny(*ZONECACHE, 'y', \%recflags, $revzone,
    4962                 $host, $type, $val, $dist, $weight, $port, $ttl, $loc, $stamp)
    4963                 if *ZONECACHE;
    4964 
    4965         # in case the zone shrunk, get rid of garbage at the end of the file.
    4966         truncate(ZONECACHE, tell(ZONECACHE));
    4967 
    4968         $recflags{$recid} = 1;
    4969       } # while ($recsth)
    4970     }
    4971     # stream from cache, whether freshly created or existing
    4972     print $datafile $_ while <ZONECACHE>;
    4973     close ZONECACHE;
    4974     # mark zone as unmodified
    4975     $zonesth->execute($revid);
    4976   } # while ($domsth)
    4977 
    4978 } # end __export_tiny()
    4979 
    4980 
    4981 # Utility sub for __export_tiny above
    4982 sub _printrec_tiny {
    4983   my ($datafile,$revrec,$recflags,$zone,$host,$type,$val,$dist,$weight,$port,$ttl,$loc,$stamp) = @_;
     5188
     5189## Records that are valid only before or after a set time
     5190
     5191# record due to expire sometime is the complex case.  we don't want to just
     5192# rely on tinydns' auto-adjusting TTLs, because the default TTL in that case
     5193# is one day instead of the SOA minttl as BIND might do.
     5194
     5195# consider the case where a record is set to expire a week ahead, but the next
     5196# day later you want to change it NOW (or as NOWish as you get with your DNS
     5197# management practice).  but now you're stuck, because someone, somewhere,
     5198# has just done a lookup before your latest change was published, and they'll
     5199# be caching that old, broken record for 1 day instead of your zone default
     5200# TTL.
     5201
     5202# $stamp-$ttl is the *latest* we can publish the record with the defined TTL
     5203# to still have the expiry happen as scheduled, but we need to find some
     5204# *earlier* point.  We can maybe guess, and 2x TTL is probably reasonable,
     5205# but we need info on the export frequency.
     5206
     5207# export the normal, non-expiring record up until $stamp-<guesstimate>, then
     5208# switch to exporting a record with the TAI64 stamp and a 0 TTL so tinydns
     5209# takes over TTL management.
     5210
     5211  if ($stampactive) {
     5212    if ($expires) {
     5213      # record expires at $stamp;  decide if we need to keep the TTL and ignore
     5214      # the stamp for a time or if we need to change the TTL to 0 and convert
     5215      # $stamp to TAI64 so tinydns can use $stamp to autoadjust the TTL on the fly.
     5216# extra hack, optimally needs more knowledge of data export frequency
     5217# smack the idiot customer who insists on 0 TTLs;  they can suck up and
     5218# deal with a 10-minute TTL.  especially on scheduled changes.  note this
     5219# should be (export freq * 2), but we don't know the actual export frequency.
     5220$ttl = 300 if $ttl == 0;        #hack phtui
     5221      my $ahead = (86400 < $ttl*2 ? 86400 : $ttl*2);
     5222      if ((time() + $ahead) < $stamp) {
     5223        # more than 2x TTL OR more than one day (whichever is less) from expiry time;  publish normal record
     5224        $stamp = '';
     5225      } else {
     5226        # less than 2x TTL from expiry time, let tinydns take over TTL management and publish the TAI64 stamp.
     5227        $ttl = 0;
     5228        $stamp = unixtai64($stamp);
     5229        $stamp =~ s/\@//;
     5230      }
     5231    } else {
     5232      # record is "active after";  convert epoch from database to TAI64, publish, and collect $200.
     5233      $stamp = unixtai64($stamp);
     5234      $stamp =~ s/\@//;
     5235    }
     5236  } else {
     5237    # flag for active timestamp is false;  don't actually put a timestamp in the output
     5238    $stamp = '';
     5239  }
     5240
     5241  # support tinydns' auto-TTL
     5242  $ttl = '' if $ttl == -1;
     5243# these are WAY FREAKING HIGH - higher even than most TLD registry TTLs!
     5244# NS          259200  => 3d
     5245# all others   86400  => 1d
     5246
     5247  if ($revrec eq 'y') {
     5248    $val = $zone if $val eq '@';
     5249  } else {
     5250    $host = $zone if $host eq '@';
     5251  }
    49845252
    49855253  ## Convert a bare number into an octal-coded pair of octets.
     
    52025470      } elsif ($type == 65281) { # AAAA+PTR
    52035471
    5204 #$$recflags{$val}++;
     5472        $$recflags{$val}++;
    52055473        # treat these as two separate records.  since tinydns doesn't have
    52065474        # a native combined type, we have to create them separately anyway.
    5207         if ($revrec eq 'n') {
    5208           $type = 28;
    5209         } else {
    5210           $type = 12;
    5211         }
    5212         _printrec_tiny($datafile,$revrec,$recflags,$zone,$host,$type,$val,$dist,$weight,$port,$ttl,$loc,$stamp);
     5475        # print both;  a dangling record is harmless, and impossible via web
     5476        # UI anyway
     5477        _printrec_tiny($datafile,$revrec,$recflags,$zone,$host,28,$val,$dist,$weight,$port,$ttl,$loc,$stamp);
     5478        _printrec_tiny($datafile,$revrec,$recflags,$zone,$host,12,$val,$dist,$weight,$port,$ttl,$loc,$stamp);
    52135479##fixme: add a config flag to indicate use of the patch from http://www.fefe.de/dns/
    52145480# type 6 is for AAAA+PTR, type 3 is for AAAA
     
    53095575  my ($subj,$message) = @_;
    53105576
    5311   return if $config{mailhost} eq 'smtp.example.com';   # do nothing if still using default SMTP host.
    5312 
    5313   my $mailer = Net::SMTP->new($config{mailhost}, Hello => "dnsadmin.$config{domain}");
    5314 
    5315   my $mailsender = ($config{mailsender} ? $config{mailsender} : $config{mailnotify});
     5577  return if $self->{mailhost} eq 'smtp.example.com';   # do nothing if still using default SMTP host.
     5578
     5579  my $mailer = Net::SMTP->new($self->{mailhost}, Hello => "dnsadmin.$self->{domain}");
     5580
     5581  my $mailsender = ($self->{mailsender} ? $self->{mailsender} : $self->{mailnotify});
    53165582
    53175583  $mailer->mail($mailsender);
    5318   $mailer->to($config{mailnotify});
    5319   $mailer->data("From: \"$config{mailname}\" <$mailsender>\n",
    5320         "To: <$config{mailnotify}>\n",
     5584  $mailer->to($self->{mailnotify});
     5585  $mailer->data("From: \"$self->{mailname}\" <$mailsender>\n",
     5586        "To: <$self->{mailnotify}>\n",
    53215587        "Date: ".strftime("%a, %d %b %Y %H:%M:%S %z",localtime)."\n",
    53225588        "Subject: $subj\n",
    5323         "X-Mailer: DNSAdmin Notify v".sprintf("%.1d",$DNSDB::VERSION)."\n",
    5324         "Organization: $config{orgname}\n",
     5589        "X-Mailer: DNSAdmin v".$DNSDB::VERSION." Notify\n",
     5590        "Organization: $self->{orgname}\n",
    53255591        "\n$message\n");
    53265592  $mailer->quit;
Note: See TracChangeset for help on using the changeset viewer.