RT# 83450 - fixed rateplan export
[freeside.git] / FS / FS / domain_record.pm
index 77b9550..e180e4b 100644 (file)
@@ -1,13 +1,14 @@
 package FS::domain_record;
 package FS::domain_record;
+use base qw(FS::Record);
 
 use strict;
 
 use strict;
-use vars qw( @ISA $noserial_hack );
-#use FS::Record qw( qsearch qsearchs );
-use FS::Record qw( qsearchs dbh );
-use FS::svc_domain;
+use vars qw( $noserial_hack $DEBUG $me );
+use FS::Conf;
+use FS::Record qw( qsearchs dbh ); #qsearch
 use FS::svc_www;
 
 use FS::svc_www;
 
-@ISA = qw(FS::Record);
+$DEBUG = 0;
+$me = '[FS::domain_record]';
 
 =head1 NAME
 
 
 =head1 NAME
 
@@ -48,6 +49,8 @@ supported:
 
 =item recdata - data for this entry
 
 
 =item recdata - data for this entry
 
+=item ttl - time to live
+
 =back
 
 =head1 METHODS
 =back
 
 =head1 METHODS
@@ -56,7 +59,7 @@ supported:
 
 =item new HASHREF
 
 
 =item new HASHREF
 
-Creates a new entry.  To add the example to the database, see L<"insert">.
+Creates a new entry.  To add the entry to the database, see L<"insert">.
 
 Note that this stores the hash reference, not a distinct copy of the hash it
 points to.  You can ask the object for a copy with the I<hash> method.
 
 Note that this stores the hash reference, not a distinct copy of the hash it
 points to.  You can ask the object for a copy with the I<hash> method.
@@ -110,6 +113,18 @@ sub insert {
     }
   }
 
     }
   }
 
+  my $conf = new FS::Conf;
+  if ( $self->rectype =~ /^A$/ && ! $conf->exists('disable_autoreverse') ) {
+    my $reverse = $self->reverse_record;
+    if ( $reverse && ! $reverse->recnum ) {
+      my $error = $reverse->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "error adding corresponding reverse-ARPA record: $error";
+      }
+    }
+  }
+
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
 
   '';
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
 
   '';
@@ -153,6 +168,18 @@ sub delete {
     }
   }
 
     }
   }
 
+  my $conf = new FS::Conf;
+  if ( $self->rectype =~ /^A$/ && ! $conf->exists('disable_autoreverse') ) {
+    my $reverse = $self->reverse_record;
+    if ( $reverse && $reverse->recnum && $reverse->recdata eq $self->zone.'.' ){
+      my $error = $reverse->delete;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "error removing corresponding reverse-ARPA record: $error";
+      }
+    }
+  }
+
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
 
   '';
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
 
   '';
