1 package FS::domain_record;
2 use base qw(FS::Record);
5 use vars qw( $noserial_hack $DEBUG $me );
7 use FS::Record qw( qsearchs dbh ); #qsearch
11 $me = '[FS::domain_record]';
15 FS::domain_record - Object methods for domain_record records
19 use FS::domain_record;
21 $record = new FS::domain_record \%hash;
22 $record = new FS::domain_record { 'column' => 'value' };
24 $error = $record->insert;
26 $error = $new_record->replace($old_record);
28 $error = $record->delete;
30 $error = $record->check;
34 An FS::domain_record object represents an entry in a DNS zone.
35 FS::domain_record inherits from FS::Record. The following fields are currently
40 =item recnum - primary key
42 =item svcnum - Domain (see L<FS::svc_domain>) of this entry
44 =item reczone - partial (or full) zone for this entry
46 =item recaf - address family for this entry, currently only `IN' is recognized.
48 =item rectype - record type for this entry (A, MX, etc.)
50 =item recdata - data for this entry
52 =item ttl - time to live
62 Creates a new entry. To add the entry to the database, see L<"insert">.
64 Note that this stores the hash reference, not a distinct copy of the hash it
65 points to. You can ask the object for a copy with the I<hash> method.
69 sub table { 'domain_record'; }
73 Adds this record to the database. If there is an error, returns the error,
74 otherwise returns false.
81 local $SIG{HUP} = 'IGNORE';
82 local $SIG{INT} = 'IGNORE';
83 local $SIG{QUIT} = 'IGNORE';
84 local $SIG{TERM} = 'IGNORE';
85 local $SIG{TSTP} = 'IGNORE';
86 local $SIG{PIPE} = 'IGNORE';
88 my $oldAutoCommit = $FS::UID::AutoCommit;
89 local $FS::UID::AutoCommit = 0;
92 if ( $self->rectype eq '_mstr' ) { #delete all other records
93 foreach my $domain_record ( reverse $self->svc_domain->domain_record ) {
94 my $error = $domain_record->delete;
96 $dbh->rollback if $oldAutoCommit;
102 my $error = $self->SUPER::insert;
104 $dbh->rollback if $oldAutoCommit;
108 unless ( $self->rectype =~ /^(SOA|_mstr)$/ ) {
109 my $error = $self->increment_serial;
111 $dbh->rollback if $oldAutoCommit;
116 my $conf = new FS::Conf;
117 if ( $self->rectype =~ /^A$/ && ! $conf->exists('disable_autoreverse') ) {
118 my $reverse = $self->reverse_record;
119 if ( $reverse && ! $reverse->recnum ) {
120 my $error = $reverse->insert;
122 $dbh->rollback if $oldAutoCommit;
123 return "error adding corresponding reverse-ARPA record: $error";
128 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
136 Delete this record from the database.
143 return "Can't delete a domain record which has a website!"
144 if qsearchs( 'svc_www', { 'recnum' => $self->recnum } );
146 local $SIG{HUP} = 'IGNORE';
147 local $SIG{INT} = 'IGNORE';
148 local $SIG{QUIT} = 'IGNORE';
149 local $SIG{TERM} = 'IGNORE';
150 local $SIG{TSTP} = 'IGNORE';
151 local $SIG{PIPE} = 'IGNORE';
153 my $oldAutoCommit = $FS::UID::AutoCommit;
154 local $FS::UID::AutoCommit = 0;
157 my $error = $self->SUPER::delete;
159 $dbh->rollback if $oldAutoCommit;
163 unless ( $self->rectype =~ /^(SOA|_mstr)$/ ) {
164 my $error = $self->increment_serial;
166 $dbh->rollback if $oldAutoCommit;
171 my $conf = new FS::Conf;
172 if ( $self->rectype =~ /^A$/ && ! $conf->exists('disable_autoreverse') ) {
173 my $reverse = $self->reverse_record;
174 if ( $reverse && $reverse->recnum && $reverse->recdata eq $self->zone.'.' ){
175 my $error = $reverse->delete;
177 $dbh->rollback if $oldAutoCommit;
178 return "error removing corresponding reverse-ARPA record: $error";
183 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
189 =item replace OLD_RECORD
191 Replaces the OLD_RECORD with this one in the database. If there is an error,
192 returns the error, otherwise returns false.
199 local $SIG{HUP} = 'IGNORE';
200 local $SIG{INT} = 'IGNORE';
201 local $SIG{QUIT} = 'IGNORE';
202 local $SIG{TERM} = 'IGNORE';
203 local $SIG{TSTP} = 'IGNORE';
204 local $SIG{PIPE} = 'IGNORE';
206 my $oldAutoCommit = $FS::UID::AutoCommit;
207 local $FS::UID::AutoCommit = 0;
210 my $error = $self->SUPER::replace(@_);
212 $dbh->rollback if $oldAutoCommit;
216 unless ( $self->rectype eq 'SOA' ) {
217 my $error = $self->increment_serial;
219 $dbh->rollback if $oldAutoCommit;
224 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
232 Checks all fields to make sure this is a valid entry. If there is
233 an error, returns the error, otherwise returns false. Called by the insert
238 # the check method should currently be supplied - FS::Record contains some
239 # data checking routines
245 $self->ut_numbern('recnum')
246 || $self->ut_number('svcnum')
248 return $error if $error;
250 return "Unknown svcnum (in svc_domain)"
251 unless qsearchs('svc_domain', { 'svcnum' => $self->svcnum } );
253 my $conf = new FS::Conf;
255 if ( $conf->exists('zone-underscore') ) {
256 $self->reczone =~ /^(@|[a-z0-9_\.\-\*]+)$/i
257 or return "Illegal reczone: ". $self->reczone;
260 $self->reczone =~ /^(@|[a-z0-9\.\-\*]+)$/i
261 or return "Illegal reczone: ". $self->reczone;
265 $self->recaf =~ /^(IN)$/ or return "Illegal recaf: ". $self->recaf;
268 $self->ttl =~ /^([0-9]{0,6})$/ or return "Illegal ttl: ". $self->ttl;
271 my %rectypes = map { $_=>1 } ( @{ $self->rectypes }, '_mstr' );
272 return 'Illegal rectype: '. $self->rectype
273 unless exists $rectypes{$self->rectype} && $rectypes{$self->rectype};
275 return "Illegal reczone for ". $self->rectype. ": ". $self->reczone
276 if $self->rectype !~ /^MX$/i && $self->reczone =~ /\*/;
278 if ( $self->rectype eq 'SOA' ) {
279 my $recdata = $self->recdata;
280 $recdata =~ s/\s+/ /g;
281 $recdata =~ /^([a-z0-9\.\-]+ [\w\-\+]+\.[a-z0-9\.\-]+ \( ((\d+|((\d+[WDHMS])+)) ){5}\))$/i
282 or return "Illegal data for SOA record: $recdata";
284 } elsif ( $self->rectype eq 'NS' ) {
285 $self->recdata =~ /^([a-z0-9\.\-]+)$/i
286 or return "Illegal data for NS record: ". $self->recdata;
288 } elsif ( $self->rectype eq 'MX' ) {
289 $self->recdata =~ /^(\d+)\s+([a-z0-9\.\-]+)$/i
290 or return "Illegal data for MX record: ". $self->recdata;
291 $self->recdata("$1 $2");
292 } elsif ( $self->rectype eq 'A' ) {
293 $self->recdata =~ /^((\d{1,3}\.){3}\d{1,3})$/
294 or return "Illegal data for A record: ". $self->recdata;
296 } elsif ( $self->rectype eq 'AAAA' ) {
297 $self->recdata =~ /^([\da-z:]+)$/
298 or return "Illegal data for AAAA record: ". $self->recdata;
300 } elsif ( $self->rectype eq 'PTR' ) {
301 if ( $conf->exists('zone-underscore') ) {
302 $self->recdata =~ /^([a-z0-9_\.\-]+)$/i
303 or return "Illegal data for PTR record: ". $self->recdata;
306 $self->recdata =~ /^([a-z0-9\.\-]+)$/i
307 or return "Illegal data for PTR record: ". $self->recdata;
310 } elsif ( $self->rectype eq 'CNAME' ) {
311 $self->recdata =~ /^([a-z0-9\.\-]+|\@)$/i
312 or return "Illegal data for CNAME record: ". $self->recdata;
314 } elsif ( $self->rectype eq 'TXT' ) {
315 if ( $self->recdata =~ /^((?:\S+)|(?:".+"))$/ ) {
318 $self->recdata('"'. $self->recdata. '"'); #?
320 # or return "Illegal data for TXT record: ". $self->recdata;
321 } elsif ( $self->rectype eq 'SRV' ) {
322 $self->recdata =~ /^(\d+)\s+(\d+)\s+(\d+)\s+([a-z0-9\.\-]+)$/i
323 or return "Illegal data for SRV record: ". $self->recdata;
324 $self->recdata("$1 $2 $3 $4");
325 } elsif ( $self->rectype eq '_mstr' ) {
326 $self->recdata =~ /^((\d{1,3}\.){3}\d{1,3})$/
327 or return "Illegal data for _master pseudo-record: ". $self->recdata;
329 warn "$me no specific check for ". $self->rectype. " records yet";
330 $error = $self->ut_text('recdata');
331 return $error if $error;
337 =item increment_serial
341 sub increment_serial {
342 return '' if $noserial_hack;
345 my $soa = qsearchs('domain_record', {
346 svcnum => $self->svcnum,
349 rectype => 'SOA', } )
350 || qsearchs('domain_record', {
351 svcnum => $self->svcnum,
352 reczone => $self->svc_domain->domain.'.',
356 or return "soa record not found; can't increment serial";
358 my $data = $soa->recdata;
359 $data =~ s/(\(\D*)(\d+)/$1.($2+1)/e; #well, it works.
361 my %hash = $soa->hash;
362 $hash{recdata} = $data;
363 my $new = new FS::domain_record \%hash;
369 Returns the domain (see L<FS::svc_domain>) for this record.
373 Returns the canonical zone name.
379 my $zone = $self->reczone; # or die ?
380 if ( $zone =~ /\.$/ ) {
383 my $svc_domain = $self->svc_domain; # or die ?
384 $zone .= '.'. $svc_domain->domain;
392 Returns the corresponding reverse-ARPA record as another FS::domain_record
393 object. If the specific record does not exist in the database but the
394 reverse-ARPA zone itself does, an appropriate new record is created. If no
395 reverse-ARPA zone is available at all, returns false.
397 (You can test whether or not record itself exists in the database or is a new
398 object that might need to be inserted by checking the recnum field)
400 Mostly used by the insert and delete methods - probably should see them for
407 warn "reverse_record called\n" if $DEBUG;
408 #should support classless reverse-ARPA ala rfc2317 too
409 $self->recdata =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/
411 my $domain = "$3.$2.$1.in-addr.arpa";
412 my $ptr_reczone = $4;
413 warn "reverse_record: searching for domain: $domain\n" if $DEBUG;
414 my $svc_domain = qsearchs('svc_domain', { 'domain' => $domain } )
416 warn "reverse_record: found domain: $domain\n" if $DEBUG;
418 'svcnum' => $svc_domain->svcnum,
419 'reczone' => $ptr_reczone,
423 qsearchs('domain_record', \%hash )
424 or new FS::domain_record { %hash, 'recdata' => $self->zone.'.' };
430 #http://en.wikipedia.org/wiki/List_of_DNS_record_types
431 #DHCID? other things?
433 [ qw(SOA A AAAA CNAME MX NS PTR SPF SRV TXT), #most common types
434 #qw(DNAME), #uncommon types
435 qw(DLV DNSKEY DS NSEC NSEC3 NSEC3PARAM RRSIG), #DNSSEC types
443 The data validation doesn't check everything it could. In particular,
444 there is no protection against bad data that passes the regex, duplicate
445 SOA records, forgetting the trailing `.', impossible IP addersses, etc. Of
446 course, it's still better than editing the zone files directly. :)
450 L<FS::Record>, schema.html from the base documentation.