1 package FS::domain_record;
4 use vars qw( @ISA $noserial_hack $DEBUG $me );
6 use FS::Record qw( qsearchs dbh ); #qsearch
10 @ISA = qw(FS::Record);
13 $me = '[FS::domain_record]';
17 FS::domain_record - Object methods for domain_record records
21 use FS::domain_record;
23 $record = new FS::domain_record \%hash;
24 $record = new FS::domain_record { 'column' => 'value' };
26 $error = $record->insert;
28 $error = $new_record->replace($old_record);
30 $error = $record->delete;
32 $error = $record->check;
36 An FS::domain_record object represents an entry in a DNS zone.
37 FS::domain_record inherits from FS::Record. The following fields are currently
42 =item recnum - primary key
44 =item svcnum - Domain (see L<FS::svc_domain>) of this entry
46 =item reczone - partial (or full) zone for this entry
48 =item recaf - address family for this entry, currently only `IN' is recognized.
50 =item rectype - record type for this entry (A, MX, etc.)
52 =item recdata - data for this entry
54 =item ttl - time to live
64 Creates a new entry. To add the entry to the database, see L<"insert">.
66 Note that this stores the hash reference, not a distinct copy of the hash it
67 points to. You can ask the object for a copy with the I<hash> method.
71 sub table { 'domain_record'; }
75 Adds this record to the database. If there is an error, returns the error,
76 otherwise returns false.
83 local $SIG{HUP} = 'IGNORE';
84 local $SIG{INT} = 'IGNORE';
85 local $SIG{QUIT} = 'IGNORE';
86 local $SIG{TERM} = 'IGNORE';
87 local $SIG{TSTP} = 'IGNORE';
88 local $SIG{PIPE} = 'IGNORE';
90 my $oldAutoCommit = $FS::UID::AutoCommit;
91 local $FS::UID::AutoCommit = 0;
94 if ( $self->rectype eq '_mstr' ) { #delete all other records
95 foreach my $domain_record ( reverse $self->svc_domain->domain_record ) {
96 my $error = $domain_record->delete;
98 $dbh->rollback if $oldAutoCommit;
104 my $error = $self->SUPER::insert;
106 $dbh->rollback if $oldAutoCommit;
110 unless ( $self->rectype =~ /^(SOA|_mstr)$/ ) {
111 my $error = $self->increment_serial;
113 $dbh->rollback if $oldAutoCommit;
118 my $conf = new FS::Conf;
119 if ( $self->rectype =~ /^A$/ && ! $conf->exists('disable_autoreverse') ) {
120 my $reverse = $self->reverse_record;
121 if ( $reverse && ! $reverse->recnum ) {
122 my $error = $reverse->insert;
124 $dbh->rollback if $oldAutoCommit;
125 return "error adding corresponding reverse-ARPA record: $error";
130 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
138 Delete this record from the database.
145 return "Can't delete a domain record which has a website!"
146 if qsearchs( 'svc_www', { 'recnum' => $self->recnum } );
148 local $SIG{HUP} = 'IGNORE';
149 local $SIG{INT} = 'IGNORE';
150 local $SIG{QUIT} = 'IGNORE';
151 local $SIG{TERM} = 'IGNORE';
152 local $SIG{TSTP} = 'IGNORE';
153 local $SIG{PIPE} = 'IGNORE';
155 my $oldAutoCommit = $FS::UID::AutoCommit;
156 local $FS::UID::AutoCommit = 0;
159 my $error = $self->SUPER::delete;
161 $dbh->rollback if $oldAutoCommit;
165 unless ( $self->rectype =~ /^(SOA|_mstr)$/ ) {
166 my $error = $self->increment_serial;
168 $dbh->rollback if $oldAutoCommit;
173 my $conf = new FS::Conf;
174 if ( $self->rectype =~ /^A$/ && ! $conf->exists('disable_autoreverse') ) {
175 my $reverse = $self->reverse_record;
176 if ( $reverse && $reverse->recnum && $reverse->recdata eq $self->zone.'.' ){
177 my $error = $reverse->delete;
179 $dbh->rollback if $oldAutoCommit;
180 return "error removing corresponding reverse-ARPA record: $error";
185 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
191 =item replace OLD_RECORD
193 Replaces the OLD_RECORD with this one in the database. If there is an error,
194 returns the error, otherwise returns false.
201 local $SIG{HUP} = 'IGNORE';
202 local $SIG{INT} = 'IGNORE';
203 local $SIG{QUIT} = 'IGNORE';
204 local $SIG{TERM} = 'IGNORE';
205 local $SIG{TSTP} = 'IGNORE';
206 local $SIG{PIPE} = 'IGNORE';
208 my $oldAutoCommit = $FS::UID::AutoCommit;
209 local $FS::UID::AutoCommit = 0;
212 my $error = $self->SUPER::replace(@_);
214 $dbh->rollback if $oldAutoCommit;
218 unless ( $self->rectype eq 'SOA' ) {
219 my $error = $self->increment_serial;
221 $dbh->rollback if $oldAutoCommit;
226 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
234 Checks all fields to make sure this is a valid entry. If there is
235 an error, returns the error, otherwise returns false. Called by the insert
240 # the check method should currently be supplied - FS::Record contains some
241 # data checking routines
247 $self->ut_numbern('recnum')
248 || $self->ut_number('svcnum')
250 return $error if $error;
252 return "Unknown svcnum (in svc_domain)"
253 unless qsearchs('svc_domain', { 'svcnum' => $self->svcnum } );
255 my $conf = new FS::Conf;
257 if ( $conf->exists('zone-underscore') ) {
258 $self->reczone =~ /^(@|[a-z0-9_\.\-\*]+)$/i
259 or return "Illegal reczone: ". $self->reczone;
262 $self->reczone =~ /^(@|[a-z0-9\.\-\*]+)$/i
263 or return "Illegal reczone: ". $self->reczone;
267 $self->recaf =~ /^(IN)$/ or return "Illegal recaf: ". $self->recaf;
270 $self->ttl =~ /^([0-9]{0,6})$/ or return "Illegal ttl: ". $self->ttl;
273 my %rectypes = map { $_=>1 } ( @{ $self->rectypes }, '_mstr' );
274 return 'Illegal rectype: '. $self->rectype
275 unless exists $rectypes{$self->rectype} && $rectypes{$self->rectype};
277 return "Illegal reczone for ". $self->rectype. ": ". $self->reczone
278 if $self->rectype !~ /^MX$/i && $self->reczone =~ /\*/;
280 if ( $self->rectype eq 'SOA' ) {
281 my $recdata = $self->recdata;
282 $recdata =~ s/\s+/ /g;
283 $recdata =~ /^([a-z0-9\.\-]+ [\w\-\+]+\.[a-z0-9\.\-]+ \( ((\d+|((\d+[WDHMS])+)) ){5}\))$/i
284 or return "Illegal data for SOA record: $recdata";
286 } elsif ( $self->rectype eq 'NS' ) {
287 $self->recdata =~ /^([a-z0-9\.\-]+)$/i
288 or return "Illegal data for NS record: ". $self->recdata;
290 } elsif ( $self->rectype eq 'MX' ) {
291 $self->recdata =~ /^(\d+)\s+([a-z0-9\.\-]+)$/i
292 or return "Illegal data for MX record: ". $self->recdata;
293 $self->recdata("$1 $2");
294 } elsif ( $self->rectype eq 'A' ) {
295 $self->recdata =~ /^((\d{1,3}\.){3}\d{1,3})$/
296 or return "Illegal data for A record: ". $self->recdata;
298 } elsif ( $self->rectype eq 'AAAA' ) {
299 $self->recdata =~ /^([\da-z:]+)$/
300 or return "Illegal data for AAAA record: ". $self->recdata;
302 } elsif ( $self->rectype eq 'PTR' ) {
303 if ( $conf->exists('zone-underscore') ) {
304 $self->recdata =~ /^([a-z0-9_\.\-]+)$/i
305 or return "Illegal data for PTR record: ". $self->recdata;
308 $self->recdata =~ /^([a-z0-9\.\-]+)$/i
309 or return "Illegal data for PTR record: ". $self->recdata;
312 } elsif ( $self->rectype eq 'CNAME' ) {
313 $self->recdata =~ /^([a-z0-9\.\-]+|\@)$/i
314 or return "Illegal data for CNAME record: ". $self->recdata;
316 } elsif ( $self->rectype eq 'TXT' ) {
317 if ( $self->recdata =~ /^((?:\S+)|(?:".+"))$/ ) {
320 $self->recdata('"'. $self->recdata. '"'); #?
322 # or return "Illegal data for TXT record: ". $self->recdata;
323 } elsif ( $self->rectype eq 'SRV' ) {
324 $self->recdata =~ /^(\d+)\s+(\d+)\s+(\d+)\s+([a-z0-9\.\-]+)$/i
325 or return "Illegal data for SRV record: ". $self->recdata;
326 $self->recdata("$1 $2 $3 $4");
327 } elsif ( $self->rectype eq '_mstr' ) {
328 $self->recdata =~ /^((\d{1,3}\.){3}\d{1,3})$/
329 or return "Illegal data for _master pseudo-record: ". $self->recdata;
331 warn "$me no specific check for ". $self->rectype. " records yet";
332 $error = $self->ut_text('recdata');
333 return $error if $error;
339 =item increment_serial
343 sub increment_serial {
344 return '' if $noserial_hack;
347 my $soa = qsearchs('domain_record', {
348 svcnum => $self->svcnum,
351 rectype => 'SOA', } )
352 || qsearchs('domain_record', {
353 svcnum => $self->svcnum,
354 reczone => $self->svc_domain->domain.'.',
358 or return "soa record not found; can't increment serial";
360 my $data = $soa->recdata;
361 $data =~ s/(\(\D*)(\d+)/$1.($2+1)/e; #well, it works.
363 my %hash = $soa->hash;
364 $hash{recdata} = $data;
365 my $new = new FS::domain_record \%hash;
371 Returns the domain (see L<FS::svc_domain>) for this record.
377 qsearchs('svc_domain', { svcnum => $self->svcnum } );
382 Returns the canonical zone name.
388 my $zone = $self->reczone; # or die ?
389 if ( $zone =~ /\.$/ ) {
392 my $svc_domain = $self->svc_domain; # or die ?
393 $zone .= '.'. $svc_domain->domain;
401 Returns the corresponding reverse-ARPA record as another FS::domain_record
402 object. If the specific record does not exist in the database but the
403 reverse-ARPA zone itself does, an appropriate new record is created. If no
404 reverse-ARPA zone is available at all, returns false.
406 (You can test whether or not record itself exists in the database or is a new
407 object that might need to be inserted by checking the recnum field)
409 Mostly used by the insert and delete methods - probably should see them for
416 warn "reverse_record called\n" if $DEBUG;
417 #should support classless reverse-ARPA ala rfc2317 too
418 $self->recdata =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/
420 my $domain = "$3.$2.$1.in-addr.arpa";
421 my $ptr_reczone = $4;
422 warn "reverse_record: searching for domain: $domain\n" if $DEBUG;
423 my $svc_domain = qsearchs('svc_domain', { 'domain' => $domain } )
425 warn "reverse_record: found domain: $domain\n" if $DEBUG;
427 'svcnum' => $svc_domain->svcnum,
428 'reczone' => $ptr_reczone,
432 qsearchs('domain_record', \%hash )
433 or new FS::domain_record { %hash, 'recdata' => $self->zone.'.' };
439 #http://en.wikipedia.org/wiki/List_of_DNS_record_types
440 #DHCID? other things?
442 [ qw(SOA A AAAA CNAME MX NS PTR SPF SRV TXT), #most common types
443 #qw(DNAME), #uncommon types
444 qw(DLV DNSKEY DS NSEC NSEC3 NSEC3PARAM RRSIG), #DNSSEC types
452 The data validation doesn't check everything it could. In particular,
453 there is no protection against bad data that passes the regex, duplicate
454 SOA records, forgetting the trailing `.', impossible IP addersses, etc. Of
455 course, it's still better than editing the zone files directly. :)
459 L<FS::Record>, schema.html from the base documentation.