Index: branches/stable/DNSDB.pm
===================================================================
--- branches/stable/DNSDB.pm	(revision 725)
+++ branches/stable/DNSDB.pm	(revision 756)
@@ -3,5 +3,5 @@
 ##
 # $Id$
-# Copyright 2008-2013 Kris Deugau <kdeugau@deepnet.cx>
+# Copyright 2008-2016 Kris Deugau <kdeugau@deepnet.cx>
 # 
 #    This program is free software: you can redistribute it and/or modify
@@ -36,5 +36,5 @@
 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
 
-$VERSION	= "1.2.5p2";	##VERSION##
+$VERSION	= "1.4.0";	##VERSION##
 @ISA		= qw(Exporter);
 @EXPORT_OK	= qw(
@@ -146,5 +146,5 @@
 		domain	=> 'domain_id',
 		revzone	=> 'rdns_id',
-		record	=> 'record_id'
+		record	=> 'record_id',
 	);
 my %par_col = (
@@ -456,6 +456,8 @@
 #  group_id, log entry
 # and optionally one or more of:
-#  domain_id, rdns_id
+#  domain_id, rdns_id, logparent
 # The %userdata hash provides the user ID, username, and fullname
+# Returns the log entry ID, mainly for use in bulk operations to allow a "parent" log entry
+# and a set of child entries (eg, domain add and the individual default-record-copy entries)
 sub _log {
   my $self = shift;
@@ -466,11 +468,17 @@
   $args{rdns_id} = 0 if !$args{rdns_id};
   $args{domain_id} = 0 if !$args{domain_id};
+  $args{logparent} = 0 if !$args{logparent};
 
 ##fixme:  farm out the actual logging to different subs for file, syslog, internal, etc based on config
 #  if ($self->{log_channel} eq 'sql') {
-  $dbh->do("INSERT INTO log (domain_id,rdns_id,group_id,entry,user_id,email,name) VALUES (?,?,?,?,?,?,?)",
+  $dbh->do("INSERT INTO log (domain_id,rdns_id,group_id,logparent,entry,user_id,email,name) ".
+	"VALUES (?,?,?,?,?,?,?,?)",
 	undef,
-	($args{domain_id}, $args{rdns_id}, $args{group_id}, $args{entry},
+	($args{domain_id}, $args{rdns_id}, $args{group_id}, $args{logparent}, $args{entry},
 		$self->{loguserid}, $self->{logusername}, $self->{logfullname}) );
+
+  my ($log_id) = $dbh->selectrow_array("SELECT currval('log_log_id_seq')");
+  return $log_id;
+
 #  } elsif ($self->{log_channel} eq 'file') {
 #  } elsif ($self->{log_channel} eq 'syslog') {
@@ -1655,6 +1663,12 @@
     my ($c) = ($$tmpl =~ /(\%-?c)/);  my $nld = '';  my $cld = '';
     $c = '' if !$c;
+    my ($cn) = ($$tmpl =~ /(\%x)/);
     my $skipgw = ($c =~ /\%-c/ ? 0 : 1);
     my $ipkill = 0;
+
+    if ($cn) {
+      # "natural n'th IP in the block" pattern
+      $$tmpl =~ s/$cn/$ipindex+1/e;
+    }
 
 ##fixme: still have one edge case not handled well:
@@ -2368,5 +2382,5 @@
 	undef, ($domain, $defloc));
 
-    $self->_log(domain_id => $dom_id, group_id => $group,
+    my $logparent = $self->_log(domain_id => $dom_id, group_id => $group,
 	entry => "Added ".($state ? 'active' : 'inactive')." domain $domain");
 
@@ -2384,5 +2398,5 @@
 	my @tmp1 = split /:/, $host;
 	my @tmp2 = split /:/, $val;
-	$self->_log(domain_id => $dom_id, group_id => $group,
+	$self->_log(domain_id => $dom_id, group_id => $group, logparent => $logparent,
 		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");
@@ -2391,5 +2405,5 @@
 	$logentry .= " [distance $dist]" if $typemap{$type} eq 'MX';
 	$logentry .= " [priority $dist] [weight $weight] [port $port]" if $typemap{$type} eq 'SRV';
-	$self->_log(domain_id => $dom_id, group_id => $group,
+	$self->_log(domain_id => $dom_id, group_id => $group, logparent => $logparent,
 		entry => $logentry." $val', TTL $ttl");
       }
@@ -3439,5 +3453,6 @@
 	"WHERE group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")".
 	($args{startwith} ? " AND username ~* ?" : '').
-	($args{filter} ? " AND username ~* ?" : '');
+	($args{filter} ? " AND username ~* ?" : '').
+	" AND NOT type = 'R' ";
   my ($count) = $dbh->selectrow_array($sql, undef, (@filterargs) );
   $errstr = $dbh->errstr if !$count;
@@ -3645,4 +3660,5 @@
   $errstr = $DBI::errstr if !$uname;
 
+  no warnings qw (uninitialized);
   $fullformat =~ s/\%u/$uname/g;
   $fullformat =~ s/\%f/$fname/g;
@@ -4810,31 +4826,8 @@
 # Get a count of log entries
 # Takes a database handle and a hash containing at least:
-# - Entity ID and entity type as the primary log "slice"
+# - Entity identifier and entity type as the primary log "slice"
 sub getLogCount {
   my $self = shift;
-  my $dbh = $self->{dbh};
-
-  my %args = @_;
-
-  my @filterargs;
-##fixme:  which fields do we want to filter on?
-# push @filterargs, 
-
-  $errstr = 'Missing primary parent ID and/or type';
-  # fail early if we don't have a "prime" ID to look for log entries for
-  return if !$args{id};
-
-  # or if the prime id type is missing or invalid
-  return if !$args{logtype};
-  $args{logtype} = 'revzone' if $args{logtype} eq 'rdns';	# hack pthui
-  $args{logtype} = 'domain' if $args{logtype} eq 'dom';		# hack pthui
-  return if !grep /^$args{logtype}$/, ('group', 'domain', 'revzone', 'user');
-
-  my $sql = "SELECT count(*) FROM log ".
-	"WHERE $id_col{$args{logtype}}=?".
-	($args{filter} ? " AND entry ~* ?" : '');
-  my ($count) = $dbh->selectrow_array($sql, undef, ($args{id}, @filterargs) );
-  $errstr = $dbh->errstr if !$count;
-  return $count;
+  return $self->getLogEntries(@_, count => 1);
 } # end getLogCount()
 
@@ -4843,4 +4836,6 @@
 # Get a list of log entries
 # Takes arguments as with getLogCount() above, plus optional:
+# - "count" flag 
+#  OR
 # - sort field
 # - sort order
@@ -4854,12 +4849,15 @@
   my @filterargs;
 
-  # fail early if we don't have a "prime" ID to look for log entries for
-  return if !$args{id};
-
-  # or if the prime id type is missing or invalid
+  # fail if the prime id type is missing or invalid
+  $errstr = "Missing primary log slice type";
   return if !$args{logtype};
   $args{logtype} = 'revzone' if $args{logtype} eq 'rdns';	# hack pthui
   $args{logtype} = 'domain' if $args{logtype} eq 'dom';		# hack pthui
+  $errstr = "Invalid primary log slice type";
   return if !grep /^$args{logtype}$/, ('group', 'domain', 'revzone', 'user');
+
+  # fail if we don't have a prime ID to look for log entries for
+  $errstr = "Missing ID for primary log slice";
+  return if !($args{id} || $args{fname});
 
   # Sorting defaults
@@ -4868,18 +4866,172 @@
   $args{offset} = 0 if !$args{offset} || $args{offset} !~ /^(?:all|\d+)$/;
 
-  my %sortmap = (fname => 'name', username => 'email', entry => 'entry', stamp => 'stamp');
+  my %sortmap = (fname => 'name', username => 'email', entry => 'entry', stamp => 'stamp',
+      revzone => 'revnet', domain => 'domain');
   $args{sortby} = $sortmap{$args{sortby}};
 
-  my $sql = "SELECT user_id AS userid, email AS useremail, name AS userfname, entry AS logentry, ".
-	"date_trunc('second',stamp) AS logtime ".
-	"FROM log ".
-	"WHERE $id_col{$args{logtype}}=?".
-	($args{filter} ? " AND entry ~* ?" : '').
-	" ORDER BY $args{sortby} $args{sortorder}, log_id $args{sortorder}".
+  push @filterargs, $args{filter} if $args{filter};
+  my $sql;
+  if ($args{count}) {
+    $sql = "SELECT count(*) FROM log l ";
+  } else {
+    $sql = "SELECT l.log_id AS logparent, l.user_id AS userid, l.name AS userfname, d.domain, l.domain_id, ".
+        "r.revnet AS revzone, ".
+        "l.rdns_id, l.entry AS logentry, date_trunc('second',l.stamp) AS logtime ".
+        "FROM log l ".
+        "LEFT JOIN domains d ON l.domain_id = d.domain_id ".
+        "LEFT JOIN revzones r ON l.rdns_id = r.rdns_id ";
+  }
+
+  # decide which ID argument to use.  Only use the "full name" if no normal ID is present
+  my $idarg;
+  if ($args{id}) {
+    $sql .= "WHERE l.$id_col{$args{logtype}} = ? ";
+    $idarg = $args{id};
+  } else {
+    $sql .= "WHERE l.name = ? ";
+    $idarg = $args{fname};
+  }
+
+  # trim log "subentries" - we'll figure out where to stash these later
+  $sql .= " AND logparent = 0";
+
+  # add the entry filter, if any
+  $sql .= ($args{filter} ? " AND entry ~* ?" : '');
+
+  # Limit scope based on group.  Mainly useful for ltype==user, so subgroup
+  # users can see what the deities in parent groups have done to their domains.
+  if ($args{group} != 1) {
+    my @grouplist;
+    $self->getChildren($args{group}, \@grouplist);
+    my $groupset = join(',', $args{group}, @grouplist);
+    $sql .= " AND l.group_id IN ($groupset)";
+  }
+
+  if ($args{count}) {
+    my ($count) = $dbh->selectrow_array($sql, undef, ($idarg, @filterargs) );
+    $errstr = $dbh->errstr if !$count;
+    return $count;
+  } else {
+    $sql .= " ORDER BY $args{sortby} $args{sortorder}, log_id $args{sortorder}".
 	($args{offset} eq 'all' ? '' : " LIMIT $self->{perpage} OFFSET ".$args{offset}*$self->{perpage});
-  my $loglist = $dbh->selectall_arrayref($sql, { Slice => {} }, ($args{id}, @filterargs) );
-  $errstr = $dbh->errstr if !$loglist;
-  return $loglist;
+    my @loglist;
+    my $sth = $dbh->prepare($sql);
+    my $logchild = $dbh->prepare("SELECT entry FROM log WHERE logparent = ? ORDER BY log_id");
+    $sth->execute($idarg, @filterargs);
+    while (my $row = $sth->fetchrow_hashref) {
+      $logchild->execute($row->{logparent});
+      my $childlist = $logchild->fetchall_arrayref({});
+      $row->{childentries} = $childlist;
+      push @loglist, $row;
+    }
+    return \@loglist;
+  }
+
+  # Your llama is on fire
+
 } # end getLogEntries()
+
+
+# a collection of joins for the record search
+our $recsearchsqlbase = q(
+FROM records r
+LEFT JOIN domains d ON r.domain_id = d.domain_id
+  LEFT JOIN groups g1 ON d.group_id = g1.group_id
+LEFT JOIN revzones z ON r.rdns_id = z.rdns_id
+  LEFT JOIN groups g2 on z.group_id = g2.group_id
+JOIN rectypes t ON r.type = t.val
+LEFT JOIN locations l ON r.location = l.location
+WHERE r.type <> 6 AND (r.host ~* ? OR r.val ~* ?)
+);
+
+
+## DNSDB::recSearchCount()
+# Get a total count for a global record search
+# Takes a hash with the search string and the login group of the user
+sub recSearchCount {
+  my $self = shift;
+  my $dbh = $self->{dbh};
+  my %args = @_;
+
+  my $sql = "SELECT count(*)".$recsearchsqlbase;
+
+  # Limit scope based on group
+  if ($args{group} != 1) {
+    my @grouplist;
+    $self->getChildren($args{group}, \@grouplist);
+    my $groupset = join(',', $args{group}, @grouplist);
+    # oh my aching HEAD.  there has to be a better way to do conditions on joined tables...
+    $sql .= "AND (
+    (g1.group_id IN ($groupset) AND g2.group_id IN ($groupset)) OR
+    (g1.group_id IN ($groupset) AND g2.group_id IS NULL) OR
+    (g1.group_id IS NULL AND g2.group_id IN ($groupset))
+    )
+";
+  }
+
+  my $count = $dbh->selectrow_array($sql, undef, $args{searchfor}, $args{searchfor});
+  $errstr = $dbh->errstr if !$count;
+  return $count;
+
+} # end recSearchCount()
+
+
+## DNSDB::recSearch()
+# Find records matching the search string
+# Takes a hash with the search string, login group of the user, pagination offset, sort field,
+# and sort direction
+# Returns a reference to a list of hashrefs
+sub recSearch {
+  my $self = shift;
+  my $dbh = $self->{dbh};
+  my %args = @_;
+
+  my $sql = q(SELECT
+    r.domain_id, d.domain, g1.group_name AS domgroup,
+    r.rdns_id, z.revnet AS revzone, g2.group_name AS revgroup,
+    r.host, t.name AS rectype, r.val, l.description AS location, r.record_id).
+    $recsearchsqlbase;
+
+  # Limit scope based on group
+  if ($args{group} != 1) {
+    my @grouplist;
+    $self->getChildren($args{group}, \@grouplist);
+    my $groupset = join(',', $args{group}, @grouplist);
+    # oh my aching HEAD.  there has to be a better way to do conditions on joined tables...
+    $sql .= "AND (
+    (g1.group_id IN ($groupset) AND g2.group_id IN ($groupset)) OR
+    (g1.group_id IN ($groupset) AND g2.group_id IS NULL) OR
+    (g1.group_id IS NULL AND g2.group_id IN ($groupset))
+    )
+";
+  }
+
+  # mixed tables means this isn't a simple prefix like the regular record list filter.  :/
+  my %sortmap = (
+    domain => 'd.domain',
+    revzone => 'z.revnet',
+    host => 'r.host',
+    type => 't.name', 
+    val => 'inetlazy(r.val)',
+    location => 'r.location',
+    );
+
+  # Sorting defaults
+  $args{sortorder} = 'ASC' if !$args{sortorder} || !grep /^$args{sortorder}$/, ('ASC','DESC');
+  $args{sortby} = 'r.host' if !$args{sortby} || $args{sortby} !~ /^[\w_.]+$/;
+  $args{offset} = 0 if !$args{offset} || $args{offset} !~ /^(?:all|\d+)$/;
+
+  $args{sortby} = $sortmap{$args{sortby}} if $args{sortby} !~ /\./;
+
+  # Add sort and offset to SQL
+  $sql .= "ORDER BY $args{sortby} $args{sortorder},record_id ASC\n";
+  $sql .= ($args{offset} eq 'all' ? '' : "LIMIT $self->{perpage} OFFSET ".$args{offset}*$self->{perpage});
+
+##fixme: should probably sent the warning somewhere else
+  my $ret = $dbh->selectall_arrayref($sql, { Slice => {} }, $args{searchfor}, $args{searchfor})
+    or warn $dbh->errstr;
+  return $ret;
+
+} # end recSearch()
 
 
@@ -5379,4 +5531,6 @@
   eval {
 
+    my $logparent;
+
     if ($rev eq 'n') {
 ##fixme:  serial
@@ -5386,5 +5540,5 @@
       ($zone_id) = $dbh->selectrow_array("SELECT currval('domains_domain_id_seq')");
       $domain_id = $zone_id;
-      $self->_log(group_id => $group, domain_id => $domain_id,
+      $logparent = $self->_log(group_id => $group, domain_id => $domain_id,
 		entry => "[Added ".($args{status} ? 'active' : 'inactive')." domain $zone via AXFR]");
     } else {
@@ -5395,5 +5549,5 @@
       ($zone_id) = $dbh->selectrow_array("SELECT currval('revzones_rdns_id_seq')");
       $rdns_id = $zone_id;
-      $self->_log(group_id => $group, rdns_id => $rdns_id,
+      $logparent = $self->_log(group_id => $group, rdns_id => $rdns_id,
 		entry => "[Added ".($args{status} ? 'active' : 'inactive')." reverse zone $cidr via AXFR]");
     }
@@ -5647,5 +5801,6 @@
 	$logentry .= " ".($rev eq 'y' ? $host : $val)."', TTL $ttl";
       }
-      $self->_log(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,
+	logparent => $logparent, entry => $logentry);
 
     } # while axfr_next