@@ -202,7 +229,7 @@ sub replace {
 
 =item check
 
 
 =item check
 
-Checks all fields to make sure this is a valid example.  If there is
+Checks all fields to make sure this is a valid entry.  If there is
 an error, returns the error, otherwise returns false.  Called by the insert
 and replace methods.
 
 an error, returns the error, otherwise returns false.  Called by the insert
 and replace methods.
 
@@ -223,17 +250,27 @@ sub check {
   return "Unknown svcnum (in svc_domain)"
     unless qsearchs('svc_domain', { 'svcnum' => $self->svcnum } );
 
   return "Unknown svcnum (in svc_domain)"
     unless qsearchs('svc_domain', { 'svcnum' => $self->svcnum } );
 
-  $self->reczone =~ /^(@|[a-z0-9\.\-\*]+)$/i
-    or return "Illegal reczone: ". $self->reczone;
-  $self->reczone($1);
+  my $conf = new FS::Conf;
+
+  if ( $conf->exists('zone-underscore') ) {
+    $self->reczone =~ /^(@|[a-z0-9_\.\-\*]+)$/i
+      or return "Illegal reczone: ". $self->reczone;
+    $self->reczone($1);
+  } else {
+    $self->reczone =~ /^(@|[a-z0-9\.\-\*]+)$/i
+      or return "Illegal reczone: ". $self->reczone;
+    $self->reczone($1);
+  }
 
   $self->recaf =~ /^(IN)$/ or return "Illegal recaf: ". $self->recaf;
   $self->recaf($1);
 
 
   $self->recaf =~ /^(IN)$/ or return "Illegal recaf: ". $self->recaf;
   $self->recaf($1);
 
-  $self->rectype =~ /^(SOA|NS|MX|A|PTR|CNAME|_mstr)$/
-    or return "Illegal rectype (only SOA NS MX A PTR CNAME recognized): ".
-              $self->rectype;
-  $self->rectype($1);
+  $self->ttl =~ /^([0-9]{0,6})$/ or return "Illegal ttl: ". $self->ttl;        
+  $self->ttl($1); 
+
+  my %rectypes = map { $_=>1 } ( @{ $self->rectypes }, '_mstr' );
+  return 'Illegal rectype: '. $self->rectype
+    unless exists $rectypes{$self->rectype} && $rectypes{$self->rectype};
 
   return "Illegal reczone for ". $self->rectype. ": ". $self->reczone
     if $self->rectype !~ /^MX$/i && $self->reczone =~ /\*/;
 
   return "Illegal reczone for ". $self->rectype. ": ". $self->reczone
     if $self->rectype !~ /^MX$/i && $self->reczone =~ /\*/;
@@ -256,22 +293,45 @@ sub check {
     $self->recdata =~ /^((\d{1,3}\.){3}\d{1,3})$/
       or return "Illegal data for A record: ". $self->recdata;
     $self->recdata($1);
     $self->recdata =~ /^((\d{1,3}\.){3}\d{1,3})$/
       or return "Illegal data for A record: ". $self->recdata;
     $self->recdata($1);
-  } elsif ( $self->rectype eq 'PTR' ) {
-    $self->recdata =~ /^([a-z0-9\.\-]+)$/i
-      or return "Illegal data for PTR record: ". $self->recdata;
+  } elsif ( $self->rectype eq 'AAAA' ) {
+    $self->recdata =~ /^([\da-z:]+)$/
+      or return "Illegal data for AAAA record: ". $self->recdata;
     $self->recdata($1);
     $self->recdata($1);
+  } elsif ( $self->rectype eq 'PTR' ) {
+    if ( $conf->exists('zone-underscore') ) {
+      $self->recdata =~ /^([a-z0-9_\.\-]+)$/i
+        or return "Illegal data for PTR record: ". $self->recdata;
+      $self->recdata($1);
+    } else {
+      $self->recdata =~ /^([a-z0-9\.\-]+)$/i
+        or return "Illegal data for PTR record: ". $self->recdata;
+      $self->recdata($1);
+    }
   } elsif ( $self->rectype eq 'CNAME' ) {
     $self->recdata =~ /^([a-z0-9\.\-]+|\@)$/i
       or return "Illegal data for CNAME record: ". $self->recdata;
     $self->recdata($1);
   } elsif ( $self->rectype eq 'CNAME' ) {
     $self->recdata =~ /^([a-z0-9\.\-]+|\@)$/i
       or return "Illegal data for CNAME record: ". $self->recdata;
     $self->recdata($1);
+  } elsif ( $self->rectype eq 'TXT' ) {
+    if ( $self->recdata =~ /^((?:\S+)|(?:".+"))$/ ) {
+      $self->recdata($1);
+    } else {
+      $self->recdata('"'. $self->recdata. '"'); #?
+    }
+    #  or return "Illegal data for TXT record: ". $self->recdata;
+  } elsif ( $self->rectype eq 'SRV' ) {                                        
+    $self->recdata =~ /^(\d+)\s+(\d+)\s+(\d+)\s+([a-z0-9\.\-]+)$/i             
+      or return "Illegal data for SRV record: ". $self->recdata;               
+    $self->recdata("$1 $2 $3 $4");                        
   } elsif ( $self->rectype eq '_mstr' ) {
     $self->recdata =~ /^((\d{1,3}\.){3}\d{1,3})$/
       or return "Illegal data for _master pseudo-record: ". $self->recdata;
   } else {
   } elsif ( $self->rectype eq '_mstr' ) {
     $self->recdata =~ /^((\d{1,3}\.){3}\d{1,3})$/
       or return "Illegal data for _master pseudo-record: ". $self->recdata;
   } else {
-    die "ack!";
+    warn "$me no specific check for ". $self->rectype. " records yet";
+    $error = $self->ut_text('recdata');
+    return $error if $error;
   }
 
   }
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =item increment_serial
 }
 
 =item increment_serial
