File Coverage

blib/lib/Net/EPP/Registry/Nominet.pm
Criterion Covered Total %
statement 60 757 7.9
branch 17 300 5.6
condition 4 95 4.2
subroutine 12 53 22.6
pod 5 30 16.6
total 98 1235 7.9


line stmt bran cond sub pod time code
1             #
2             # This program is free software; you can redistribute it and/or modify
3             # it under the terms of the GNU General Public License as published by
4             # the Free Software Foundation; either version 2 of the License, or
5             # (at your option) any later version.
6             #
7             # This program is distributed in the hope that it will be useful,
8             # but WITHOUT ANY WARRANTY; without even the implied warranty of
9             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10             # GNU General Public License for more details.
11             #
12             # You should have received a copy of the GNU General Public License
13             # along with this program; if not, write to the Free Software
14             # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
15             #
16             ################################################################################
17             package Net::EPP::Registry::Nominet;
18              
19 11     11   1591427 use strict;
  11         28  
  11         492  
20 11     11   70 use warnings;
  11         97  
  11         853  
21              
22             # use other modules
23 11     11   7041 use Net::EPP::Frame;
  11         1540882  
  11         523  
24 11     11   112 use Carp;
  11         35  
  11         952  
25              
26 11     11   85 use base qw(Net::EPP::Simple);
  11         31  
  11         9215  
27 11     11   1586534 use constant EPP_XMLNS => 'urn:ietf:params:xml:ns:epp-1.0';
  11         36  
  11         1017  
28 11     11   81 use vars qw($Error $Code $Message);
  11         27  
  11         935  
