1 package FS::part_export::domreg_net_dri;
3 use vars qw(@ISA %info %options $conf);
5 use FS::part_export::null;
9 FS::part_export::domreg_net_dri - Register or transfer domains with Net::DRI
13 This module handles registering and transferring domains with select registrars or registries supported
16 As a part_export, this module can be designated for use with svc_domain services. When the svc_domain object
17 is inserted into the Freeside database, registration or transferring of the domain may be initiated, depending
18 on the setting of the svc_domain's action field. Further operations can be performed from the View Domain screen.
20 Logging information is written to the Freeside log folder.
22 For correct operation you must add name/value pairs to the protcol and transport options fields. The setttings
23 depend on the domain registry driver (DRD) selected.
27 =item N - Register the domain
29 =item M - Transfer the domain
31 =item I - Ignore the domain for registration purposes
37 @ISA = qw(FS::part_export::null);
39 my @tldlist = qw/com net org biz info name mobi at be ca cc ch cn de dk es eu fr it mx nl tv uk us/;
41 my $opensrs_protocol_opts=<<'END';
49 my $opensrs_transport_opts=<<'END';
54 tie %options, 'Tie::IxHash',
55 'drd' => { label => 'Domain Registry Driver (DRD)',
57 options => [ qw/BookMyName CentralNic Gandi OpenSRS OVH VNDS/ ],
58 default => 'OpenSRS' },
59 'log_level' => { label => 'Logging',
61 options => [ qw/debug info notice warning error critical alert emergency/ ],
62 default => 'warning' },
64 label => 'Protocol Options',
66 default => $opensrs_protocol_opts,
69 label => 'Transport Options',
71 default => $opensrs_transport_opts,
73 # 'register' => { label => 'Use for registration',
76 # 'transfer' => { label => 'Use for transfer',
79 # 'delete' => { label => 'Use for deletion',
82 # 'renew' => { label => 'Use for renewals',
85 'tlds' => { label => 'Use this export for these top-level domains (TLDs)',
88 size => scalar(@tldlist),
89 options => [ @tldlist ],
90 default => 'com net org' },
93 my $opensrs_protocol_defaults = $opensrs_protocol_opts;
94 $opensrs_protocol_defaults =~ s|\n|\\n|g;
96 my $opensrs_transport_defaults = $opensrs_transport_opts;
97 $opensrs_transport_defaults =~ s|\n|\\n|g;
100 'svc' => 'svc_domain',
101 'desc' => 'Domain registration via Net::DRI',
102 'options' => \%options,
104 Registers and transfers domains via a Net::DRI registrar or registry.
105 <a href="http://search.cpan.org/search?dist=Net-DRI">Net::DRI</a>
106 must be installed. You must have an account at the selected registrar/registry.
108 Some top-level domains have additional business rules not supported by this export. These TLDs cannot be registered or transfered with this export.
109 <BR><BR>Use these buttons for some useful presets:
112 <INPUT TYPE="button" VALUE="OpenSRS Live System (rr-n1-tor.opensrs.net)" onClick='
113 document.dummy.machine.value = "rr-n1-tor.opensrs.net";
114 this.form.machine.value = "rr-n1-tor.opensrs.net";
117 <INPUT TYPE="button" VALUE="OpenSRS Test System (horizon.opensrs.net)" onClick='
118 document.dummy.machine.value = "horizon.opensrs.net";
119 this.form.machine.value = "horizon.opensrs.net";
122 <INPUT TYPE="button" VALUE="OpenSRS protocol/transport options" onClick='
123 this.form.protocol_opts.value = "$opensrs_protocol_defaults";
124 this.form.transport_opts.value = "$opensrs_transport_defaults";
130 install_callback FS::UID sub {
131 $conf = new FS::Conf;
134 #sub rebless { shift; }
136 # experiment: want the status of these right away, so no queueing
139 my( $self, $svc_domain ) = ( shift, shift );
141 return if $svc_domain->action eq 'I'; # Ignoring registration, just doing DNS
143 if ($svc_domain->action eq 'N') {
144 return $self->register( $svc_domain );
145 } elsif ($svc_domain->action eq 'M') {
146 return $self->transfer( $svc_domain );
148 return "Unknown domain action " . $svc_domain->action;
151 =item get_portfolio_credentials
153 Returns, in list context, the user name and password for the domain portfolio.
155 This is currently specified via the username and password keys in the protocol options.
159 sub get_portfolio_credentials {
162 my %opts = $self->get_protocol_options();
163 return ($opts{username}, $opts{password});
168 Reformats a phone number according to registry rules. Currently Freeside stores phone numbers
169 in NANPA format and most registries prefer "+CCC.NPANPXNNNN"
176 #if ($tel =~ /^(\d{3})-(\d{3})-(\d{4})\s*(x\s*(\d+))?$/) {
177 if ($tel =~ /^(\d{3})-(\d{3})-(\d{4})$/) {
178 $tel = "+1.$1$2$3"; # TBD: other country codes
179 # if $tel .= "$4" if $4;
184 sub gen_contact_set {
185 my ($self, $dri, $cust_main) = @_;
187 my @invoicing_list = $cust_main->invoicing_list_emailonly;
188 if ( $conf->exists('emailinvoiceautoalways')
189 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
190 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
191 push @invoicing_list, $cust_main->all_emails;
194 my $email = ($conf->exists('business-onlinepayment-email-override'))
195 ? $conf->config('business-onlinepayment-email-override')
196 : $invoicing_list[0];
198 my $cs=$dri->local_object('contactset');
199 my $co=$dri->local_object('contact');
201 my ($user, $pass) = $self->get_portfolio_credentials();
203 $co->srid($user); # Portfolio user name for OpenSRS?
204 $co->auth($pass); # Portfolio password for OpenSRS?
206 $co->firstname($cust_main->first);
207 $co->name($cust_main->last);
208 $co->org($cust_main->company || '-');
209 $co->street([$cust_main->address1, $cust_main->address2]);
210 $co->city($cust_main->city);
211 $co->sp($cust_main->state);
212 $co->pc($cust_main->zip);
213 $co->cc($cust_main->country);
214 $co->voice(format_tel($cust_main->daytime()));
217 $cs->set($co, 'registrant');
218 $cs->set($co, 'admin');
219 $cs->set($co, 'billing');
224 =item validate_contact_set
226 Attempts to validate contact data for the domain based on OpenSRS rules.
228 Returns undef if the contact data is acceptable, an error message if the contact
229 data lacks one or more required fields.
233 sub validate_contact_set {
237 firstname => "first name",
239 street => "street address",
242 pc => "ZIP/postal code",
244 email => "email address",
245 voice => "phone number",
248 foreach my $which (qw/registrant admin billing/) {
249 my $co = $c->get($which);
250 foreach (keys %fields) {
252 push @err, $fields{$_};
256 if (scalar(@err) > 0) {
257 return "Contact information needs: " . join(', ', @err);
262 #sub _export_replace {
263 # my( $self, $new, $old ) = (shift, shift, shift);
269 ## Domain registration exports do nothing on delete. You're just removing the domain from Freeside, not the registry
270 #sub _export_delete {
271 # my( $self, $www ) = ( shift, shift );
276 =item split_textarea_options
278 Split textarea contents into lines, split lines on =, and then trim the results;
282 sub split_textarea_options {
283 my ($self, $optname) = @_;
285 my ($key, $value) = split /=/, $_;
290 $key => $value } split /\n/, $self->option($optname);
294 =item get_protocol_options
296 Return a hash of protocol options
300 sub get_protocol_options {
302 my %opts = $self->split_textarea_options('protocol_opts');
303 if ($self->machine =~ /opensrs\.net/) {
304 my %topts = $self->get_transport_options;
305 $opts{reseller_id} = $topts{client_login};
310 =item get_transport_options
312 Return a hash of transport options
316 sub get_transport_options {
318 my %opts = $self->split_textarea_options('transport_opts');
319 $opts{remote_url} = "https://" . $self->machine . ":55443/resellers" if $self->machine =~ /opensrs\.net/;
323 =item is_supported_domain
325 Return undef if the domain name uses a TLD or SLD that is supported by this registrar.
326 Otherwise return an error message explaining what's wrong.
330 sub is_supported_domain {
332 my $svc_domain = shift;
334 # Get the TLD of the new domain
335 my @bits = split /\./, $svc_domain->domain;
337 return "Can't register subdomains: " . $svc_domain->domain if scalar(@bits) != 2;
341 # See if it's one this export supports
342 my @tlds = split /\s+/, $self->option('tlds');
343 @tlds = map { s/\.//; $_ } @tlds;
344 return "Can't register top-level domain $tld, restricted to: " . $self->option('tlds') if ! grep { $_ eq $tld } @tlds;
356 # return $self->{dri} if $self->{dri}; #!!!TBD!!! connection caching.
358 eval "use Net::DRI 0.95;";
361 # $dri=Net::DRI->new(...) to create the global object. Save the result,
364 #$dri = Net::DRI::TrapExceptions->new(10);
365 $dri = Net::DRI->new({logging => [ 'files', { output_directory => '%%%FREESIDE_LOG%%%' } ]}); #!!!TBD!!!
366 $dri->logging->level( $self->option('log_level') );
367 $dri->add_registry( $self->option('drd') );
369 $protocol = 'xcp' if $self->option('drd') eq 'OpenSRS';
371 $dri->target( $self->option('drd') )->add_current_profile($self->option('drd') . '1',
372 # 'Net::DRI::Protocol::' . $self->option('protocol_type'),
373 # $self->option('protocol_type'),
375 $protocol, # Implies transport
376 # 'Net::DRI::Transport::' . $self->option('transport_type'),
377 { $self->get_transport_options() },
378 # [ $self->get_protocol_options() ]
389 Returns a reference to a hashref containing information on the domain's status. The keys
390 defined depend on the status.
392 'unregistered' means the domain is not registered.
394 Otherwise, if the domain is in an asynchronous operation such as a transfer, returns the state
397 Otherwise returns a value indicating if the domain can be managed through our reseller account.
402 my ( $self, $svc_domain ) = @_;
406 my $dri = $self->get_dri;
408 if (UNIVERSAL::isa($dri, 'Net::DRI::Exception')) {
409 $rslt->{'message'} = $dri->as_string;
413 $rc = $dri->domain_check( $svc_domain->domain );
414 if (!$rc->is_success()) {
415 # Problem accessing the registry/registrar
416 $rslt->{'message'} = $rc->message;
417 } elsif (!$dri->get_info('exist')) {
418 # Domain is not registered
419 $rslt->{'unregistered'} = 1;
421 $rc = $dri->domain_transfer_query( $svc_domain->domain );
422 if ($rc->is_success() && $dri->get_info('status')) {
423 # Transfer in progress
424 $rslt->{status} = $dri->get_info('status');
425 $rslt->{contact_email} = $dri->get_info('request_address');
426 $rslt->{last_update_time} = $dri->get_info('unixtime');
427 } elsif ($dri->get_info('reason')) {
428 $rslt->{'reason'} = $dri->get_info('reason');
429 # Domain is not being transferred...
430 $rc = $dri->domain_info( $svc_domain->domain, { $self->get_protocol_options() } );
431 if ($rc->is_success() && $dri->get_info('exDate')) {
432 $rslt->{'expdate'} = $dri->get_info('exDate');
435 $rslt->{status} = 'Unknown';
439 # rslt->{'message'} = $@->as_string if $@;
441 $rslt->{'message'} = (UNIVERSAL::isa($@, 'Net::DRI::Exception')) ? $@->as_string : $@->message;
444 return $rslt; # Success
449 Attempts to register the domain through the reseller account associated with this export.
451 Like most export functions, returns an error message on failure or undef on success.
456 my ( $self, $svc_domain, $years ) = @_;
458 my $err = $self->is_supported_domain( $svc_domain );
461 my $dri = $self->get_dri;
462 return $dri->as_string if (UNIVERSAL::isa($dri, 'Net::DRI::Exception'));
464 eval { # All $dri methods can throw an exception.
467 my $cust_main = $svc_domain->cust_svc->cust_pkg->cust_main;
469 my $cs = $self->gen_contact_set($dri, $cust_main);
471 $err = validate_contact_set($cs);
474 # !!!TBD!!! add custom name servers when supported; add ns => $ns to hash passed to domain_create
476 $res = $dri->domain_create($svc_domain->domain, { $self->get_protocol_options(), pure_create => 1, contact => $cs, duration => DateTime::Duration->new(years => $years) });
477 $err = $res->is_success ? '' : $res->message;
480 $err = (UNIVERSAL::isa($@, 'Net::DRI::Exception')) ? $@->msg : $@->message;
488 Attempts to transfer the domain into the reseller account associated with this export.
490 Like most export functions, returns an error message on failure or undef on success.
495 my ( $self, $svc_domain ) = @_;
497 my $err = $self->is_supported_domain( $svc_domain );
500 # $dri=Net::DRI->new(...) to create the global object. Save the result,
501 my $dri = $self->get_dri;
502 return $dri->as_string if (UNIVERSAL::isa($dri, 'Net::DRI::Exception'));
504 eval { # All $dri methods can throw an exception
507 my $cust_main = $svc_domain->cust_svc->cust_pkg->cust_main;
509 my $cs = $self->gen_contact_set($dri, $cust_main);
511 $err = validate_contact_set($cs);
514 # !!!TBD!!! add custom name servers when supported; add ns => $ns to hash passed to domain_transfer_start
516 $res = $dri->domain_transfer_start($svc_domain->domain, { $self->get_protocol_options(), contact => $cs });
517 $err = $res->is_success ? '' : $res->message;
520 $err = (UNIVERSAL::isa($@, 'Net::DRI::Exception')) ? $@->msg : $@->message;
528 Attempts to renew the domain for the specified number of years.
530 Like most export functions, returns an error message on failure or undef on success.
535 my ( $self, $svc_domain, $years ) = @_;
537 my $err = $self->is_supported_domain( $svc_domain );
540 my $dri = $self->get_dri;
541 return $dri->as_string if (UNIVERSAL::isa($dri, 'Net::DRI::Exception'));
543 eval { # All $dri methods can throw an exception
545 my $res = $dri->domain_info( $svc_domain->domain, { $self->get_protocol_options() } );
546 if ($res->is_success() && $dri->get_info('exDate')) {
547 $expdate = $dri->get_info('exDate');
549 # return "Domain renewal not enabled" if !$self->option('renew');
550 $res = $dri->domain_renew( $svc_domain->domain, { $self->get_protocol_options(), duration => DateTime::Duration->new(years => $years), current_expiration => $expdate });
552 $err = $res->is_success ? '' : $res->message;
555 $err = (UNIVERSAL::isa($@, 'Net::DRI::Exception')) ? $@->msg : $@->message;
563 Attempts to revoke the domain registration. Only succeeds if invoked during the DRI
564 grace period immediately after registration.
566 Like most export functions, returns an error message on failure or undef on success.
571 my ( $self, $svc_domain ) = @_;
573 my $err = $self->is_supported_domain( $svc_domain );
576 my $dri = $self->get_dri;
577 return $dri->as_string if (UNIVERSAL::isa($dri, 'Net::DRI::Exception'));
579 eval { # All $dri methods can throw an exception
581 # return "Domain registration revocation not enabled" if !$self->option('revoke');
582 my $res = $dri->domain_delete( $svc_domain->domain, { $self->get_protocol_options(), domain => $svc_domain->domain, pure_delete => 1 });
583 $err = $res->is_success ? '' : $res->message;
586 $err = (UNIVERSAL::isa($@, 'Net::DRI::Exception')) ? $@->msg : $@->message;
594 Should return a full-blown object representing the Net::DRI DRD, but current just returns a hashref
595 containing the registrar name.
602 name => $self->option('drd'),
608 L<FS::part_export_option>, L<FS::export_svc>, L<FS::svc_domain>,
609 L<FS::Record>, schema.html from the base documentation.