@@ -284,10 +344,16 @@ sub increment_serial {
 
   my $soa = qsearchs('domain_record', {
     svcnum  => $self->svcnum,
 
   my $soa = qsearchs('domain_record', {
     svcnum  => $self->svcnum,
-    reczone => '@', #or full domain ?
+    reczone => '@',
+    recaf   => 'IN',
+    rectype => 'SOA', } )
+  || qsearchs('domain_record', {
+    svcnum  => $self->svcnum,
+    reczone => $self->svc_domain->domain.'.',
     recaf   => 'IN',
     rectype => 'SOA', 
     recaf   => 'IN',
     rectype => 'SOA', 
-  } ) or return "soa record not found; can't increment serial";
+  } )
+  or return "soa record not found; can't increment serial";
 
   my $data = $soa->recdata;
   $data =~ s/(\(\D*)(\d+)/$1.($2+1)/e; #well, it works.
 
   my $data = $soa->recdata;
   $data =~ s/(\(\D*)(\d+)/$1.($2+1)/e; #well, it works.
@@ -302,13 +368,6 @@ sub increment_serial {
 
 Returns the domain (see L<FS::svc_domain>) for this record.
 
 
 Returns the domain (see L<FS::svc_domain>) for this record.
 
-=cut
-
-sub svc_domain {
-  my $self = shift;
-  qsearchs('svc_domain', { svcnum => $self->svcnum } );
-}
-
 =item zone
 
 Returns the canonical zone name.
 =item zone
 
 Returns the canonical zone name.
@@ -328,11 +387,56 @@ sub zone {
   $zone;
 }
 
   $zone;
 }
 
-=back
+=item reverse_record 
+
+Returns the corresponding reverse-ARPA record as another FS::domain_record
+object.  If the specific record does not exist in the database but the 
+reverse-ARPA zone itself does, an appropriate new record is created.  If no
+reverse-ARPA zone is available at all, returns false.
+
+(You can test whether or not record itself exists in the database or is a new
+object that might need to be inserted by checking the recnum field)
 
 
-=head1 VERSION
+Mostly used by the insert and delete methods - probably should see them for
+examples.
 
 
-$Id: domain_record.pm,v 1.15 2003-04-29 18:28:50 khoff Exp $
+=cut
+
+sub reverse_record {
+  my $self = shift;
+  warn "reverse_record called\n" if $DEBUG;
+  #should support classless reverse-ARPA ala rfc2317 too
+  $self->recdata =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/
+    or return '';
+  my $domain = "$3.$2.$1.in-addr.arpa"; 
+  my $ptr_reczone = $4;
+  warn "reverse_record: searching for domain: $domain\n" if $DEBUG;
+  my $svc_domain = qsearchs('svc_domain', { 'domain' => $domain } )
+    or return '';
+  warn "reverse_record: found domain: $domain\n" if $DEBUG;
+  my %hash = (
+    'svcnum'  => $svc_domain->svcnum,
+    'reczone' => $ptr_reczone,
+    'recaf'   => 'IN',
+    'rectype' => 'PTR',
+  );
+  qsearchs('domain_record', \%hash )
+    or new FS::domain_record { %hash, 'recdata' => $self->zone.'.' };
+}
+
+=item rectypes
+
+=cut
+#http://en.wikipedia.org/wiki/List_of_DNS_record_types
+#DHCID?  other things?
+sub rectypes {
+  [ qw(SOA A AAAA CNAME MX NS PTR SPF SRV TXT), #most common types
+    #qw(DNAME), #uncommon types
+    qw(DLV DNSKEY DS NSEC NSEC3 NSEC3PARAM RRSIG), #DNSSEC types
+  ];
+}
+
+=back
 
 =head1 BUGS
 
 
 =head1 BUGS