29              
30             BEGIN {
31 11     11   123126 our $VERSION = '0.12';
32             }
33              
34             # file-scoped lexicals
35             my %Host = (
36             prod => 'epp.nominet.org.uk',
37             test => 'testbed-epp.nominet.org.uk',
38             ote => 'ote-epp.nominet.org.uk',
39             );
40             my $EPPVer = '1.0';
41             my $EPPLang = 'en';
42             my $NSVer = '2.0';
43             my $Debug = 0;
44              
45             =pod
46              
47             =head1 Name
48              
49             Net::EPP::Registry::Nominet - a simple client interface to the Nominet EPP
50             service
51              
52             =head1 Synopsis
53              
54             use strict;
55             use warnings;
56             use Net::EPP::Registry::Nominet;
57              
58             my $epp = Net::EPP::Registry::Nominet->new (
59             user => 'MYTAG',
60             pass => 'mypass'
61             ) or die ('Could not login to EPP server: ', $Net::EPP::Registry::Nominet::Message);
62              
63             my $dom = 'foo.co.uk';
64              
65             if ($epp->check_domain($dom) == 1) {
66             print "Domain $dom is available\n" ;
67             } else {
68             my $info = $epp->domain_info($dom);
69             my $res = $epp->renew_domain ({
70             name => $dom,
71             cur_exp_date => $info->{exDate},
72             period => 5
73             });
74             if ($res) {
75             print "$dom renewed; new expiry date is $res\n";
76             } else {
77             warn "Unable to renew $dom: " . $epp->get_reason;
78             }
79             }
80              
81             =head1 Description
82              
83             L is the organisation in charge of
84             domain names under the .uk TLD. Historically it used cryptographically
85             signed email communications with registrars to provision domains.
86             More recently (since 2010) it has instituted an EPP system
87             which is sufficiently different from standard EPP that none of the
88             standard modules will work seamlessly with it.
89              
90             This module exists to provide a client interface to the Nominet EPP
91             servers. It is a subclass of L and aims to adhere
92             closely to that interface style so as to act as a drop-in replacement.
93              
94             =cut
95              
96       11     END {}
97              
98             # subs and methods
99              
100             =pod
101              
102             =head1 Constructor
103              
104             my $epp = Net::EPP::Registry::Nominet->new (
105             user => 'MYTAG',
106             pass => 'mypass'
107             ) or die ('Could not login to EPP server: ', $Net::EPP::Registry::Nominet::Message);
108              
109             The constructor for C has the same
110             general form as the one for L, but with the following
111             exceptions:
112              
113             =over
114              
115             =item * C will be set to the appropriate endpoint. Specify C
116             with a true value to connect to the OT&E endpoint, C with a true
117             value for the testbed endpoint and none of these for the standard live
118             endpoint.
119              
120             =item * C defaults to 5 (seconds).
121              
122             =item * C specifies the verbosity. 0 = almost silent, 1 = displays
123             warnings/errors, 2 = displays EPP frames in over-ridden methods. Default
124             is 0.
125              
126             =item * C changes the default number of years for
127             registrations and renewals from the system default of 2. This is only
128             used if no explicit number of years is given in each registration or
129             renewal command. It must be an integer between 1 and 10 inclusive (but
130             note that renewing for 10 years pre-expiry will always fail because
131             Nominet prohibits it).
132              
133             =item * C is a hashref of options to be passed directly
134             through as the third optional argument to L.
135              
136             =item * There is no facility for a config file but this may be added in
137             future versions.
138              
139             =item * There is no facility for supplying SSL client certificates
140             because there is no support for them in the Nominet EPP server.
141              
142             =back
143              
144             =cut
145              
146             sub new {
147 1     1 1 227903 my ($class, %params) = @_;
148              
149             # Set the (deprecated) flag for XML responses. Should be the
150             # grandparent default anyway these days, but useful if someone tries
151             # to use with old version of Net::EPP.
152 1         3 $params{dom} = 1;
153              
154             carp 'Nominet only supports port 700 these days - using that instead.'
155 1 50 33     5 if $params{port} && $params{port} != 700;
156             carp 'The testssl parameter is deprecated. ' .
157             'From August 2025 all connections use TLS.'
158 1 50       4 if exists $params{testssl};
159 1         2 $params{port} = 700; # Only option from August 2025
160 1         2 $params{ssl} = 1; # Only option from August 2025
161              
162 1 50       22 if (defined $params{debug}) { $Debug = $params{debug}; }
  1         2  
163             $params{host} =
164             $params{test} ? $Host{test} :
165             $params{ote} ? $Host{ote} :
166 1 50       6 $Host{prod};
    50          
167              
168 1 50       2 warn "Connecting to $params{host}:$params{port}\n" if $Debug;
169 1 50 50     6 $params{timeout} = (int($params{timeout} || 0) > 0 ? $params{timeout} : 5);
170 1 50       4 if ($params{verify}) {
171 0   0     0 $params{SSL_ca_file} ||= $params{ca_file};
172 0   0     0 $params{SSL_ca_path} ||= $params{ca_path};
173 0         0 $params{SSL_verify_mode} = 0x01;
174             } else {
175 1         2 $params{SSL_verify_mode} = 0x00;
176             }
177 1 50 33     5 if ($params{ssl} and $params{ciphers}) {
178 0         0 $params{SSL_cipher_list} = $params{ciphers};
179             }
180              
181 1         14 my $self = Net::EPP::Client->new(%params);
182 1 50       23 unless ($self->{timeout}) { $self->{timeout} = $params{timeout}; }
  1         2  
183 1 50       4 $self->{debug} = $Debug > 1 ? 1 : 0; # for parent
184              
185             # Set the default years.
186 1         2 $self->{def_years} = 2;
187 1 50       3 if (defined $params{def_years}) {
188 0         0 my $years = scalar $params{def_years};
189 0 0       0 if ($years =~ /^[0-9]+$/) {
190 0 0 0     0 if ($years > 0 and $years < 11) {
191 0         0 $self->{def_years} = $years;
192             } else {
193 0         0 carp "Supplied parameter def_years is not between 0 and 11";
194             }
195             } else {
196 0         0 carp "Supplied parameter def_years is not an integer";
197             }
198             }
199 1         3 $self->{authenticated} = 0;
200 1   50     5 $self->{reconnect} ||= 3; # Upwards compatibility
201              
202 1         2 bless($self, $class);
203              
204             # Connect to server
205 1 50       4 $self->_go_connect (%params) or return;
206              
207             # Login
208 0 0 0     0 unless (defined $params{login} and $params{login} == 0) {
209 0         0 $self->login ($params{user}, $params{pass}, $params{login_opt});
210             }
211              
212             # If there was an error in the constructor, there's no point
213             # continuing - return undef just like Net::EPP::Simple
214 0 0       0 return $Error ? undef : $self;
215             }
216              
217             sub _go_connect {
218 1     1   9 my ($self, %params) = @_;
219 1 50       3 if (scalar keys %params) {
220 1         9 $self->{connect_params} = \%params;
221             } else {
222 0         0 %params = %{$self->{connect_params}};
  0         0  
223             }
224              
225             # Connect to server
226 1         20 eval { $self->{greeting} = $self->connect (%params); };
  1         9  
227 1 50       133815 unless ($self->{greeting}) {
228 1         4 $self->{connected} = undef;
229 1         47 warn 'No greeting returned: cannot continue';
230 1 50       7 warn ($@) if $@;
231 1         15 return undef;
232             }
233 0         0 $self->{connected} = 1;
234             }
235              
236             =pod
237              
238             =head1 Login
239              
240             The client can perform a standalone EPP Login if required.
241              
242             $epp->login ($username, $password, $opt_ref)
243             or die ("Could not login: ", $epp->get_reason);
244              
245             The optional third argument, C<$opt_ref>, is a hash ref of login
246             options. Currently the only supported option is 'tag_list' which
247             should be set to a true value if the user needs to use the
248             L
249             method. Nominet operates a number of
250             L
251             so that the user needs to login again to perform different tasks. At present
252             this module only supports two sets: the standard tasks and the tag list.
253              
254             =cut
255              
256             sub login {
257 0     0 1 0 my ($self, $user, $pass, $options) = @_;
258              
259 0 0       0 if ($self->{authenticated}) {
260 0         0 $Error = 'Already logged in';
261 0         0 carp ($Error);
262 0         0 return;
263             }
264              
265 0 0       0 unless (defined $user) {
266 0         0 $Error = 'No username (tagname) supplied';
267 0         0 carp ($Error);
268 0         0 return;
269             }
270              
271 0 0       0 unless (defined $pass) {
272 0         0 $Error = 'No password supplied';
273 0         0 carp ($Error);
274 0         0 return;
275             }
276              
277 0 0       0 $self->_go_connect () unless $self->connected;
278              
279             # Set login frame
280 0         0 my $login = Net::EPP::Frame::Command::Login->new;
281              
282 0         0 $login->clID->appendText($user);
283 0         0 $login->pw->appendText($pass);
284 0         0 $login->version->appendText($EPPVer);
285 0         0 $login->lang->appendText($EPPLang);
286              
287 0         0 my @ns = ();
288 0         0 my @svcs = ();
289 0         0 my $baseuri = 'http://www.nominet.org.uk/epp/xml';
290              
291 0 0       0 if ($options->{'tag_list'}) {
292 0         0 push @ns, 'nom-tag';
293             } else {
294 0         0 push @ns, qw/epp eppcom domain host contact secDNS/;
295 0         0 push @svcs, qw/domain-nom-ext-1.2 contact-nom-ext-1.0
296             std-notifications-1.2 std-warning-1.1 std-contact-id-1.0
297             std-release-1.0 std-handshake-1.0 nom-abuse-feed-1.0
298             std-fork-1.0 std-list-1.0 std-locks-1.0 std-unrenew-1.0/;
299             }
300             # Standard schemas and extensions
301 0         0 for my $ns (@ns) {
302 0         0 my $el = $login->createElement('objURI');
303 0         0 my $ver = $EPPVer;
304 0 0       0 $ver = 1.1 if $ns eq 'secDNS';
305 0         0 my $text = "urn:ietf:params:xml:ns:$ns-$ver";
306 0 0       0 $text = "$baseuri/$ns-$ver" if $ns =~ /-/;
307 0         0 $el->appendText($text);
308 0         0 $login->svcs->appendChild($el);
309             }
310             # Extensions go here
311 0 0       0 if (scalar @svcs) {
312 0         0 my $ext = $login->createElement('svcExtension');
313 0         0 for my $ns (@svcs) {
314 0         0 my $el = $login->createElement('extURI');
315 0         0 $el->appendText("$baseuri/$ns");
316 0         0 $ext->appendChild($el);
317             }
318 0         0 $login->svcs->appendChild($ext);
319             }
320              
321 0         0 my $response = $self->_send_frame ($login);
322              
323 0 0       0 if ($Code != 1000) {
324 0         0 $Error = "Error logging in (response code $Code)";
325 0         0 return undef;
326             }
327              
328 0         0 $self->{authenticated} = 1;
329 0         0 $self->{login_params} = [$user, $pass, $options];
330 0         0 return $self;
331             }
332              
333             sub logout {
334 1     1 0 163 my $self = shift;
335 1         12 my $res = $self->SUPER::logout (@_);
336 1 50       4830 $self->{authenticated} = 0 if $res;
337 1         11 return $res;
338             }
339              
340             =head1 Availability checks
341              
342             The availability checks work similarly to L except
343             that in list context they return an array with up to three elements.
344             The first element is the
345             availability indicator as before (0 if provisioned, 1 if available,
346             undef on error). The second element is the abuse counter which used to
347             show how many more such checks you may run but is now always zero
348             because that automated restriction has been removed.
349             The third element gives the reason for the lack of availability, if any.
350              
351             These two extra fields are only relevant for check_domain and will always
352             be undef for the other check methods.
353              
354             # List context
355             my ($avail, $left, $reason) = $epp->check_domain ("foo.uk");
356             ($avail) = $epp->check_contact ("ABC123");
357             ($avail) = $epp->check_host ("ns0.foo.co.uk");
358              
359             # Scalar context
360             $avail = $epp->check_domain ("foo.uk");
361             $avail = $epp->check_contact ("ABC123");
362             $avail = $epp->check_host ("ns0.foo.co.uk");
363              
364             =cut
365              
366             sub _check {
367 0     0     my ($self, $type, $identifier) = @_;
368              
369             # If there's nothing to check, don't bother asking the server
370 0 0         unless (defined $identifier) {
371 0           $Error = "Missing identifier as argument";
372 0           carp $Error;
373 0           return undef;
374             }
375              
376 0           my $frame;
377 0           my @spec = $self->spec ($type);
378 0 0         my $key = $type eq 'contact' ? 'id' : 'name';
379 0 0 0       if ($type eq 'domain' or $type eq 'contact' or $type eq 'host') {
      0        
380 0           $frame = Net::EPP::Frame::Command::Check->new;
381 0           my $obj = $frame->addObject (@spec);
382 0           my $name = $frame->createElement ("$type:$key");
383 0           $name->appendText ($identifier);
384 0           $obj->appendChild ($name);
385 0           $frame->getCommandNode->appendChild ($obj);
386             } else {
387 0           $Error = "Unknown object type '$type'";
388 0 0         warn $Error if $Debug;
389 0           return undef;
390             }
391              
392 0 0         my $response = $self->_send_frame ($frame) or return undef;
393 0           my $avail = $response->getNode($spec[1], $key)->getAttribute('avail');
394 0 0         return $avail unless wantarray;
395              
396 0           my $extra = $response->getNode("$type-nom-ext:chkData");
397 0           my $count = undef;
398 0 0         $count = $extra->getAttribute('abuse-limit') if defined $extra;
399 0 0 0       warn "Remaining checks = $count\n" if ($Debug and defined $count);
400              
401 0           $extra = $response->getNode($spec[1], 'reason');
402 0           my $reason;
403 0 0         $reason = $extra->textContent if defined $extra;
404              
405 0           return ($avail, $count, $reason);
406             }
407              
408             =head1 Domain Renewal
409              
410             You can renew an existing domain with the renew() command.
411              
412             my $new_expiry = $epp->renew ({
413             name => $domstr,
414             cur_exp_date => $old_expiry,
415             period => $years
416             });
417              
418             On success, C<$new_expiry> contains the new expiry date in long form.
419             Otherwise returns undef.
420              
421             C<$domstr> is just the domain as a string, eg. "foo.co.uk".
422              
423             If you do not specify the old expiry date in your request, the system
424             will attempt to retrieve it from the registry first. It should be in the
425             form YYYY-MM-DD.
426              
427             C<$years> must be an integer between 1 and 10 inclusive and defaults to any
428             value specified in the constructor or 2 otherwise. 10 year renewals must
429             be post-expiry.
430              
431             =cut
432              
433             sub renew {
434 0     0 0   my ($self, $renew) = @_;
435 0 0         unless (defined $renew) {
436 0           carp "No argument provided";
437 0           return undef;
438             }
439 0 0 0       unless (ref $renew and ref $renew eq 'HASH') {
440 0           carp "Argument to renew is not a hash reference";
441 0           return undef;
442             }
443 0 0         unless ($renew->{name}) {
444 0           carp "Argument to renew has no 'name' field";
445 0           return undef;
446             }
447 0           my $domain = $renew->{name};
448 0           my $expiry = $renew->{cur_exp_date};
449 0           my $years = $renew->{period};
450 0           my @spec = $self->spec ('domain');
451 0           my $frame = Net::EPP::Frame::Command::Renew->new;
452 0           my $obj = $frame->addObject (@spec);
453 0           my $name = $frame->createElement ('domain:name');
454 0           $name->appendText ($domain);
455 0           $obj->appendChild ($name);
456              
457 0 0 0       unless (defined $expiry and $expiry =~ /^2\d\d\d-\d\d-\d\d$/) {
458 0 0 0       warn "Badly defined expiry (" . ($expiry || '') . ") - retrieving from registry" if $Debug;
459 0           my $dominfo = $self->domain_info ($domain);
460 0 0 0       unless ($dominfo->{exDate} and
461             $dominfo->{exDate} =~ /^2\d\d\d-\d\d-\d\d/) {
462 0           $Error = "Unable to get expiry date from registry for $domain";
463 0           warn $Error;
464 0           return undef;
465             }
466 0           $expiry = substr($dominfo->{exDate}, 0, 10);
467             }
468 0           $name = $frame->createElement ('domain:curExpDate');
469 0           $name->appendText ($expiry);
470 0           $obj->appendChild ($name);
471              
472 0   0       $years ||= $self->{def_years};
473 0           $name = $frame->createElement ('domain:period');
474 0           $name->appendText ($years);
475 0           $name->setAttribute ('unit', 'y');
476 0           $obj->appendChild ($name);
477              
478 0           $frame->getCommandNode->appendChild ($obj);
479              
480 0 0         if (my $response = $self->_send_frame ($frame)) {
481 0           my $date = $response->getNode ($spec[1], 'exDate')->firstChild->toString ();
482 0 0         warn "New expiry date = $date\n" if $Debug;
483 0           return $date;
484             }
485 0           return undef;
486             }
487              
488             =head1 Domain Unrenewal
489              
490             You can unrenew a list of recently renewed domains with the unrenew() command.
491              
492             my $new_expiry = $epp->unrenew ($domstr, $domstr2, ... )
493              
494             On success, C<$new_expiry> is a hashref with the domain names as keys and
495             the new expiry dates in long form as the values.
496             Otherwise returns an empty hashref or undef on complete failure.
497              
498             C<$domstr>, C<$domstr2> are just the domains as a string, eg. "foo.co.uk".
499              
500             =cut
501              
502             sub unrenew {
503 0     0 0   my ($self, @doms) = @_;
504              
505 0           my $type = 'u';
506 0           my @spec = $self->spec ($type);
507 0           my $frame = Net::EPP::Frame::Command::Update->new;
508              
509 0           my $elem = $frame->createElement ('u:unrenew');
510 0           $elem->setAttribute ("xmlns:$type", $spec[1]);
511              
512 0           for my $domain (@doms) {
513 0           my $name = $frame->createElement ('u:domainName');
514 0           $name->appendText ($domain);
515 0           $elem->appendChild ($name);
516             }
517 0           $frame->getCommandNode->appendChild ($elem);
518              
519 0 0         if (my $response = $self->_send_frame ($frame)) {
520              
521             # Results not necessarily returned by EPP in the same order.
522             # Construct a hash ref with domains as keys and expiry dates as
523             # values
524 0           my $dates = {};
525 0           for my $node ($response->getElementsByLocalName ('renData')) {
526 0           my $dom = $node->getChildrenByLocalName('name')->[0]->firstChild->toString;
527 0           my $exp = $node->getChildrenByLocalName('exDate')->[0]->firstChild->toString;
528 0           $dates->{$dom} = $exp;
529             }
530 0           return $dates;
531             }
532 0           return undef;
533             }
534              
535             =head1 Release domains
536              
537             To transfer a domain to another registrar, use the release_domain
538             method. Returns 1 on success (including success pending handshake), 0 on
539             failure
540              
541             my $res = $epp->release_domain ('foo.co.uk', 'OTHER_TAG');
542             if ($res) {
543             if ($epp->get_code == 1001) {
544             warn "Handshake pending\n";
545             }
546             } else {
547             warn "Could not release $dom: ", $epp->get_reason;
548             }
549              
550             =cut
551              
552             # This does not fit well with Standard EPP, so we need to create our own
553             # command frame from scratch
554             sub release_domain {
555 0     0 0   my ($self, $domain, $tag) = @_;
556 0           my $frame;
557 0           my $type = 'r';
558 0           my @spec = $self->spec ($type);
559 0           $frame = Net::EPP::Frame::Command::Update->new;
560              
561 0           my $elem = $frame->createElement ('r:release');
562 0           $elem->setAttribute ("xmlns:$type", $spec[1]);
563              
564 0           my $name = $frame->createElement ('r:domainName');
565 0           $name->appendText ($domain);
566 0           $elem->appendChild ($name);
567              
568 0           $name = $frame->createElement ('r:registrarTag');
569 0           $name->appendText ($tag);
570 0           $elem->appendChild ($name);
571 0           $frame->getCommandNode->appendChild ($elem);
572              
573 0           my $response = $self->_send_frame ($frame);
574 0 0 0       if ($Code > 999 and $Code < 1002) { return 1; }
  0            
575 0           return 0;
576             }
577              
578             =head1 Create objects
579              
580             Standard EPP allows the creation of domains, contacts and hosts
581             (nameservers). The same is true of Nominet's version, with several
582             differences.
583              
584             =head2 Register domains
585              
586             To register a domain, there must already be a registrant in the system.
587             You will need to create a hashref of the domain like this to perform the
588             registration.
589              
590              
591             my $domain = {
592             name => "foo.co.uk",
593             period => "5",
594             registrant => "ABC123",
595             nameservers => {
596             'nsname0' => "ns1.bar.co.uk",
597             'nsname1' => "ns2.bar.co.uk"
598             },
599             secDNS => [
600             {
601             keyTag => 123,
602             alg => 5,
603             digestType => 1,
604             digest => '8A9CEBB665B78E0142F1CEF47CC9F4205F600685'
605             }
606             ]
607             };
608             my ($res) = $epp->create_domain ($domain);
609             if ($res) {
610             print "Expiry date of new domain: $res->{expiry}\n";
611             } else {
612             warn "Domain not registered: ", $epp->get_reason, "\n";
613             }
614              
615             It returns undef on failure, 1 on success in scalar context and a
616             hashref on success in list context. Only the keys "expiry" and "regid"
617             in this hashref are populated so far.
618              
619             To register a new domain to a new registrant you can either create the
620             registrant first to get the ID or you can replace the 'registrant' value
621             in the C<$domain> with a hashref of the registrant and C will
622             create the registrant first as a handy shortcut.
623              
624             The alias C can be used in place of C.
625              
626             =cut
627              
628             sub register {
629 0     0 0   my $self = shift;
630 0           return $self->create_domain (@_);
631             }
632              
633             sub create_domain {
634 0     0 0   my ($self, $domain) = @_;
635              
636             # New contact? Register them first
637 0 0         if (ref $domain->{registrant}) {
638 0           my $contyes = $self->create_contact ($domain->{registrant});
639 0 0 0       if ($contyes and $contyes == 1) {
640 0           $domain->{registrant} = $domain->{registrant}->{id};
641             } else {
642 0           return undef;
643             }
644             }
645              
646 0           my $frame;
647 0           my @spec = $self->spec ('domain');
648 0           $frame = Net::EPP::Frame::Command::Create->new;
649 0           my $obj = $frame->addObject (@spec);
650 0           my $name = $frame->createElement ('domain:name');
651 0           $name->appendText ($domain->{name});
652 0           $obj->appendChild ($name);
653              
654             # Set the duration - integral years only
655 0           my $years = $domain->{period};
656 0   0       $years ||= $self->{def_years};
657 0           $name = $frame->createElement ('domain:period');
658 0           $name->appendText ($years);
659 0           $name->setAttribute ('unit', 'y');
660 0           $obj->appendChild ($name);
661              
662              
663             # Add in the nameservers, if any
664 0           my $ns = $domain->{nameservers};
665 0 0         if (scalar keys %$ns) {
666 0           my @hostspec = $self->spec ('host');
667 0           $name = $frame->createElement ('domain:ns');
668 0           $name->setNamespace ($hostspec[1], 'ns', 0);
669 0           for my $i (0..9) {
670 0 0         if ($ns->{"nsid$i"}) {
    0          
671             # Not used anymore. Logic kept in case Nominet reverse their
672             # decision
673 0           $self->_add_nsid ($name, $frame, $ns->{"nsid$i"});
674             } elsif ($ns->{"nsname$i"}) {
675 0           $self->_add_nsname ($name, $frame, $ns->{"nsname$i"});
676             }
677             }
678 0           $obj->appendChild ($name);
679             }
680              
681             # Set up the registrant
682 0           $name = $frame->createElement ('domain:registrant');
683 0           $name->appendText ($domain->{registrant});
684 0           $obj->appendChild ($name);
685              
686             # add auth
687             # Crazily, this element must be present to pass the XML checks, but
688             # after detecting its presence, Nominet subsequently ignores it.
689 0           $name = $frame->createElement ('domain:authInfo');
690 0           my $pw = $frame->createElement ('domain:pw');
691 0           $pw->appendText ('dummyvalue');
692 0           $name->appendChild ($pw);
693 0           $obj->appendChild ($name);
694              
695              
696             # DNSSEC
697 0           my $exttype = 'secDNS';
698 0 0         if ($domain->{$exttype}) {
699 0           my @spec = $self->spec ($exttype);
700 0           my $obj2 = $frame->addObject (@spec);
701 0           for my $dsrec (@{$domain->{$exttype}}) {
  0            
702 0           $self->_add_dsrec ($obj2, $frame, $dsrec);
703             }
704              
705 0           my $extension = $frame->command->new ('extension');
706 0           $extension->appendChild ($obj2);
707 0           $frame->command->insertAfter ($extension, $frame->getCommandNode);
708             }
709              
710             # Request complete, so send the frame
711 0           $frame->getCommandNode->appendChild ($obj);
712              
713 0           my $response = $self->_send_frame ($frame);
714 0 0         return undef unless $Code == 1000;
715 0           my $date = $response->getNode ($spec[1], 'exDate')->firstChild->toString ();
716 0 0         warn "expiry date = $date\n" if $Debug;
717              
718             # Perhaps this return should use wantarray instead?
719 0           return @{[{ expiry => $date, regid => $domain->{registrant} }]};
  0            
720             }
721              
722             =head2 Register accounts
723              
724             To register an account, you will need to create a hashref of the
725             account like this to perform the registration.
726              
727             my $registrant = {
728             'id' => "ABC123",
729             'name' => 'Example Company',
730             'trad-name' => 'Examples4u',
731             'type' => 'LTD',
732             'co-no' => '12345678',
733             'disclose' => {
734             'org' => 1,
735             'addr' => 0
736             },
737             'postalInfo' => { loc => {
738             'name' => 'Arnold N Other',
739             'org' => 'Example Company',
740             'addr' => {
741             'street' => ['555 Carlton Heights'],
742             'city' => 'Testington',
743             'sp' => 'Testshire',
744             'pc' => 'XL99 9XL',
745             'cc' => 'GB'
746             }
747             }},
748             'voice' => '+44.1234567890',
749             'email' => 'a.n.other@example.com'
750             };
751             my $res = $epp->create_contact ($registrant) or die $epp->get_reason;
752              
753             It returns undef on failure, 1 on success. The new id must be unique
754             (across the entire registry) otherwise the creation will fail. If no id
755             is specified a random one will be used instead and can subsequently be
756             extracted as C<$registrant-E{id}> in the calling code.
757              
758             =cut
759              
760             # Nominet now only has one contact per registrant, so this is
761             # effectively creating a new registrant.
762             sub create_contact {
763 0     0 0   my ($self, $contact) = @_;
764              
765             # Use random id if none supplied
766 0   0       $contact->{id} ||= $self->random_id;
767 0   0       $contact->{authInfo} ||= 12345;
768 0 0         unless (defined $contact->{fax}) { $contact->{fax} = ''; }
  0            
769 0 0 0       unless (defined $contact->{voice}) {
770 0           $Error = "Missing contact phone number";
771 0           return undef;
772             } elsif (not $self->valid_voice ($contact->{voice})) {
773             $Error = "Bad phone number $contact->{voice} should be +NNN.NNNNNNNNNN";
774             return undef;
775             }
776 0           my $frame = $self->_prepare_create_contact_frame($contact);
777              
778             # Extensions
779 0           my @spec = $self->spec ('contact-nom-ext');
780 0           my $obj = $frame->addObject (@spec);
781 0           for my $field (qw/ trad-name type co-no /) {
782 0 0         next unless ($contact->{$field});
783 0           my $name = $frame->createElement("contact-nom-ext:$field");
784 0           $name->appendText ($contact->{$field});
785 0           $obj->appendChild ($name);
786             }
787 0           my $extension = $frame->command->new ('extension');
788 0           $extension->appendChild ($obj);
789 0           $frame->command->insertAfter ($extension, $frame->getCommandNode);
790              
791 0 0         if (defined $contact->{disclose}) {
792 0           my $add = $frame->createElement ('contact:disclose');
793 0           $add->setAttribute('flag', '1');
794 0           for my $field (qw/org addr/) {
795 0 0         next unless $contact->{disclose}->{$field};
796 0           my $disc = $frame->createElement ("contact:$field");
797 0           $disc->setAttribute('type', 'loc');
798 0           $add->appendChild ($disc);
799             }
800 0 0         $frame->getNode('create')->getChildNodes->shift->appendChild ($add)
801             if $add->hasChildNodes;
802             }
803              
804 0           my $response = $self->_send_frame ($frame);
805 0 0         return $Code == 1000 ? 1 : undef;
806             }
807              
808             =head2 Register nameservers
809              
810             To register a nameserver:
811              
812             my $host = {
813             name => "ns1.foo.co.uk",
814             addrs => [
815             { ip => '10.2.2.1', version => 'v4' },
816             ],
817             };
818             my ($res) = $epp->create_host ($host);
819              
820             It returns undef on failure or 1 on success.
821              
822             =cut
823              
824             # Only need this to set $Code, which is rather annoying.
825             sub create_host {
826 0     0 0   my ($self, $host) = @_;
827 0           my $frame = $self->_prepare_create_host_frame($host);
828 0           return defined $self->_send_frame ($frame);
829             }
830              
831             sub _add_nsname {
832 0     0     my ($self, $name, $frame, $fqdn) = @_;
833 0           my $nsname = $frame->createElement ('domain:hostObj');
834 0           $nsname->appendText ($fqdn);
835 0           $name->appendChild ($nsname);
836 0           return;
837             }
838              
839             sub _add_nsaddr {
840 0     0     my ($self, $name, $frame, $addr) = @_;
841 0           my $nsaddr = $frame->createElement ('host:addr');
842 0           $nsaddr->setAttribute ('ip', $addr->{version});
843 0           $nsaddr->appendText ($addr->{ip});
844 0           $name->appendChild ($nsaddr);
845 0           return;
846             }
847              
848             sub _add_dsrec {
849 0     0     my ($self, $name, $frame, $dsrec) = @_;
850 0           my $ds = $frame->createElement ('secDNS:dsData');
851 0           for my $key (qw/ keyTag alg digestType digest /) {
852 0           my $field = $frame->createElement ("secDNS:$key");
853 0           $field->appendText ($dsrec->{$key});
854 0           $ds->appendChild ($field);
855             }
856 0           $name->appendChild ($ds);
857 0           return;
858             }
859              
860             =head1 Modify objects
861              
862             The domains, contacts and hosts once created can be modified using
863             these methods.
864              
865             =head2 Modify domains
866              
867             To modify a domain, you will need to create a hashref of the
868             changes like this:
869              
870             my $changes = {
871             'name' => 'foo.co.uk',
872             'add' => { ns => ['ns1.newhost.com', 'ns2.newhost.com'] },
873             'rem' => { ns => ['ns1.oldhost.net', 'ns2.oldhost.net'] },
874             'chg' => {},
875             'auto-bill' => 21,
876             'auto-period' => 5,
877             'next-bill' => '',
878             'notes' => ['A first note', 'The second note']
879             };
880             my $res = $epp->update_domain ($changes) or die $epp->get_reason;
881              
882             This example adds and removes nameservers using the C and C groups.
883             You cannot use C to change nameservers or extension fields. The C
884             entry is only used to move a domain between registrants with the same
885             name.
886              
887             The C and C groups are also used to add and remove DS records.
888             eg:
889              
890             my $changes = {
891             'name' => 'foo.co.uk',
892             'add' => {
893             secDNS => [{
894             keyTag => 25103,
895             alg => 5,
896             digestType => 1,
897             digest => '8A9CEBB665B78E0142F1CEF47CC9F4205F600685'
898             }]
899             },
900             'rem' => {}
901             };
902              
903             The extension fields can only be set outside of the add, rem and
904             chg fields. The supported extensions in this module are: C,
905             C, C, C, C and
906             C. All of these are scalars aside from C which is an array ref.
907              
908             C returns undef on failure, 1 on success.
909              
910             There is also a convenience method C which takes the
911             domain name as the first argument and the hashref of changes as the
912             second argument.
913              
914             =cut
915              
916             sub update_domain {
917 0     0 0   my ($self, $data) = @_;
918 0           return $self->modify_domain ($data->{name}, $data);
919             }
920              
921             sub modify_domain {
922 0     0 0   my ($self, $domain, $data) = @_;
923              
924             # Sort out the domain to be updated
925 0           my $frame;
926 0           my @spec = $self->spec ('domain');
927 0           $frame = Net::EPP::Frame::Command::Update->new;
928 0           my $obj = $frame->addObject (@spec);
929 0           my $name = $frame->createElement ('domain:name');
930 0           $name->appendText ($domain);
931 0           $obj->appendChild ($name);
932              
933             #Add nameservers as applicable
934 0           my @hostspec = $self->spec ('host');
935              
936 0           for my $action ('add', 'rem', 'chg') {
937 0 0         if ($data->{$action}) {
938 0           $name = $frame->createElement ("domain:$action");
939 0 0 0       if ($action ne 'chg' && $data->{$action}->{ns}) {
    0          
940 0           my $name2 = $frame->createElement ("domain:ns");
941 0           for my $ns (@{$data->{$action}->{ns}}) {
  0            
942 0           $self->_add_nsname ($name2, $frame, $ns);
943             }
944 0           $name->appendChild ($name2);
945             } elsif ($action eq 'chg') {
946 0 0         if ($data->{$action}->{registrant}) {
947 0           my $name2 = $frame->createElement ("domain:registrant");
948 0           $name2->appendText ($data->{$action}->{registrant});
949 0           $name->appendChild ($name2);
950             } else {
951 0           carp "'chg' is present but no 'registrant' field";
952             }
953             }
954 0           $obj->appendChild ($name);
955             }
956             }
957 0           $frame->getCommandNode->appendChild ($obj);
958              
959             # Extensions
960 0           @spec = $self->spec ('domain-nom-ext');
961 0           $obj = $frame->addObject (@spec);
962 0           for my $field (qw/ auto-bill auto-period
963             next-bill next-period renew-not-required reseller /) {
964 0 0         next unless ($data->{$field});
965 0           my $name = $frame->createElement("domain-nom-ext:$field");
966 0           $name->appendText ($data->{$field});
967 0           $obj->appendChild ($name);
968             }
969 0 0         if ($data->{notes}) {
970 0           for my $field (@{$data->{notes}}) {
  0            
971 0           my $name = $frame->createElement("domain-nom-ext:notes");
972 0           $name->appendText ($field);
973 0           $obj->appendChild ($name);
974             }
975             }
976             # DNSSEC
977             # Nominet does not support MaxSigLife which is the only possible use
978             # for 'chg', so do not cater for it here (yet).
979             #
980 0           my $exttype = 'secDNS';
981 0           @spec = $self->spec ($exttype);
982 0           my $obj2 = $frame->addObject (@spec);
983 0           for my $action ('rem', 'add') {
984 0 0         if ($data->{$action}->{$exttype}) {
985 0           $name = $frame->createElement ("$exttype:$action");
986 0           for my $dsrec (@{$data->{$action}->{$exttype}}) {
  0            
987 0           $self->_add_dsrec ($name, $frame, $dsrec);
988             }
989 0           $obj2->appendChild ($name);
990             }
991             }
992              
993 0           my $extension = $frame->command->new ('extension');
994 0           $extension->appendChild ($obj);
995 0           $extension->appendChild ($obj2);
996 0           $frame->command->insertAfter ($extension, $frame->getCommandNode);
997              
998 0           my $response = $self->_send_frame ($frame);
999 0 0         return $Code == 1000 ? 1 : undef;
1000             }
1001              
1002              
1003             =head2 Modify contacts
1004              
1005             To modify a contact, which includes aspects of the registrant such as
1006             the disclose flags etc., you will again need to create a hashref of the
1007             changes like this:
1008              
1009             my $changes = {
1010             'id' => 'ABC123',
1011             'type' => 'FCORP',
1012             'trad-name' => 'American Industries',
1013             'co-no' => '99998888',
1014             'postalInfo' => {
1015             'loc' => {
1016             'name' => 'James Johnston',
1017             'addr' => {
1018             'street' => ['7500 Test Plaza', 'Testingburg'],
1019             'city' => 'Testsville',
1020             'sp' => 'Testifornia',
1021             'pc' => '99999',
1022             'cc' => 'US',
1023             }
1024             }
1025             },
1026             'voice' => '+1.77777776666',
1027             'email' => 'jj@example.com',
1028             'disclose' => {
1029             'addr' => 1
1030             }
1031             };
1032             my $res = $epp->update_contact ($changes) or die $epp->get_reason;
1033              
1034             Note that this differs from the syntax of L where that
1035             takes the stock C, C and C elements.
1036              
1037             It returns undef on failure, 1 on success.
1038              
1039             There is also a convenience method C which takes the
1040             contact id as the first argument and the hashref of changes as the
1041             second argument.
1042              
1043             Note that due to an undocumented restriction in Nominet's EPP servers
1044             it is not possible to modify the disclose flags for both addr and org
1045             to different values in one request.
1046              
1047             If the hashref contains the key C like so:
1048              
1049             my $changes = { id => 'ABC123', 'new-id' => 'XYZ789' };
1050              
1051             then the ID of the contact will be changed to the new ID (which must
1052             be unique in the entire registry). In this case any other fields in the
1053             hashref will be ignored.
1054              
1055             =cut
1056              
1057             sub update_contact {
1058 0     0 0   my ($self, $data) = @_;
1059 0           return $self->modify_contact ($data->{id}, $data);
1060             }
1061              
1062             sub modify_contact {
1063 0     0 0   my ($self, $cont, $data) = @_;
1064              
1065             # If given a 'new-id', update that and nothing else
1066             return $self->_modify_contact_id ($cont, $data->{'new-id'})
1067 0 0         if exists $data->{'new-id'};
1068              
1069             # Sort out the contact to be updated
1070 0           my $frame;
1071 0           my @spec = $self->spec ('contact');
1072 0           $frame = Net::EPP::Frame::Command::Update->new;
1073 0           my $obj = $frame->addObject (@spec);
1074 0           my $name = $frame->createElement ('contact:id');
1075 0           $name->appendText ($cont);
1076 0           $obj->appendChild ($name);
1077 0           my $chg = $frame->createElement ('contact:chg');
1078              
1079             # Ideally we should be able to do this:
1080             # $data->{id} ||= $cont;
1081             # my $frame = $self->_generate_update_contact_frame($data);
1082             # but it won't work because of present, but empty, add/rem/chg
1083             # elements. Equally we cannot do this:
1084             # my $frame = Net::EPP::Frame::Command::Update::Contact->new;
1085             # $frame->setContact ( $cont );
1086             # so instead it needs this extra chunk of code which follows:
1087              
1088             # Set contact details
1089 0 0         if (defined $data->{postalInfo}) {
1090             #Update name and addr
1091 0           for my $intloc ('int', 'loc') {
1092 0 0         next unless $data->{postalInfo}->{$intloc};
1093 0           my $elem = $frame->createElement ("contact:postalInfo");
1094 0           $elem->setAttribute('type', $intloc);
1095             # Name change?
1096 0           my $thisone = $data->{postalInfo}->{$intloc};
1097 0 0         if ($thisone->{name}) {
1098 0           my $newname = $frame->createElement ('contact:name');
1099 0           $newname->appendText ($thisone->{name});
1100 0           $elem->appendChild ($newname);
1101             }
1102 0 0         if ($thisone->{addr}) {
1103 0           my $addr = $frame->createElement ('contact:addr');
1104 0           for my $addrbitkey (qw/street city sp pc cc/) {
1105 0 0         next unless defined $thisone->{addr}->{$addrbitkey};
1106 0           my $addrbit = $thisone->{addr}->{$addrbitkey};
1107 0 0         if (ref($addrbit) eq 'ARRAY') {
1108             # Only for street
1109 0           for my $street (@$addrbit) {
1110 0           my $stbit = $frame->createElement ("contact:$addrbitkey");
1111 0           $stbit->appendText ($street);
1112 0           $addr->appendChild ($stbit);
1113             }
1114             } else {
1115 0           my $field = $frame->createElement ("contact:$addrbitkey");
1116 0           $field->appendText ($addrbit);
1117 0           $addr->appendChild ($field);
1118             }
1119             }
1120 0           $elem->appendChild($addr);
1121             }
1122 0           $chg->appendChild ($elem);
1123             }
1124             }
1125 0 0 0       if (defined $data->{voice} and not $self->valid_voice ($data->{voice})) {
1126 0           $Error = "Bad phone number $data->{voice} should be +NNN.NNNNNNNNNN";
1127 0           return undef;
1128             }
1129 0           for my $field ('voice', 'email') {
1130 0 0         next unless defined $data->{$field};
1131 0           my $elem = $frame->createElement ("contact:$field");
1132 0           $elem->appendText ($data->{$field});
1133 0           $chg->appendChild ($elem);
1134             }
1135 0 0         if (defined $data->{disclose}) {
1136             # Return an error if there's a mix of flags
1137 0           my @flags = values (%{$data->{disclose}});
  0            
1138 0 0 0       if ($#flags > 0 && $flags[0] != $flags[1]) {
1139 0           $Error = "Nominet prohibits adding and removing disclosures " .
1140             "in one action";
1141 0           return;
1142             }
1143             # This doesn't need to be so complicated but it's staying this
1144             # way in case Nominet decide to allow a mix of actions in the
1145             # future.
1146 0           my $add = $frame->createElement ('contact:disclose');
1147 0           $add->setAttribute('flag', '1');
1148 0           my $del = $frame->createElement ('contact:disclose');
1149 0           $del->setAttribute('flag', '0');
1150 0           for my $field (qw/org addr/) {
1151 0 0         next unless defined $data->{disclose}->{$field};
1152 0           my $disc = $frame->createElement ("contact:$field");
1153 0           $disc->setAttribute('type', 'loc');
1154 0 0         ($data->{disclose}->{$field} ? $add : $del)->appendChild ($disc);
1155             }
1156 0           for my $child ($add, $del) {
1157 0 0         $chg->appendChild ($child) if $child->hasChildNodes;
1158             }
1159             }
1160 0 0         if ($chg->hasChildNodes) { $obj->appendChild($chg); }
  0            
1161              
1162             # Extensions
1163 0           @spec = $self->spec ('contact-nom-ext');
1164 0           $obj = $frame->addObject (@spec);
1165 0           for my $field (qw/ trad-name type co-no /) {
1166 0 0         next unless ($data->{$field});
1167 0           my $name = $frame->createElement("contact-nom-ext:$field");
1168 0           $name->appendText ($data->{$field});
1169 0           $obj->appendChild ($name);
1170             }
1171 0           my $extension = $frame->command->new ('extension');
1172 0           $extension->appendChild ($obj);
1173 0           $frame->command->insertAfter ($extension, $frame->getCommandNode);
1174              
1175 0           my $response = $self->_send_frame($frame);
1176 0 0         return $Code == 1000 ? 1 : undef;
1177             }
1178              
1179             sub _modify_contact_id {
1180 0     0     my ($self, $old_id, $new_id) = @_;
1181              
1182 0           my @spec = $self->spec ('contact-id');
1183 0           my $frame = Net::EPP::Frame::Command::Update->new;
1184 0           my $obj = $frame->addObject (@spec);
1185 0           my $id = $frame->createElement ('contact-id:id');
1186 0           $id->appendText ($old_id);
1187 0           $obj->appendChild ($id);
1188 0           my $chg = $frame->createElement ('contact-id:chg');
1189 0           $id = $frame->createElement ('contact-id:id');
1190 0           $id->appendText ($new_id);
1191 0           $chg->appendChild ($id);
1192 0           $obj->appendChild ($chg);
1193              
1194 0           $frame->getCommandNode->appendChild ($obj);
1195 0           my $response = $self->_send_frame ($frame);
1196 0 0         return $Code == 1000 ? 1 : undef;
1197             }
1198              
1199             =head2 Modify nameservers
1200              
1201             To modify a nameserver, you will need to create a hashref of the
1202             changes like this:
1203              
1204             my $changes = {
1205             name => 'ns1.foo.co.uk',
1206             add => { 'addr' => [ { ip => '192.168.0.51', version => 'v4' } ] },
1207             rem => { 'addr' => [ { ip => '192.168.0.50', version => 'v4' } ] },
1208             };
1209             my $res = $epp->update_host ($changes) or die $epp->get_reason;
1210              
1211             This operation can only be used to add and remove ip addresses. The C
1212             element is not permitted to change addresses, so it is likely that only
1213             the C and C elements will ever be needed.
1214              
1215             It returns undef on failure, 1 on success.
1216              
1217             There is also a convenience method C which takes the
1218             host name as the first argument and the hashref of changes as the
1219             second argument.
1220              
1221             =cut
1222              
1223             sub update_host {
1224 0     0 0   my ($self, $data) = @_;
1225 0           return $self->modify_host ($self, $data->{name}, $data);
1226             }
1227              
1228             sub modify_host {
1229 0     0 0   my ($self, $host, $data) = @_;
1230              
1231             # Sort out the domain to be updated
1232 0           my $frame;
1233 0           my @spec = $self->spec ('host');
1234 0           $frame = Net::EPP::Frame::Command::Update->new;
1235 0           my $obj = $frame->addObject (@spec);
1236 0           my $name = $frame->createElement ('host:name');
1237 0           $name->appendText ($host);
1238 0           $obj->appendChild ($name);
1239              
1240 0           for my $action ('add', 'rem', 'chg') {
1241 0 0         if ($data->{$action}) {
1242 0           $name = $frame->createElement ("host:$action");
1243 0 0         if ($data->{$action}->{addr}) {
1244             #my $name2 = $frame->createElement ("host:addr");
1245 0           for my $addr (@{$data->{$action}->{addr}}) {
  0            
1246 0           $self->_add_nsaddr ($name, $frame, $addr);
1247             }
1248             #$name->appendChild ($name2);
1249             }
1250 0           $obj->appendChild ($name);
1251             }
1252             }
1253              
1254 0           $frame->getCommandNode->appendChild ($obj);
1255 0           my $response = $self->_send_frame ($frame);
1256 0 0         return $Code == 1000 ? 1 : undef;
1257             }
1258              
1259             =head1 Fork contact
1260              
1261             my $res = $epp->fork ($old_id, $new_id, @domains);
1262              
1263             Splitting out some domains on a contact to a copy of that contact can be
1264             achieved using C. The first optional argument is the existing
1265             contact ID. If this is undef then the existing contact will be that on
1266             the listed domains.
1267              
1268             The second optional argument is the ID of the new contact to create. If
1269             this is undef then a random ID will be assigned by Nominet.
1270              
1271             The third and subsequent arguments are the domain names to be moved from the
1272             existing contact to the new.
1273              
1274             Returns the new contact ID on success, undef otherwise.
1275              
1276             =cut
1277              
1278             sub fork {
1279 0     0 0   my ($self, $old_id, $new_id, @doms) = @_;
1280              
1281 0           my $type = 'f';
1282 0           my @spec = $self->spec ($type);
1283 0           my $frame = Net::EPP::Frame::Command::Update->new;
1284              
1285 0           my $obj = $frame->createElement ('f:fork');
1286 0           $obj->setAttribute ("xmlns:$type", $spec[1]);
1287              
1288 0 0         if (defined $old_id) {
1289 0           my $id = $frame->createElement ('f:contactId');
1290 0           $id->appendText ($old_id);
1291 0           $obj->appendChild ($id);
1292             }
1293 0 0         if (defined $new_id) {
1294 0           my $id = $frame->createElement ('f:newContactId');
1295 0           $id->appendText ($new_id);
1296 0           $obj->appendChild ($id);
1297             }
1298 0           for my $dom (@doms) {
1299 0           my $elem = $frame->createElement ('f:domainName');
1300 0           $elem->appendText ($dom);
1301 0           $obj->appendChild ($elem);
1302             }
1303              
1304 0           $frame->getCommandNode->appendChild ($obj);
1305 0           my $response = $self->_send_frame ($frame);
1306 0 0         return undef unless $Code == 1000;
1307 0           my $id = $response->getNode ('contact:id')->firstChild->toString ();
1308 0 0         warn "Forked contact ID is $id\n" if $Debug;
1309 0           return $id;
1310             }
1311              
1312             =head1 Querying objects
1313              
1314             The interface for querying domains, contacts and hosts is the same as
1315             for L with the addendum that authinfo is not used at
1316             Nominet so can be ignored. The interface is simply:
1317              
1318             my $domhash = $epp->domain_info ($domainname);
1319             my $fulldomhash = $epp->domain_info ($domainname, undef, $follow);
1320             my $conthash = $epp->contact_info ($contid);
1321             my $hosthash = $epp->host_info ($hostname);
1322              
1323             =cut
1324              
1325             sub _info {
1326 0     0     my ($self, $type, $identifier) = @_;
1327 0           my $frame;
1328 0 0         warn "In _info, type = $type\n" if $Debug;
1329 0 0         if ($type eq 'domain') {
    0          
    0          
1330 0           my @spec = $self->spec ('domain');
1331 0           $frame = Net::EPP::Frame::Command::Info->new;
1332             # The stock frame adds an incorrect domain element - need it
1333             # removed or overwritten first
1334 0           my $obj = $frame->addObject (@spec);
1335 0           my $name = $frame->createElement ('domain:name');
1336 0           $name->appendText ($identifier);
1337 0           $obj->appendChild ($name);
1338 0           $frame->getCommandNode->appendChild ($obj);
1339             } elsif ($type eq 'contact') {
1340 0           my @spec = $self->spec ($type);
1341 0           $frame = Net::EPP::Frame::Command::Info->new;
1342 0           my $obj = $frame->addObject (@spec);
1343 0           my $name = $frame->createElement ('contact:id');
1344 0           $identifier =~ s/-UK$//;
1345 0           $name->appendText ($identifier);
1346 0           $obj->appendChild ($name);
1347 0           $frame->getCommandNode->appendChild ($obj);
1348             } elsif ($type eq 'host') {
1349 0           $frame = Net::EPP::Frame::Command::Info::Host->new;
1350 0           $frame->setHost($identifier);
1351             } else {
1352 0           $Code = 0;
1353 0           $Error = "Unknown object type '$type'";
1354 0           return undef;
1355             }
1356              
1357 0 0         my $response = $self->_send_frame ($frame) or return undef;
1358 0           my $infData = $response->getNode(($self->spec($type))[1], 'infData');
1359              
1360 0 0         if ($type eq 'domain') {
    0          
    0          
1361 0           my $extra = $response->getNode('domain-nom-ext:infData');
1362 0           my $secdns = $response->getNode('secDNS:infData');
1363 0           return $self->_domain_infData_to_hash($infData, $extra, $secdns);
1364             } elsif ($type eq 'contact') {
1365             # Grab disclose infdata before Net::EPP::Simple deletes it
1366 0           my $disclose = $self->_disclose_infData_to_hash ($infData);
1367 0           $self->_clean_addr($infData);
1368 0           my $this = $self->_contact_infData_to_hash($infData);
1369             # Add in the Nominet extras (reg, rather than contact)
1370 0           my $extra = $response->getNode('contact-nom-ext:infData');
1371 0           return $self->_merge_contact_infData ($this, $extra, $disclose);
1372             } elsif ($type eq 'host') {
1373 0           return $self->_host_infData_to_hash($infData);
1374             }
1375             }
1376              
1377             sub _domain_infData_to_hash {
1378 0     0     my ($self, $infData, $extra, $secdns) = @_;
1379              
1380 0           my $hash = $self->_node_to_hash ($infData, ['registrant',
1381             'clID', 'crID', 'crDate', 'exDate', 'name', 'roid']);
1382              
1383 0           my $extrahash = $self->_node_to_hash ($extra, [
1384             'auto-bill', 'next-bill', 'auto-period',
1385             'next-period', 'reg-status', 'renew-not-required', 'notes', 'reseller']);
1386              
1387 0           for (keys %$extrahash) {
1388 0           $hash->{$_} = $extrahash->{$_};
1389             }
1390              
1391 0 0         if ($secdns) {
1392 0           my $dsObjs = $secdns->nonBlankChildNodes;
1393 0           while (my $dsObj = $dsObjs->shift) {
1394 0           push @{$hash->{secDNS}}, $self->_node_to_hash ($dsObj);
  0            
1395             }
1396             }
1397              
1398 0           my $hostObjs = $infData->getElementsByLocalName('hostObj');
1399 0           while (my $hostObj = $hostObjs->shift) {
1400 0           push(@{$hash->{ns}}, $hostObj->textContent);
  0            
1401             }
1402              
1403 0           return $hash;
1404             }
1405              
1406             sub _disclose_infData_to_hash {
1407 0     0     my ($self, $infData) = @_;
1408 0           my $disc = $infData->getElementsByLocalName('disclose')->shift;
1409 0 0         return unless $disc;
1410 0           my $flag = $disc->getAttribute('flag');
1411 0           my $hash;
1412 0           for my $child ($disc->getChildrenByTagName('*')) {
1413 0           $hash->{$child->localname} = $flag;
1414             }
1415 0           return $hash;
1416             }
1417              
1418             sub _tag_infData_to_hash {
1419 0     0     my ($self, $infData) = @_;
1420 0           return $self->_node_to_hash ($infData, ['registrar-tag',
1421             'name', 'handshake', 'trad-name']);
1422             }
1423              
1424             sub _clean_addr {
1425 0     0     my ($self, $infData) = @_;
1426             # Remove the whitespace text nodes from the addresses as
1427             # Net::EPP::Simple does not handle them nicely
1428 0           for my $addr ($infData->getElementsByTagName('contact:addr')) {
1429 0           for my $child ($addr->childNodes) {
1430 0 0         $addr->removeChild ($child)
1431             unless $child->nodeType == XML::LibXML::XML_ELEMENT_NODE;
1432             }
1433             }
1434             }
1435              
1436             sub _merge_contact_infData {
1437 0     0     my ($self, $old, $extra, $disclose) = @_;
1438              
1439 0           my $extrahash = $self->_node_to_hash ($extra, ['type',
1440             'co-no', 'trad-name']);
1441              
1442 0           for (keys %$extrahash) {
1443 0           $old->{$_} = $extrahash->{$_};
1444             }
1445 0           $old->{disclose} = $disclose;
1446 0           return $old;
1447              
1448             }
1449              
1450             sub _node_to_hash {
1451 0     0     my ($self, $node, $namelist) = @_;
1452 0           my $hash = {};
1453 0           foreach my $child ($node->childNodes) {
1454 0 0         next if $child->nodeType != 1;
1455 0           my $tag = $child->localname;
1456 0           my $value = $child->textContent;
1457 0 0         if ($hash->{$tag}) {
1458 0           $hash->{$tag} .= "\n$value";
1459             } else {
1460 0           $hash->{$tag} = $value;
1461             }
1462             }
1463             # Not very efficient for a deep copy, but it works.
1464 0 0         if ($namelist) {
1465 0           my $temp = {};
1466 0           for my $key (@$namelist) {
1467 0   0       $temp->{$key} = $hash->{$key} || '';
1468             }
1469 0           $hash = $temp;
1470             }
1471 0           return $hash;
1472             }
1473              
1474             =head1 List Domains
1475              
1476             Nominet allows listing domains either by registration date (ie. creation
1477             date) or expiry date. The date must be a month in the form YYYY-MM. eg.
1478              
1479             my $domlist = $epp->list_domains ('2019-01', 'expiry');
1480              
1481             will list all the domains expiring in January 2019 as an arrayref. It
1482             will return an empty array ref if there are no matches and undef on
1483             error. The second argument can only be 'expiry' or 'month' (for creation
1484             date). If it is not supplied, the default is 'expiry'.
1485              
1486             =cut
1487              
1488             sub list_domains {
1489 0     0 0   my $self = shift;
1490 0           my $range = shift;
1491 0   0       my $datetype = shift || 'expiry';
1492 0           my $type = 'l';
1493 0           my @spec = $self->spec ($type);
1494 0           my $frame = Net::EPP::Frame::Command::Info->new;
1495 0           my $name = $frame->createElement ('l:list');
1496 0           $name->setAttribute ("xmlns:$type", $spec[1]);
1497 0           my $child = $frame->createElement ("l:$datetype");
1498 0           $child->appendText ($range);
1499 0           $name->appendChild ($child);
1500 0           $frame->getCommandNode->appendChild ($name);
1501              
1502 0 0         my $response = $self->_send_frame($frame) or return undef;
1503 0 0         if ($Code != 1000) { return undef; }
  0            
1504              
1505 0           my $infData = $response->getNode(($self->spec($type))[1], 'listData');
1506 0           my $domlist = [];
1507 0           for my $node ($infData->childNodes) {
1508 0           my $txt = $node->textContent;
1509 0 0         push @$domlist, $txt if $txt =~ /\./;
1510             }
1511              
1512 0           return $domlist;
1513             }
1514              
1515             =head2 List Tags
1516              
1517             When transferring domains it may be useful to have a list of possible
1518             tag names. This method returns the full list of tags as an array ref.
1519             Each entry in the arrayref is itself a hashref with these keys:
1520              
1521             =over
1522              
1523             =item C
1524              
1525             is the tag name to use in release actions, etc.
1526              
1527             =item C
1528              
1529             is the name of the registrar for display purposes
1530              
1531             =item C
1532              
1533             is the trading name of the registrar (may be empty
1534             string)
1535              
1536             =item C
1537              
1538             is "Y" if they require handshakes on transfer
1539             or "N" otherwise
1540              
1541             =back
1542              
1543             my $taglist = $epp->list_tags;
1544              
1545             It accepts no arguments and returns undef on error.
1546              
1547             Note that you must have passed the C option to L
1548             in order to use this method.
1549              
1550             =cut
1551              
1552             sub list_tags {
1553 0     0 0   my $self = shift;
1554 0           my $type = 'tag';
1555 0           my @spec = $self->spec ($type);
1556 0           my $frame = Net::EPP::Frame::Command::Info->new;
1557 0           my $name = $frame->createElement ('tag:list');
1558 0           $name->setAttribute ("xmlns:$type", $spec[1]);
1559 0           $frame->getCommandNode->appendChild ($name);
1560              
1561 0 0         my $response = $self->_send_frame($frame) or return undef;
1562 0 0         if ($Code != 1000) { return undef; }
  0            
1563              
1564 0           my $infData = $response->getNode($spec[1], 'listData');
1565 0           my $taglist = [];
1566 0           for my $node ($infData->childNodes) {
1567 0 0         next unless $node->nodeType == XML::LibXML::XML_ELEMENT_NODE;
1568 0           push @$taglist, $self->_tag_infData_to_hash ($node);
1569             }
1570 0           return $taglist;
1571             }
1572              
1573             =head1 Hello
1574              
1575             EPP allows the use of a "hello" operation which effectively tests that
1576             the connection to the EPP server is still active and also serves to
1577             reset any inactivity timeout which the server might apply. Nominet's
1578             documentation seems to indicate a 60 minute timeout (as at August 2013).
1579              
1580             my $res = $epp->hello ();
1581              
1582             The hello method takes no arguments. It returns 1 on success, undef
1583             otherwise.
1584              
1585             This performs much the same function as the ping method of
1586             L (which could be used instead) but provides more
1587             extensive error handling.
1588              
1589             =cut
1590              
1591             sub hello {
1592 0     0 0   my $self = shift;
1593 0 0         unless ($self->connected) {
1594 0 0         warn "Hello attempt while disconnected\n" if $Debug;
1595 0           return undef;
1596             }
1597 0           my $frame = Net::EPP::Frame::Hello->new;
1598              
1599 0 0         warn "Sending XML = \n" . $frame . "\n" if $Debug > 1;
1600 0           my $greeting = $self->request($frame);
1601 0 0 0       warn "Response XML = \n" . $greeting->toString() . "\n"
1602             if ($Debug > 1 && defined $greeting);
1603              
1604 0 0         unless ($greeting) {
1605 0           $Error = sprintf("Server returned a %d code", $Code);
1606 0           return undef;
1607             }
1608             # greeting returned. Interested in details?
1609 0           return 1;
1610             }
1611              
1612             =head1 Utility methods
1613              
1614             The following utility methods are used internally but are described
1615             here in case they are useful for other purposes.
1616              
1617             =head2 spec
1618              
1619             This utility method takes a 'type' argument and returns a three-valued
1620             array of type, XMLNS and XSI for use with various frame and XML
1621             routines. It is not expected to be called independently by the user but
1622             is here if you need it.
1623              
1624             Type can currently be one of: domain, contact, contact-ext, contact-id
1625             host, l (for list), u (for unrenew), r (for release), f (for fork)
1626              
1627             my @spec = $epp->spec ('domain');
1628              
1629             =cut
1630              
1631             sub spec {
1632 0     0 1   my ($self, $type) = @_;
1633              
1634 0 0         return '' unless $type;
1635              
1636 0 0         if ($type eq 'domain') {
1637 0           return ($type,
1638             "urn:ietf:params:xml:ns:domain-$EPPVer",
1639             "urn:ietf:params:xml:ns:domain-$EPPVer domain-$EPPVer.xsd");
1640             }
1641 0 0 0       if ($type eq 'domain-ext' or $type eq 'domain-nom-ext') {
1642 0           return ($type,
1643             'http://www.nominet.org.uk/epp/xml/domain-nom-ext-1.2',
1644             'http://www.nominet.org.uk/epp/xml/domain-nom-ext-1.2 domain-nom-ext-1.2.xsd');
1645             }
1646 0 0         if ($type eq 'secDNS') {
1647 0           my $ver = 1.1;
1648 0           return ($type,
1649             "urn:ietf:params:xml:ns:secDNS-$ver",
1650             "urn:ietf:params:xml:ns:secDNS-$ver secDNS-$ver.xsd");
1651             }
1652 0 0         if ($type eq 'contact') {
1653 0           return ($type,
1654             "urn:ietf:params:xml:ns:contact-$EPPVer",
1655             "urn:ietf:params:xml:ns:contact-$EPPVer contact-$EPPVer.xsd");
1656             }
1657 0 0 0       if ($type eq 'contact-ext' or $type eq 'contact-nom-ext') {
1658 0           return ($type,
1659             'http://www.nominet.org.uk/epp/xml/contact-nom-ext-1.0',
1660             'http://www.nominet.org.uk/epp/xml/contact-nom-ext-1.0 contact-nom-ext-1.0.xsd');
1661             }
1662 0 0 0       if ($type eq 'contact-ext' or $type eq 'contact-id') {
1663 0           return ($type,
1664             'http://www.nominet.org.uk/epp/xml/std-contact-id-1.0',
1665             'http://www.nominet.org.uk/epp/xml/std-contact-id-1.0 std-contact-id-1.0.xsd');
1666             }
1667 0 0         if ($type eq 'host') {
1668 0           return ($type,
1669             "urn:ietf:params:xml:ns:host-$EPPVer",
1670             "urn:ietf:params:xml:ns:host-$EPPVer host-$EPPVer.xsd");
1671             }
1672 0 0         if ($type eq 'l') {
1673 0           return ($type,
1674             "http://www.nominet.org.uk/epp/xml/std-list-1.0",
1675             "http://www.nominet.org.uk/epp/xml/std-list-1.0 std-list-1.0.xsd");
1676             }
1677 0 0         if ($type eq 'u') {
1678 0           return ($type,
1679             "http://www.nominet.org.uk/epp/xml/std-unrenew-1.0",
1680             "http://www.nominet.org.uk/epp/xml/std-unrenew-1.0 std-unrenew-1.0.xsd");
1681             }
1682 0 0         if ($type eq 'r') {
1683 0           return ($type,
1684             "http://www.nominet.org.uk/epp/xml/std-release-1.0",
1685             "http://www.nominet.org.uk/epp/xml/std-release-1.0 std-release-1.0.xsd");
1686             }
1687 0 0         if ($type eq 'f') {
1688 0           return ($type,
1689             "http://www.nominet.org.uk/epp/xml/std-fork-1.0",
1690             "http://www.nominet.org.uk/epp/xml/std-fork-1.0 std-fork-1.0.xsd");
1691             }
1692 0 0         if ($type eq 'tag') {
1693 0           return ($type,
1694             "http://www.nominet.org.uk/epp/xml/nom-tag-$EPPVer",
1695             "http://www.nominet.org.uk/epp/xml/nom-tag-$EPPVer nom-tag-$EPPVer.xsd");
1696             }
1697             }
1698              
1699             =head2 valid_voice
1700              
1701             The valid_voice method takes one argument which is a
1702             string representing a telephone number and returns 1 if it is a valid
1703             string for the "voice" field of a contact or undef otherwise.
1704              
1705             unless ($epp->valid_voice ($phone)) {
1706             die "The phone number $phone is not in a valid format.";
1707             }
1708              
1709             =cut
1710              
1711             sub valid_voice {
1712 0     0 1   my $self = shift;
1713 0 0         my $phone = shift or return undef;
1714 0 0         if ($phone !~ /^\+\d{1,3}\.[0-9x]+$/) {
1715 0           $Error = "Bad phone number $phone should be +NNN.NNNNNNNNNN";
1716 0           return undef;
1717             }
1718 0           return 1;
1719             }
1720              
1721             =head2 random_id
1722              
1723             The random_id method takes an integer as its optional argument and
1724             returns a random string suitable for use as an ID. When creating a new
1725             contact an ID must be supplied and it must not be globally unique within
1726             the registry (not just within the TAG). This method is used to generate
1727             one of 26339361174458854765907679379456 possible 16-character IDs,
1728             rendering clashes less likely that winning the Lottery two weeks
1729             running (ie. good enough FAPP).
1730              
1731             my $almost_unique_id = $epp->random_id (16);
1732              
1733             The length defaults to 16 if not supplied. L
1734             5730|https://tools.ietf.org/html/rfc5730> specifies that this is the
1735             maximum length for a contact ID.
1736              
1737             =cut
1738              
1739             sub random_id {
1740             # Produce a random 16-character string suitable for use as an object
1741             # ID string if none provided.
1742             # RFC 5730 says 16 chars max for contact ID
1743 0     0 1   my ($self, $len) = @_;
1744 0   0       $len ||= 16;
1745 0           my $randstr = '';
1746 0           while (length ($randstr) < $len) {
1747 0           my $num = int(rand(94)) + 33;
1748 0 0 0       next if ($num == 38 or $num == 60); # XML chars - could escape, but no need
1749 0           $randstr .= chr($num);
1750             }
1751 0           return $randstr;
1752             }
1753              
1754             =head1 Accessors
1755              
1756             The following accessors may be used to extract diagnostic information
1757             from the EPP object:
1758              
1759             my $code = $epp->get_code;
1760             my $error = $epp->get_error;
1761             my $msg = $epp->get_message;
1762             my $reason = $epp->get_reason;
1763              
1764             The first three of these just provide an OO interface to $Code, $Error
1765             and $Message respectively. The user should use these in preference to
1766             the explicit variable names except in the specific instance of a login
1767             or connection failure when no epp object will be returned.
1768              
1769             =cut
1770              
1771             sub get_code {
1772 0     0 0   return $Code;
1773             }
1774              
1775             sub get_error {
1776 0     0 0   return $Error;
1777             }
1778              
1779             sub get_message {
1780 0     0 0   return $Message;
1781             }
1782              
1783             sub get_reason {
1784 0     0 0   my $self = shift;
1785 0           return $self->{'_reason'};
1786             }
1787              
1788             sub set_reason {
1789 0     0 0   my ($self, $response, @spec) = @_;
1790 0           my $reasonnode = $response->getNode ($spec[1], 'reason');
1791 0 0         my $reason = $reasonnode ? $reasonnode->firstChild->toString () : '';
1792 0           $reason .= $response->getElementsByLocalName ('msg')->get_node (1)->firstChild->toString ();
1793 0           $self->{'_reason'} = $reason;
1794              
1795 0           return $self->{'_reason'};
1796             }
1797              
1798             sub get_debug {
1799 0     0 0   my $self = shift;
1800 0           return $self->{debug};
1801             }
1802              
1803             sub set_debug {
1804 0     0 0   my ($self, $debug) = @_;
1805 0 0         croak "Debug must be whole number" unless $debug =~ /^\d+$/;
1806 0           $Debug = $debug;
1807 0 0         $self->{debug} = $Debug > 1 ? 1 : 0; # for parent
1808 0           return $Debug;
1809             }
1810              
1811             sub _send_frame {
1812 0     0     my ($self, $frame) = @_;
1813              
1814 0 0         warn "Frame to send = " . $frame->toString . "\n" if $Debug > 1;
1815 0           my $response = $self->request($frame);
1816 0 0         unless (defined $response) {
1817             # Critical error
1818 0           $Code = 0;
1819 0           $Error = "No response from server";
1820 0           warn $Error;
1821 0           $self->{connected} = undef;
1822 0           $self->{authenticated} = 0;
1823 0 0         if ($self->{reconnect}) {
1824             # Attempt to reconnect
1825 0           for (1 .. $self->{reconnect}) {
1826             $self->_go_connect and
1827 0 0         $self->login (@{$self->{login_params}});
  0            
1828 0 0         if ($self->{authenticated}) {
1829 0           $frame->clTRID->firstChild->setData ('');
1830 0           return $self->_send_frame ($frame);
1831             }
1832 0           warn "Re-connection attempt $_ of $self->{reconnect} failed.\n";
1833 0           sleep 2;
1834             }
1835             }
1836 0           return undef;
1837             }
1838 0 0         warn "Response = " . $response->toString . "\n" if $Debug > 1;
1839              
1840 0           $Code = $self->_get_response_code($response);
1841 0 0 0       if ($Code < 1000 or $Code > 1999) {
1842 0           $Error = sprintf("Server returned a %d code", $Code);
1843 0 0         warn $Error if $Debug;
1844 0           $Message = $response->msg;
1845             # Get the actual reason
1846 0           my $reason = $response->getElementsByTagName ('reason');
1847 0 0         $self->{'_reason'} = $#$reason >= 0 ? $reason->[0]->firstChild->toString () : undef;
1848 0           return undef;
1849             } else {
1850             # Clear the error
1851 0           $Error = '';
1852 0           $Message = '';
1853 0           $self->{'_reason'} = undef;
1854             }
1855 0           return $response;
1856             }
1857              
1858             =head1 TODO
1859              
1860             =encoding utf8
1861              
1862             =over
1863              
1864             =item * The poll, handshake, lock and reseller operations
1865             are not yet supported.
1866              
1867             =item * Much more extensive tests should be performed.
1868              
1869             =back
1870              
1871             =head1 See Also
1872              
1873             =over
1874              
1875             =item * L
1876              
1877             =item * Nominet's L
1878             Documentation|https://registrars.nominet.uk/uk-namespace/registration-and-domain-management/registration-systems/epp/>
1879              
1880             =item * The EPP RFCs: L,
1881             L,
1882             L and
1883             L.
1884              
1885             =back
1886              
1887             =head1 Author
1888              
1889             Pete Houston
1890              
1891             =head1 Licence
1892              
1893             This software is copyright © 2013-2025 by Pete Houston. It is released
1894             under the Artistic Licence (version 2) and the
1895             GNU General Public Licence (version 2).
1896              
1897             =cut
1898              
1899             1;