DNS, RT#8933
[freeside.git] / FS / FS / svc_domain.pm
index bdaf79b..7d527e5 100644 (file)
@@ -1,16 +1,19 @@
 package FS::svc_domain;
 
 use strict;
-use vars qw( @ISA $whois_hack $conf
+use base qw( FS::svc_Parent_Mixin FS::svc_CGP_Mixin FS::svc_CGPRule_Mixin
+             FS::svc_Common );
+use vars qw( $whois_hack $conf
   @defaultrecords $soadefaultttl $soaemail $soaexpire $soamachine
   $soarefresh $soaretry
 );
 use Carp;
+use Scalar::Util qw( blessed );
 use Date::Format;
 #use Net::Whois::Raw;
+use Net::Domain::TLD qw(tld_exists);
 use FS::Record qw(fields qsearch qsearchs dbh);
 use FS::Conf;
-use FS::svc_Common;
 use FS::cust_svc;
 use FS::svc_acct;
 use FS::cust_pkg;
@@ -18,8 +21,6 @@ use FS::cust_main;
 use FS::domain_record;
 use FS::queue;
 
-@ISA = qw( FS::svc_Common );
-
 #ask FS::UID to run this stuff for us later
 $FS::UID::callback{'FS::domain'} = sub { 
   $conf = new FS::Conf;
@@ -72,6 +73,22 @@ FS::svc_Common.  The following fields are currently supported:
 
 =item catchall - optional svcnum of an svc_acct record, designating an email catchall account.
 
+=item suffix - 
+
+=item parent_svcnum -
+
+=item registrarnum - Registrar (see L<FS::registrar>)
+
+=item registrarkey - Registrar key or password for this domain
+
+=item setup_date - UNIX timestamp
+
+=item renewal_interval - Number of days before expiration date to start renewal
+
+=item expiration_date - UNIX timestamp
+
+=item max_accounts
+
 =back
 
 =head1 METHODS
@@ -84,8 +101,197 @@ Creates a new domain.  To add the domain to the database, see L<"insert">.
 
 =cut
 
+sub table_info {
+  {
+    'name' => 'Domain',
+    'sorts' => 'domain',
+    'display_weight' => 20,
+    'cancel_weight'  => 60,
+    'fields' => {
+      'domain' => 'Domain',
+      'parent_svcnum' => { 
+                         label => 'Parent domain / Communigate administrator domain',
+                         type  => 'select',
+                         select_table => 'svc_domain',
+                         select_key => 'svcnum',
+                         select_label => 'domain',
+                         disable_inventory => 1,
+                         disable_select    => 1,
+                       },
+      'max_accounts' => { label => 'Maximum number of accounts',
+                          'disable_inventory' => 1,
+                        },
+      'cgp_aliases' => { 
+                         label => 'Communigate aliases',
+                         type  => 'text',
+                         disable_inventory => 1,
+                         disable_select    => 1,
+                       },
+      'cgp_accessmodes' => { 
+                             label => 'Communigate enabled services',
+                             type  => 'communigate_pro-accessmodes',
+                             disable_inventory => 1,
+                             disable_select    => 1,
+                           },
+
+      'acct_def_cgp_accessmodes' => { 
+                             label => 'Acct. default Communigate enabled services',
+                             type  => 'communigate_pro-accessmodes',
+                             disable_inventory => 1,
+                             disable_select    => 1,
+                           },
+      'acct_def_password_selfchange' => { label => 'Acct. default Password modification',
+                                 type  => 'checkbox',
+                            disable_inventory => 1,
+                            disable_select    => 1,
+                               },
+      'acct_def_password_recover'    => { label => 'Acct. default Password recovery',
+                                 type  => 'checkbox',
+                            disable_inventory => 1,
+                            disable_select    => 1,
+                               },
+      'acct_def_cgp_deletemode' => { 
+                            label => 'Acct. default Communigate message delete method',
+                            type  => 'select',
+                            select_list => [ 'Move To Trash', 'Immediately', 'Mark' ],
+                            disable_inventory => 1,
+                            disable_select    => 1,
+                          },
+      'acct_def_cgp_emptytrash' => { 
+                            label => 'Acct. default Communigate on logout remove trash',
+                            type        => 'select',
+                            select_list => __PACKAGE__->cgp_emptytrash_values,
+                            disable_inventory => 1,
+                            disable_select    => 1,
+                          },
+      'acct_def_quota'     => { 
+                       label => 'Acct. default Quota', #Mail storage limit
+                       type => 'text',
+                       disable_inventory => 1,
+                       disable_select => 1,
+                     },
+      'acct_def_file_quota'=> { 
+                       label => 'Acct. default File storage limit',
+                       type => 'text',
+                       disable_inventory => 1,
+                       disable_select => 1,
+                     },
+      'acct_def_file_maxnum'=> { 
+                       label => 'Acct. default Number of files limit',
+                       type => 'text',
+                       disable_inventory => 1,
+                       disable_select => 1,
+                     },
+      'acct_def_file_maxsize'=> { 
+                       label => 'Acct. default File size limit',
+                       type => 'text',
+                       disable_inventory => 1,
+                       disable_select => 1,
+                     },
+      'acct_def_cgp_rulesallowed'   => {
+        label       => 'Acct. default Allowed mail rules',
+        type        => 'select',
+        select_list => [ '', 'No', 'Filter Only', 'All But Exec', 'Any' ],
+        disable_inventory => 1,
+        disable_select    => 1,
+      },
+      'acct_def_cgp_rpopallowed'    => {
+        label => 'Acct. default RPOP modifications',
+        type  => 'checkbox',
+      },
+      'acct_def_cgp_mailtoall'      => {
+        label => 'Acct. default Accepts mail to "all"',
+        type  => 'checkbox',
+      },
+      'acct_def_cgp_addmailtrailer' => {
+        label => 'Acct. default Add trailer to sent mail',
+        type  => 'checkbox',
+      },
+      'acct_def_cgp_archiveafter'   => {
+        label       => 'Archive messages after',
+        type        => 'select',
+        select_hash => [ 
+                         -2 => 'default(730 days)',
+                         0 => 'Never',
+                         86400 => '24 hours',
+                         172800 => '2 days',
+                         259200 => '3 days',
+                         432000 => '5 days',
+                         604800 => '7 days',
+                         1209600 => '2 weeks',
+                         2592000 => '30 days',
+                         7776000 => '90 days',
+                         15552000 => '180 days',
+                         31536000 => '365 days',
+                         63072000 => '730 days',
+                       ],
+        disable_inventory => 1,
+        disable_select    => 1,
+      },
+      'trailer' => {
+        label => 'Mail trailer',
+        type  => 'textarea',
+        disable_inventory => 1,
+        disable_select    => 1,
+      },
+      'acct_def_cgp_language' => {
+                            label => 'Acct. default language',
+                            type  => 'select',
+                            select_list => [ '', qw( English Arabic Chinese Dutch French German Hebrew Italian Japanese Portuguese Russian Slovak Spanish Thai ) ],
+                            disable_inventory => 1,
+                            disable_select    => 1,
+                        },
+      'acct_def_cgp_timezone' => {
+                            label       => 'Acct. default time zone',
+                            type        => 'select',
+                            select_list => __PACKAGE__->cgp_timezone_values,
+                            disable_inventory => 1,
+                            disable_select    => 1,
+                        },
+      'acct_def_cgp_skinname' => {
+                            label => 'Acct. default layout',
+                            type  => 'select',
+                            select_list => [ '', '***', 'GoldFleece', 'Skin2' ],
+                            disable_inventory => 1,
+                            disable_select    => 1,
+                        },
+      'acct_def_cgp_prontoskinname' => {
+                            label => 'Acct. default Pronto style',
+                            type  => 'select',
+                            select_list => [ '', 'Pronto', 'Pronto-darkflame', 'Pronto-steel', 'Pronto-twilight', ],
+                            disable_inventory => 1,
+                            disable_select    => 1,
+                        },
+      'acct_def_cgp_sendmdnmode' => {
+        label => 'Acct. default send read receipts',
+        type  => 'select',
+        select_list => [ '', 'Never', 'Manually', 'Automatically' ],
+        disable_inventory => 1,
+        disable_select    => 1,
+      },
+    },
+  };
+}
+
 sub table { 'svc_domain'; }
 
+sub search_sql {
+  my($class, $string) = @_;
+  $class->search_sql_field('domain', $string);
+}
+
+
+=item label
+
+Returns the domain.
+
+=cut
+
+sub label {
+  my $self = shift;
+  $self->domain;
+}
+
 =item insert [ , OPTION => VALUE ... ]
 
 Adds this domain to the database.  If there is an error, returns the error,
@@ -94,8 +300,8 @@ otherwise returns false.
 The additional fields I<pkgnum> and I<svcpart> (see L<FS::cust_svc>) should be 
 defined.  An FS::cust_svc record will be created and inserted.
 
-The additional field I<action> should be set to I<N> for new domains or I<M>
-for transfers.
+The additional field I<action> should be set to I<N> for new domains, I<M>
+for transfers, or I<I> for no action (registered elsewhere).
 
 A registration or transfer email will be submitted unless
 $FS::svc_domain::whois_hack is true.
@@ -135,29 +341,35 @@ sub insert {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
-  $error = $self->check;
-  return $error if $error;
-
-  return "Domain in use (here)"
-    if qsearchs( 'svc_domain', { 'domain' => $self->domain } );
-
-  my $whois = $self->whois;
-  if ( $self->action eq "N" && ! $whois_hack && $whois ) {
-    $dbh->rollback if $oldAutoCommit;
-    return "Domain in use (see whois)";
-  }
-  if ( $self->action eq "M" && ! $whois ) {
-    $dbh->rollback if $oldAutoCommit;
-    return "Domain not found (see whois)";
-  }
-
-  $error = $self->SUPER::insert(@_);
+  $error =  $self->SUPER::insert(@_)
+         || $self->insert_defaultrecords;
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
   }
 
-  $self->submit_internic unless $whois_hack;
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  ''; #no error
+}
+
+=item insert_defaultrecords
+
+=cut
+
+sub insert_defaultrecords {
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
 
   if ( $soamachine ) {
     my $soa = new FS::domain_record {
@@ -168,10 +380,10 @@ sub insert {
       'recdata' => "$soamachine $soaemail ( ". time2str("%Y%m%d", time). "00 ".
                    "$soarefresh $soaretry $soaexpire $soadefaultttl )"
     };
-    $error = $soa->insert;
+    my $error = $soa->insert;
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
-      return "couldn't insert SOA record for new domain: $error";
+      return "couldn't insert SOA record: $error";
     }
 
     foreach my $record ( @defaultrecords ) {
@@ -186,7 +398,7 @@ sub insert {
       my $error = $domain_record->insert;
       if ( $error ) {
         $dbh->rollback if $oldAutoCommit;
-        return "couldn't insert record for new domain: $error";
+        return "couldn't insert record: $error";
       }
     }
 
@@ -238,7 +450,7 @@ sub delete {
     }
   }
 
-  my $error = $self->SUPER::delete;
+  my $error = $self->SUPER::delete(@_);
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
@@ -255,14 +467,19 @@ returns the error, otherwise returns false.
 =cut
 
 sub replace {
-  my ( $new, $old ) = ( shift, shift );
+  my $new = shift;
+
+  my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
+              ? shift
+              : $new->replace_old;
 
   return "Can't change domain - reorder."
-    if $old->getfield('domain') ne $new->getfield('domain'); 
+    if $old->getfield('domain') ne $new->getfield('domain')
+    && ! $conf->exists('svc_domain-edit_domain'); 
 
   # Better to do it here than to force the caller to remember that svc_domain is weird.
-  $new->setfield(action => 'M');
-  my $error = $new->SUPER::replace($old);
+  $new->setfield(action => 'I');
+  my $error = $new->SUPER::replace($old, @_);
   return $error if $error;
 }
 
@@ -303,6 +520,32 @@ sub check {
 
   my $error = $self->ut_numbern('svcnum')
               || $self->ut_numbern('catchall')
+              || $self->ut_numbern('max_accounts')
+              || $self->ut_anything('trailer') #well
+              || $self->ut_textn('cgp_aliases') #well
+              || $self->ut_enum('acct_def_password_selfchange', [ '', 'Y' ])
+              || $self->ut_enum('acct_def_password_recover',    [ '', 'Y' ])
+              || $self->ut_textn('acct_def_cgp_accessmodes')
+              || $self->ut_alphan('acct_def_quota')
+              || $self->ut_alphan('acct_def_file_quota')
+              || $self->ut_alphan('acct_def_maxnum')
+              || $self->ut_alphan('acct_def_maxsize')
+              #settings
+              || $self->ut_alphasn('acct_def_cgp_rulesallowed')
+              || $self->ut_enum('acct_def_cgp_rpopallowed', [ '', 'Y' ])
+              || $self->ut_enum('acct_def_cgp_mailtoall', [ '', 'Y' ])
+              || $self->ut_enum('acct_def_cgp_addmailtrailer', [ '', 'Y' ])
+              || $self->ut_snumbern('acct_def_cgp_archiveafter')
+              #preferences
+              || $self->ut_alphasn('acct_def_cgp_deletemode')
+              || $self->ut_enum('acct_def_cgp_emptytrash',
+                                   $self->cgp_emptytrash_values )
+              || $self->ut_alphan('acct_def_cgp_language')
+              || $self->ut_textn('acct_def_cgp_timezone')
+              || $self->ut_textn('acct_def_cgp_skinname')
+              || $self->ut_textn('acct_def_cgp_prontoskinname')
+              || $self->ut_alphan('acct_def_cgp_sendmdnmode')
+              #mail
   ;
   return $error if $error;
 
@@ -317,49 +560,54 @@ sub check {
 
   my($recref) = $self->hashref;
 
-  unless ( $whois_hack ) {
-    unless ( $self->email ) { #find out an email address
-      my @svc_acct;
-      foreach ( qsearch( 'cust_svc', { 'pkgnum' => $pkgnum } ) ) {
-        my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $_->svcnum } );
-        push @svc_acct, $svc_acct if $svc_acct;
-      }
-
-      if ( scalar(@svc_acct) == 0 ) {
-        return "Must order an account in package ". $pkgnum. " first";
-      } elsif ( scalar(@svc_acct) > 1 ) {
-        return "More than one account in package ". $pkgnum. ": specify admin contact email";
-      } else {
-        $self->email($svc_acct[0]->email );
-      }
-    }
-  }
-
   #if ( $recref->{domain} =~ /^([\w\-\.]{1,22})\.(com|net|org|edu)$/ ) {
-  if ( $recref->{domain} =~ /^([\w\-]{1,63})\.(com|net|org|edu)$/ ) {
+  if ( $recref->{domain} =~ /^([\w\-]{1,63})\.(com|net|org|edu|tv|info|biz)$/ ) {
     $recref->{domain} = "$1.$2";
+    $recref->{suffix} ||= $2;
   # hmmmmmmmm.
-  } elsif ( $whois_hack && $recref->{domain} =~ /^([\w\-\.]+)$/ ) {
-    $recref->{domain} = $1;
+  } elsif ( $whois_hack && $recref->{domain} =~ /^([\w\-\.]+)\.(\w+)$/ ) {
+    $recref->{domain} = "$1.$2";
+    # need to match a list of suffixes - no guarantee they're top-level..
+    # http://wiki.mozilla.org/TLD_List
+    # but this will have to do for now...
+    $recref->{suffix} ||= $2;
   } else {
     return "Illegal domain ". $recref->{domain}.
            " (or unknown registry - try \$whois_hack)";
   }
 
-  $recref->{action} =~ /^(M|N)$/
-    or return "Illegal action: ". $recref->{action};
-  $recref->{action} = $1;
+  $self->suffix =~ /(^|\.)(\w+)$/
+    or return "can't parse suffix for TLD: ". $self->suffix;
+  my $tld = $2;
+  return "No such TLD: .$tld" unless tld_exists($tld);
 
   if ( $recref->{catchall} ne '' ) {
     my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $recref->{catchall} } );
     return "Unknown catchall" unless $svc_acct;
   }
 
-  $self->ut_textn('purpose')
+  $self->ut_alphan('suffix')
+    or $self->ut_foreign_keyn('registrarnum', 'registrar', 'registrarnum')
+    or $self->ut_textn('registrarkey')
+    or $self->ut_numbern('setup_date')
+    or $self->ut_numbern('renewal_interval')
+    or $self->ut_numbern('expiration_date')
     or $self->SUPER::check;
 
 }
 
+sub _check_duplicate {
+  my $self = shift;
+
+  $self->lock_table;
+
+  if ( qsearchs( 'svc_domain', { 'domain' => $self->domain } ) ) {
+    return "Domain in use (here)";
+  } else {
+    return '';
+  }
+}
+
 =item domain_record
 
 =cut
@@ -377,8 +625,26 @@ sub domain_record {
     'PTR'   => 7,
   );
 
-  sort { $order{$a->rectype} <=> $order{$b->rectype} }
-    qsearch('domain_record', { svcnum => $self->svcnum } );
+  my %sort = (
+    #'SOA'   => sub { $_[0]->recdata cmp $_[1]->recdata }, #sure hope not though
+#    'SOA'   => sub { 0; },
+#    'NS'    => sub { 0; },
+    'MX'    => sub { my( $a_weight, $a_name ) = split(/\s+/, $_[0]->recdata);
+                     my( $b_weight, $b_name ) = split(/\s+/, $_[1]->recdata);
+                     $a_weight <=> $b_weight or $a_name cmp $b_name;
+                   },
+    'CNAME' => sub { $_[0]->reczone cmp $_[1]->reczone },
+    'A'     => sub { $_[0]->reczone cmp $_[1]->reczone },
+
+#    'TXT'   => sub { 0; },
+    'PTR'   => sub { $_[0]->reczone <=> $_[1]->reczone },
+  );
+
+  map { $_ } #return $self->num_domain_record( PARAMS ) unless wantarray;
+  sort {    $order{$a->rectype} <=> $order{$b->rectype}
+         or &{ $sort{$a->rectype} || sub { 0; } }($a, $b)
+       }
+       qsearch('domain_record', { svcnum => $self->svcnum } );
 
 }
 
@@ -402,28 +668,7 @@ sub catchall_svc_acct {
 
 sub whois {
   #$whois_hack or new Net::Whois::Domain $_[0]->domain;
-  $whois_hack or die "whois_hack not set...\n";
-}
-
-=item _whois
-
-Depriciated.
-
-=cut
-
-sub _whois {
-  die "_whois depriciated";
-}
-
-=item submit_internic
-
-Submits a registration email for this domain.
-
-=cut
-
-sub submit_internic {
-  #my $self = shift;
-  carp "submit_internic depreciated";
+  #$whois_hack or die "whois_hack not set...\n";
 }
 
 =back