Changed description of "action" field to match domain registration exports.
[freeside.git] / FS / FS / svc_domain.pm
1 package FS::svc_domain;
2
3 use strict;
4 use vars qw( @ISA $whois_hack $conf
5   @defaultrecords $soadefaultttl $soaemail $soaexpire $soamachine
6   $soarefresh $soaretry
7 );
8 use Carp;
9 use Scalar::Util qw( blessed );
10 use Date::Format;
11 #use Net::Whois::Raw;
12 use Net::Domain::TLD qw(tld_exists);
13 use FS::Record qw(fields qsearch qsearchs dbh);
14 use FS::Conf;
15 use FS::svc_Common;
16 use FS::svc_Parent_Mixin;
17 use FS::cust_svc;
18 use FS::svc_acct;
19 use FS::cust_pkg;
20 use FS::cust_main;
21 use FS::domain_record;
22 use FS::queue;
23
24 @ISA = qw( FS::svc_Parent_Mixin FS::svc_Common );
25
26 #ask FS::UID to run this stuff for us later
27 $FS::UID::callback{'FS::domain'} = sub { 
28   $conf = new FS::Conf;
29
30   @defaultrecords = $conf->config('defaultrecords');
31   $soadefaultttl = $conf->config('soadefaultttl');
32   $soaemail      = $conf->config('soaemail');
33   $soaexpire     = $conf->config('soaexpire');
34   $soamachine    = $conf->config('soamachine');
35   $soarefresh    = $conf->config('soarefresh');
36   $soaretry      = $conf->config('soaretry');
37
38 };
39
40 =head1 NAME
41
42 FS::svc_domain - Object methods for svc_domain records
43
44 =head1 SYNOPSIS
45
46   use FS::svc_domain;
47
48   $record = new FS::svc_domain \%hash;
49   $record = new FS::svc_domain { 'column' => 'value' };
50
51   $error = $record->insert;
52
53   $error = $new_record->replace($old_record);
54
55   $error = $record->delete;
56
57   $error = $record->check;
58
59   $error = $record->suspend;
60
61   $error = $record->unsuspend;
62
63   $error = $record->cancel;
64
65 =head1 DESCRIPTION
66
67 An FS::svc_domain object represents a domain.  FS::svc_domain inherits from
68 FS::svc_Common.  The following fields are currently supported:
69
70 =over 4
71
72 =item svcnum - primary key (assigned automatically for new accounts)
73
74 =item domain
75
76 =item catchall - optional svcnum of an svc_acct record, designating an email catchall account.
77
78 =item suffix - 
79
80 =item parent_svcnum -
81
82 =item registrarnum - Registrar (see L<FS::registrar>)
83
84 =item registrarkey - Registrar key or password for this domain
85
86 =item setup_date - UNIX timestamp
87
88 =item renewal_interval - Number of days before expiration date to start renewal
89
90 =item expiration_date - UNIX timestamp
91
92 =back
93
94 =head1 METHODS
95
96 =over 4
97
98 =item new HASHREF
99
100 Creates a new domain.  To add the domain to the database, see L<"insert">.
101
102 =cut
103
104 sub table_info {
105   {
106     'name' => 'Domain',
107     'sorts' => 'domain',
108     'display_weight' => 20,
109     'cancel_weight'  => 60,
110     'fields' => {
111       'domain' => 'Domain',
112     },
113   };
114 }
115
116 sub table { 'svc_domain'; }
117
118 sub search_sql {
119   my($class, $string) = @_;
120   $class->search_sql_field('domain', $string);
121 }
122
123
124 =item label
125
126 Returns the domain.
127
128 =cut
129
130 sub label {
131   my $self = shift;
132   $self->domain;
133 }
134
135 =item insert [ , OPTION => VALUE ... ]
136
137 Adds this domain to the database.  If there is an error, returns the error,
138 otherwise returns false.
139
140 The additional fields I<pkgnum> and I<svcpart> (see L<FS::cust_svc>) should be 
141 defined.  An FS::cust_svc record will be created and inserted.
142
143 The additional field I<action> should be set to I<N> for new domains, I<M>
144 for transfers, or I<I> for no action (registered elsewhere).
145
146 A registration or transfer email will be submitted unless
147 $FS::svc_domain::whois_hack is true.
148
149 The additional field I<email> can be used to manually set the admin contact
150 email address on this email.  Otherwise, the svc_acct records for this package 
151 (see L<FS::cust_pkg>) are searched.  If there is exactly one svc_acct record
152 in the same package, it is automatically used.  Otherwise an error is returned.
153
154 If any I<soamachine> configuration file exists, an SOA record is added to
155 the domain_record table (see <FS::domain_record>).
156
157 If any records are defined in the I<defaultrecords> configuration file,
158 appropriate records are added to the domain_record table (see
159 L<FS::domain_record>).
160
161 Currently available options are: I<depend_jobnum>
162
163 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
164 jobnums), all provisioning jobs will have a dependancy on the supplied
165 jobnum(s) (they will not run until the specific job(s) complete(s)).
166
167 =cut
168
169 sub insert {
170   my $self = shift;
171   my $error;
172
173   local $SIG{HUP} = 'IGNORE';
174   local $SIG{INT} = 'IGNORE';
175   local $SIG{QUIT} = 'IGNORE';
176   local $SIG{TERM} = 'IGNORE';
177   local $SIG{TSTP} = 'IGNORE';
178   local $SIG{PIPE} = 'IGNORE';
179
180   my $oldAutoCommit = $FS::UID::AutoCommit;
181   local $FS::UID::AutoCommit = 0;
182   my $dbh = dbh;
183
184   $error = $self->check;
185   return $error if $error;
186
187   return "Domain in use (here)"
188     if qsearchs( 'svc_domain', { 'domain' => $self->domain } );
189
190
191   $error = $self->SUPER::insert(@_);
192   if ( $error ) {
193     $dbh->rollback if $oldAutoCommit;
194     return $error;
195   }
196
197   if ( $soamachine ) {
198     my $soa = new FS::domain_record {
199       'svcnum'  => $self->svcnum,
200       'reczone' => '@',
201       'recaf'   => 'IN',
202       'rectype' => 'SOA',
203       'recdata' => "$soamachine $soaemail ( ". time2str("%Y%m%d", time). "00 ".
204                    "$soarefresh $soaretry $soaexpire $soadefaultttl )"
205     };
206     $error = $soa->insert;
207     if ( $error ) {
208       $dbh->rollback if $oldAutoCommit;
209       return "couldn't insert SOA record for new domain: $error";
210     }
211
212     foreach my $record ( @defaultrecords ) {
213       my($zone,$af,$type,$data) = split(/\s+/,$record,4);
214       my $domain_record = new FS::domain_record {
215         'svcnum'  => $self->svcnum,
216         'reczone' => $zone,
217         'recaf'   => $af,
218         'rectype' => $type,
219         'recdata' => $data,
220       };
221       my $error = $domain_record->insert;
222       if ( $error ) {
223         $dbh->rollback if $oldAutoCommit;
224         return "couldn't insert record for new domain: $error";
225       }
226     }
227
228   }
229
230   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
231
232   ''; #no error
233 }
234
235 =item delete
236
237 Deletes this domain from the database.  If there is an error, returns the
238 error, otherwise returns false.
239
240 The corresponding FS::cust_svc record will be deleted as well.
241
242 =cut
243
244 sub delete {
245   my $self = shift;
246
247   return "Can't delete a domain which has accounts!"
248     if qsearch( 'svc_acct', { 'domsvc' => $self->svcnum } );
249
250   #return "Can't delete a domain with (domain_record) zone entries!"
251   #  if qsearch('domain_record', { 'svcnum' => $self->svcnum } );
252
253   local $SIG{HUP} = 'IGNORE';
254   local $SIG{INT} = 'IGNORE';
255   local $SIG{QUIT} = 'IGNORE';
256   local $SIG{TERM} = 'IGNORE';
257   local $SIG{TSTP} = 'IGNORE';
258   local $SIG{PIPE} = 'IGNORE';
259
260   my $oldAutoCommit = $FS::UID::AutoCommit;
261   local $FS::UID::AutoCommit = 0;
262   my $dbh = dbh;
263
264   foreach my $domain_record ( reverse $self->domain_record ) {
265     my $error = $domain_record->delete;
266     if ( $error ) {
267       $dbh->rollback if $oldAutoCommit;
268       return "can't delete DNS entry: ".
269              join(' ', map $domain_record->$_(),
270                            qw( reczone recaf rectype recdata )
271                  ).
272              ":$error";
273     }
274   }
275
276   my $error = $self->SUPER::delete(@_);
277   if ( $error ) {
278     $dbh->rollback if $oldAutoCommit;
279     return $error;
280   }
281
282   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
283 }
284
285 =item replace OLD_RECORD
286
287 Replaces OLD_RECORD with this one in the database.  If there is an error,
288 returns the error, otherwise returns false.
289
290 =cut
291
292 sub replace {
293   my $new = shift;
294
295   my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
296               ? shift
297               : $new->replace_old;
298
299   return "Can't change domain - reorder."
300     if $old->getfield('domain') ne $new->getfield('domain'); 
301
302   # Better to do it here than to force the caller to remember that svc_domain is weird.
303   $new->setfield(action => 'I');
304   my $error = $new->SUPER::replace($old, @_);
305   return $error if $error;
306 }
307
308 =item suspend
309
310 Just returns false (no error) for now.
311
312 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
313
314 =item unsuspend
315
316 Just returns false (no error) for now.
317
318 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
319
320 =item cancel
321
322 Just returns false (no error) for now.
323
324 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
325
326 =item check
327
328 Checks all fields to make sure this is a valid domain.  If there is an error,
329 returns the error, otherwise returns false.  Called by the insert and replace
330 methods.
331
332 Sets any fixed values; see L<FS::part_svc>.
333
334 =cut
335
336 sub check {
337   my $self = shift;
338
339   my $x = $self->setfixed;
340   return $x unless ref($x);
341   #my $part_svc = $x;
342
343   my $error = $self->ut_numbern('svcnum')
344               || $self->ut_numbern('catchall')
345   ;
346   return $error if $error;
347
348   #hmm
349   my $pkgnum;
350   if ( $self->svcnum ) {
351     my $cust_svc = qsearchs( 'cust_svc', { 'svcnum' => $self->svcnum } );
352     $pkgnum = $cust_svc->pkgnum;
353   } else {
354     $pkgnum = $self->pkgnum;
355   }
356
357   my($recref) = $self->hashref;
358
359   #if ( $recref->{domain} =~ /^([\w\-\.]{1,22})\.(com|net|org|edu)$/ ) {
360   if ( $recref->{domain} =~ /^([\w\-]{1,63})\.(com|net|org|edu|tv|info|biz)$/ ) {
361     $recref->{domain} = "$1.$2";
362     $recref->{suffix} ||= $2;
363   # hmmmmmmmm.
364   } elsif ( $whois_hack && $recref->{domain} =~ /^([\w\-\.]+)\.(\w+)$/ ) {
365     $recref->{domain} = "$1.$2";
366     # need to match a list of suffixes - no guarantee they're top-level..
367     # http://wiki.mozilla.org/TLD_List
368     # but this will have to do for now...
369     $recref->{suffix} ||= $2;
370   } else {
371     return "Illegal domain ". $recref->{domain}.
372            " (or unknown registry - try \$whois_hack)";
373   }
374
375   $self->suffix =~ /(^|\.)(\w+)$/
376     or return "can't parse suffix for TLD: ". $self->suffix;
377   my $tld = $2;
378   return "No such TLD: .$tld" unless tld_exists($tld);
379
380   if ( $recref->{catchall} ne '' ) {
381     my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $recref->{catchall} } );
382     return "Unknown catchall" unless $svc_acct;
383   }
384
385   $self->ut_alphan('suffix')
386     or $self->ut_foreign_keyn('registrarnum', 'registrar', 'registrarnum')
387     or $self->ut_textn('registrarkey')
388     or $self->ut_numbern('setup_date')
389     or $self->ut_numbern('renewal_interval')
390     or $self->ut_numbern('expiration_date')
391     or $self->SUPER::check;
392
393 }
394
395 =item domain_record
396
397 =cut
398
399 sub domain_record {
400   my $self = shift;
401
402   my %order = (
403     'SOA'   => 1,
404     'NS'    => 2,
405     'MX'    => 3,
406     'CNAME' => 4,
407     'A'     => 5,
408     'TXT'   => 6,
409     'PTR'   => 7,
410   );
411
412   my %sort = (
413     #'SOA'   => sub { $_[0]->recdata cmp $_[1]->recdata }, #sure hope not though
414 #    'SOA'   => sub { 0; },
415 #    'NS'    => sub { 0; },
416     'MX'    => sub { my( $a_weight, $a_name ) = split(/\s+/, $_[0]->recdata);
417                      my( $b_weight, $b_name ) = split(/\s+/, $_[1]->recdata);
418                      $a_weight <=> $b_weight or $a_name cmp $b_name;
419                    },
420     'CNAME' => sub { $_[0]->reczone cmp $_[1]->reczone },
421     'A'     => sub { $_[0]->reczone cmp $_[1]->reczone },
422
423 #    'TXT'   => sub { 0; },
424     'PTR'   => sub { $_[0]->reczone <=> $_[1]->reczone },
425   );
426
427   sort {    $order{$a->rectype} <=> $order{$b->rectype}
428          or &{ $sort{$a->rectype} || sub { 0; } }($a, $b)
429        }
430        qsearch('domain_record', { svcnum => $self->svcnum } );
431
432 }
433
434 sub catchall_svc_acct {
435   my $self = shift;
436   if ( $self->catchall ) {
437     qsearchs( 'svc_acct', { 'svcnum' => $self->catchall } );
438   } else {
439     '';
440   }
441 }
442
443 =item whois
444
445 # Returns the Net::Whois::Domain object (see L<Net::Whois>) for this domain, or
446 # undef if the domain is not found in whois.
447
448 (If $FS::svc_domain::whois_hack is true, returns that in all cases instead.)
449
450 =cut
451
452 sub whois {
453   #$whois_hack or new Net::Whois::Domain $_[0]->domain;
454   #$whois_hack or die "whois_hack not set...\n";
455 }
456
457 =back
458
459 =head1 BUGS
460
461 Delete doesn't send a registration template.
462
463 All registries should be supported.
464
465 Should change action to a real field.
466
467 The $recref stuff in sub check should be cleaned up.
468
469 =head1 SEE ALSO
470
471 L<FS::svc_Common>, L<FS::Record>, L<FS::Conf>, L<FS::cust_svc>,
472 L<FS::part_svc>, L<FS::cust_pkg>, L<Net::Whois>, schema.html from the base
473 documentation, config.html from the base documentation.
474
475 =cut
476
477 1;
478
479