File Coverage

blib/lib/Net/TPP.pm
Criterion Covered Total %
statement 73 206 35.4
branch 16 80 20.0
condition 9 63 14.2
subroutine 13 30 43.3
pod 18 18 100.0
total 129 397 32.4


line stmt bran cond sub pod time code
1             package Net::TPP;
2              
3             # Module used to interact with the TPP API
4             # http://www.tppwholesale.com.au/documents/2012_TPPW_API_Specs.pdf
5              
6 2     2   47222 use warnings;
  2         5  
  2         57  
7 2     2   11 use strict;
  2         3  
  2         59  
8 2     2   1617 use LWP;
  2         100707  
  2         2303  
9              
10             our $VERSION = '0.02';
11              
12             sub new {
13 1     1 1 236 my ($class,%options) = @_;
14              
15 1         4 for my $option_key (keys %options) {
16 3         10 my $new_option_key = _fix_case($option_key);
17 3 50       12 if ($new_option_key ne $option_key) {
18 0         0 $options{$new_option_key} = $options{$option_key};
19 0         0 delete $options{$option_key};
20             }
21             }
22              
23             # Add credentials if passed to new()
24 1         6 my $mode_map = _mode_map();
25 1 50       3 my $self = { map { ($options{$_}) ? ($_ => $options{$_}) : () } @{$mode_map->{AUTHENTICATION}->{required_parameters}} };
  3         14  
  1         5  
26 1   50     12 $self->{_url} = $options{URL} || 'https://theconsole.tppwholesale.com.au';
27             # https://theconsole.tppwholesale.com.au/api/auth.pl?AccountNo=xxxx&UserId=xxxx&Password=xxxx
28 1         7 $self->{_url} =~ s#/+$##;
29 1   50     8 $self->{_auth_url} = $options{AuthURL} || '/api/auth.pl';
30 1         4 $self->{_auth_url} =~ s#\?.*$##;
31 1         4 $self->{_auth_url} =~ s#/+$##;
32 1   33     16 $self->{_ua} ||= LWP::UserAgent->new();
33 1   50     1163260 $self->{_ua}->agent( $options{UserAgent} || 'Mozilla/5.0 (Net::TPP perl/LWP)' );
34 1         142 $self->{_error_codes} = _error_codes();
35 1         6 $self->{_mode_map} = $mode_map;
36              
37 1         8 bless $self, $class;
38 1         11 return $self;
39             }
40              
41             sub error {
42 2     2 1 1097 my $self = shift;
43 2   50     15 return $self->{error} || '';
44             }
45              
46             sub call {
47             # Note that this method (or those that call it like can_register_domain) will return a false value where it makes sense -
48             # E.g. can_register_domain(domain => 'test.com') will return false if the domain exists, however can_register_domain(domain => ['test.com','test2.com']) might return
49             # a true value simply so that it can return specific output for all domains.
50 1     1 1 7 my ($self,%options) = @_;
51              
52             # Check the mode
53 1   33     13 my $mode = uc($options{mode}) || uc($options{Mode}) || do {
54             $self->{error_code} = '';
55             $self->{error} = 'No mode passed in';
56             $self->{error_output} = '';
57             return;
58             };
59 1         6 delete $options{mode};
60 1 50       18 unless ($self->{_mode_map}->{$mode}) {
61 0         0 $self->{error_code} = '';
62 0         0 $self->{error} = 'Bad mode passed in '.$mode;
63 0         0 $self->{error_output} = '';
64 0         0 return;
65             }
66              
67             # Convert parameters to appropriate case
68 1         8 for my $option_key (keys %options) {
69 0         0 my $new_option_key = _fix_case($option_key);
70 0 0       0 if ($new_option_key ne $option_key) {
71 0         0 $options{$new_option_key} = $options{$option_key};
72 0         0 delete $options{$option_key};
73             }
74             }
75              
76             # Check for missing parameters
77 1 50       9 if ($mode eq 'AUTHENTICATION') {
78 1         3 for (@{$self->{_mode_map}->{$mode}->{required_parameters}}) {
  1         13  
79 3 50 33     64 $options{$_} = $self->{$_} if ! $options{$_} && $self->{$_};
80             }
81             }
82 1 50 33     13 if ($mode ne 'AUTHENTICATION' && ! $options{SessionID}) {
83 0 0       0 if (! $self->{SessionID}) {
84             # Login automatically
85 0 0       0 return unless $self->login();
86             }
87 0         0 $options{SessionID} = $self->{SessionID};
88             }
89              
90 1 50       4 my @missing_parameters = map { ($options{$_}) ? () : ($_) } @{$self->{_mode_map}->{$mode}->{required_parameters}};
  3         17  
  1         6  
91 1 50       6 if (@missing_parameters) {
92 0         0 $self->{error_code} = '';
93 0         0 $self->{error} = "Missing parameters '".join("', '",@missing_parameters)."'";
94 0         0 $self->{error_output} = '';
95 0         0 return;
96             }
97              
98             # Submit request to TPP
99 1         44 my $response = $self->_request(
100             url => $self->{_mode_map}->{$mode}->{url},
101             %options
102             );
103 1 50       36 return unless $response;
104              
105             # Save the SessionID if need be.
106 0 0 0     0 $self->{SessionID} = $response->{output_string} if $mode eq 'AUTHENTICATION' && ! $self->{SessionID} && $response->{output_string};
      0        
107              
108 0         0 return $response;
109             }
110              
111             sub _fix_case {
112             # If, like the author, you prefer underscores to camel case, this method is used to modify the parameters sent to TPP so that either can be used by this module.
113 3     3   4 my $option_key = shift;
114 3 50       13 if ($option_key eq lc($option_key)) {
115 0         0 my $old_option_key = $option_key;
116 0         0 $option_key =~ s/_id(\b|_)/ID$1/g;
117 0         0 $option_key =~ s/_(\w)/\U$1/g;
118 0         0 $option_key = ucfirst($option_key);
119 0         0 $option_key =~ s/^UserID$/UserId/; # Different case for some reason
120 0         0 $option_key =~ s/^Url$/URL/;
121             }
122 3         7 return $option_key;
123             }
124              
125             sub _stringify_parameter {
126             # Join multiple values if an array ref is passed in.
127 3     3   17 my %options = @_;
128 3 50       25 my $key_equals = (defined $options{key}) ? $options{key} .'=' : '';
129 3 50 33     33 if (ref $options{value} && ref $options{value} eq 'ARRAY') {
    50          
130 0   0     0 return join (($options{delimiter} || '&'),map { $key_equals.$_ } @{$options{value}});
  0         0  
  0         0  
131             } elsif (! ref $options{value}) {
132 3         20 return $key_equals.$options{value}
133             } else {
134 0   0     0 die "Invalid input - only strings or array refs can be passed in, however ".($options{key} || '')." is a ".ref($options{value}).":\n";
135             }
136             }
137              
138             sub _request {
139 1     1   9 my ($self,%options) = @_;
140              
141             # Create request and submit
142 1 100       5 my $q_string = join ('&',map { ($_ ne 'url') ? (_stringify_parameter(key => $_,value => $options{$_}, delimiter => '&')) : () } keys %options);
  4         20  
143 1         17 my $req = HTTP::Request->new(POST => $self->{_url}.$options{url}.'?'.$q_string);
144 1         11006 $req->content($q_string);
145 1         33 my $res = $self->{_ua}->request($req);
146            
147 1 50       1831 if ($res->is_success) {
148             # Certain query responses may have the value of an option passed in prefixing the OK/ERR response code, we check for those here.
149 0 0       0 my $value_prefixes = join('|',
150 0         0 map { (defined $options{$_}) ? (_stringify_parameter(value => $options{$_}, delimiter => '|')) : () }
151             (qw(Domain OrderID)));
152 0 0       0 $value_prefixes = '('.$value_prefixes.')' if $value_prefixes;
153              
154 0 0       0 if ($res->content =~ m#^($value_prefixes:\s)?(?OK|ERR):\s*(?.*)#s) {
155 2     2   1876 my %output = %+;
  2         1085  
  2         4158  
  0         0  
156 0         0 chomp($output{value});
157 0 0       0 if ($output{status} eq 'ERR') {
158 0         0 $self->{error_code} = $output{value};
159 0         0 $self->{error_code} =~ tr/0-9//cd;
160 0         0 $self->{error_output} = $res->content;
161 0   0     0 $self->{error} = $self->{_error_codes}->{ $self->{error_code} } || 'Unknown Error.';
162 0         0 $self->{error}.=' '.$output{value};
163 0         0 return;
164             } else {
165 0         0 return { OK => 1, output_string => $output{value}, output => _create_output($res->content), raw_output => $res->content };
166             }
167             } else {
168 0         0 $self->{error} = "Cannot read output: ".$res->content;
169 0         0 $self->{error_code} = '';
170 0         0 $self->{error_output} = '';
171 0         0 return;
172             }
173             } else {
174 1         15 $self->{error} = "Failed: ".$res->status_line;
175 1         13 $self->{error_code} = '';
176 1         2 $self->{error_output} = '';
177 1         26 return;
178             }
179             }
180              
181             sub _create_output {
182             # This method will handle the different types of output generated by API responses to create a generic output data structure that makes sense.
183 0     0   0 my $output = shift;
184 0         0 my %output_hash;
185 0         0 for my $output_line (split(/[\r\n]+/,$output)) {
186 0         0 chomp($output_line);
187 0 0 0     0 if ($output_line =~ m#^(?\S+):\s*(?OK|ERR):\s*(?.*)#s) {
    0          
    0          
    0          
188 0         0 my ($type,$status,$value) = ($+{type},$+{status},$+{value});
189 0         0 $value =~ s/^\s+//; $value =~ s/\s+$//;
  0         0  
190 0 0 0     0 if ($status eq 'ERR' && $value =~ s/^(\d\d\d)\b(,\s+)?//) {
191 0         0 $output_hash{$type} = _add_output_values($output_hash{$type},_get_output_values($value),status => $status, code => $1);
192             } else {
193 0         0 $output_hash{$type} = _add_output_values($output_hash{$type},_get_output_values($value),status => $status);
194             }
195             } elsif ($output_line =~ m#^(?[^\s\=]+)=(?.*)#) {
196 0         0 my ($type,$value) = ($+{type},$+{value});
197 0         0 $value =~ s/^\s+//; $value =~ s/\s+$//;
  0         0  
198 0         0 $output_hash{$type} = _add_output_values($output_hash{$type},_get_output_values($value));
199             } elsif ($output_line =~ m#^OK:\s*$#) {
200             # OK then.
201             } elsif ($output !~ /[\r\n]/ && $output_line =~ m#^OK: (.+)$#) {
202             # OK: 1234567
203 0         0 return $1;
204             } else {
205             # This is not a key-value output. Exit the loop early, split into an array or just one string if necessary.
206 0 0       0 if ($output =~ /[\r\n]/s) {
207 0         0 return [ split(/[\r\n]+/,$output) ];
208             } else {
209 0         0 return $output;
210             }
211             }
212             }
213 0 0       0 if (scalar keys %output_hash == 1) {
214 0         0 %output_hash = (%{$output_hash{(keys %output_hash)[0]}}); # Remove the top level hash key if it is not necessary.
  0         0  
215             }
216 0         0 return \%output_hash;
217             }
218              
219             sub _add_output_values {
220 0     0   0 my ($hashref,$value,%options) = @_;
221 0 0 0     0 if (defined $hashref && $hashref) {
222 0 0       0 if (ref $hashref eq 'ARRAY') {
223 0         0 push @{$hashref},$value;
  0         0  
224             } else {
225 0         0 $hashref = [ $hashref, $value ];
226             }
227             } else {
228 0 0 0     0 $hashref = $value if defined $value && $value ne '';
229             }
230 0 0       0 if (scalar keys %options) {
231 0 0       0 if (! ref $hashref) {
232 0   0     0 my $message = ''.($hashref || '');
233 0         0 undef $hashref;
234 0         0 $hashref->{message} = $message;
235             }
236 0         0 $hashref->{$_} = $options{$_} for keys %options;
237             }
238 0   0     0 return $hashref || '';
239             }
240              
241             sub _get_output_values {
242 0     0   0 my $value = shift;
243 0         0 my $output_value;
244 0 0       0 if ($value =~ /&[^\&]+=/) { # query string type values
    0          
245 0         0 for my $single_value (split(/\&/,$value)) {
246 0         0 my ($query_key,$query_value) = split(/=/,$single_value,2);
247 0         0 $output_value->{$query_key} = _add_output_values($output_value->{$query_key},$query_value);
248             }
249 0         0 return $output_value;
250             } elsif ($value =~ /^(?[^=]+)=(?.*)/) {
251 0         0 my ($query_key,$query_value) = ($+{query_key},$+{query_value});
252 0         0 $output_value->{$query_key} = _add_output_values($output_value->{$query_key},$query_value);
253 0         0 return $output_value;
254             } else {
255 0         0 return $value;
256             }
257             }
258              
259             sub _mode_map {
260             # A map of basic information for each mode.
261 1     1   5 my $default_parameters = [qw(SessionID Type Object Action)];
262             return {
263 1         13 AUTHENTICATION => {
264             url => '/api/auth.pl',
265             required_parameters => [qw(AccountNo UserId Password)],
266             },
267             ORDER => {
268             url => '/api/order.pl',
269             required_parameters => $default_parameters,
270             },
271             QUERY => {
272             url => '/api/query.pl',
273             required_parameters => $default_parameters,
274             },
275             RESOURCE => {
276             url => '/api/resource.pl',
277             required_parameters => $default_parameters,
278             }
279             }
280             }
281              
282             sub _error_codes {
283 1     1   60 my $error_codes = {
284             '100' => 'Missing required parameters.',
285             '101' => 'API Site not currently functioning.',
286             '102' => 'Cannot authenticate user. AccountNo/UserID/Password/SessionID does not match or session has timed out.',
287             '103' => 'Account has been disabled.',
288             '104' => 'Request coming from incorrect IP addressIP Lock error.',
289             '105' => 'IP lockdown. API request is coming from an IP other than the IPs specified in API settings.',
290             '108' => 'Invalid or not supplied "Type" parameter.',
291             '201' => 'Your Account has not been enabled for this ‘Type’.',
292             '202' => 'Missing "Type" URL parameter or the value of “Type” parameter is not "Domains".',
293             '203' => 'Invalid or not supplied Action/Object parameter(s), or the API call has not been implemented.',
294             '301' => 'Invalid OrderID or order does not belong to your reseller account',
295             '302' => 'Domain name is either invalid or not supplied.',
296             '303' => 'Domain pricings are not setted up for this TLD. If the TLD is disabled in the Reseller Portal, you cannot register the domain.',
297             '304' => 'Domain is already registered, or there is an incomplete domain registration order for this domain already.',
298             '305' => "There's an existing renewal order for this domain, or a new order cannot be created.",
299             '306' => 'Domain is not registered; or is the process of being transferred; or is already with TPP Wholesale; or status prevents it from being transferrred.',
300             '307' => 'Incorrect Domain Password.',
301             '308' => 'Domain UserID or Password not supplied.',
302             '309' => 'Registration for the supplied TLD is not supported.',
303             '310' => 'Domain does not exist, has been deleted or transferred away.',
304             '311' => 'Domain does not exist in your reseller account.',
305             '312' => 'Invalid LinkDomain UserID and/or Password.',
306             '313' => 'The account specified by AccountID does not exist in your reseller profile.',
307             '401' => 'Cannot check for domain availability. Registry connection failed.',
308             '500' => 'Pre-Paid balance is not enough to cover order cost.',
309             '501' => 'Invalid credit card type. See Appendix H for a list of valid credit card types.',
310             '502' => "Invalid credit card number or credit card number doesn't match credit card type.",
311             '503' => 'Invalid credit card expiry date.',
312             '504' => 'Credit Card amount plus current pre-paid balance is not sufficient to cover the cost of the order.',
313             '505' => 'Error with credit card transaction at bank.Please Note: This error code will always be followed by a comma then a description of the error.',
314             '600' => 'Missing mandatory fields or field values are invalid.',
315             '601' => 'Missing mandatory fields.',
316             '602' => 'Invalid hosts have been supplied.',
317             '603' => 'Invalid eligibility fields supplied.',
318             '604' => 'Error with one or more fields associated with aNameserver.Please Note: This error code will always befollowed by a comma then a space separatedlist of fields that have failed.',
319             '610' => 'Registry connection failed.',
320             '611' => 'Domain cannot be renewed,',
321             '612' => 'Domain lock/unlock is not supported',
322             '614' => 'Domain lock/unlock failed.',
323             '615' => 'Delegation failed.',
324             };
325 1         7 return $error_codes;
326             }
327              
328             # Some convenience methods
329              
330             sub login {
331 1     1 1 10 my ($self,%options) = @_;
332 1         10 return $self->call(mode => 'AUTHENTICATION', %options);
333             }
334              
335             sub get_domain_details {
336 0     0 1   my ($self,%options) = @_;
337 0           return $self->call(mode => 'QUERY', Type => 'Domains', Object => 'Domain', Action => 'Details', %options);
338             }
339              
340             sub get_order_status {
341 0     0 1   my ($self,%options) = @_;
342 0           return $self->call(mode => 'QUERY', Type => 'Domains', Object => 'Order', Action => 'OrderStatus', %options);
343             }
344              
345             sub can_renew_domain {
346 0     0 1   my ($self,%options) = @_;
347 0           return $self->call(mode => 'QUERY', Type => 'Domains', Object => 'Domain', Action => 'Renewal', %options);
348             }
349              
350             sub renew_domain {
351 0     0 1   my ($self,%options) = @_;
352 0           return $self->call(mode => 'ORDER', Type => 'Domains', Object => 'Domain', Action => 'Renewal', %options);
353             }
354              
355             sub can_register_domain {
356 0     0 1   my ($self,%options) = @_;
357 0           return $self->call(mode => 'QUERY', Type => 'Domains', Object => 'Domain', Action => 'Availability', %options);
358             }
359              
360             sub register_domain {
361             # Note 'Host' parameter (for name servers) should be an array ref. "If there is less than 2 hosts attached to a domain, the domain will be inactive."
362 0     0 1   my ($self,%options) = @_;
363 0           return $self->call(mode => 'ORDER', Type => 'Domains', Object => 'Domain', Action => 'Create', %options);
364             }
365              
366             sub can_transfer_domain {
367 0     0 1   my ($self,%options) = @_;
368 0           return $self->call(mode => 'QUERY', Type => 'Domains', Object => 'Domain', Action => 'Transfer', %options);
369             }
370              
371             sub transfer_domain {
372 0     0 1   my ($self,%options) = @_;
373 0           return $self->call(mode => 'ORDER', Type => 'Domains', Object => 'Domain', Action => 'TransferRequest', %options);
374             }
375              
376             sub update_hosts {
377 0     0 1   my ($self,%options) = @_;
378 0           return $self->call(mode => 'ORDER', Type => 'Domains', Object => 'Domain', Action => 'UpdateHosts', %options);
379             }
380              
381             sub replace_hosts {
382             # The same as update_hosts but we pass in an extra parameter that TPP recognises that replaces all name servers.
383             # We also check if a domain is locked first, and temporary unlock in order to change the hosts/name servers
384 0     0 1   my ($self,%options) = @_;
385              
386              
387 0           my $domain_locked = 0;
388             # Check if domain is locked and unlock if necessary
389 0   0       my $this_domain = $options{Domain} || $options{domain};
390 0 0         if ($this_domain) {
391 0           my $domain_details = $self->get_domain_details(Domain => $this_domain);
392 0 0 0       if ($domain_details && $domain_details->{output} && defined $domain_details->{output}->{LockStatus} && $domain_details->{output}->{LockStatus} == 2) {
      0        
      0        
393 0           $domain_locked = 1;
394 0           $self->unlock_domain(Domain => $this_domain);
395             }
396             }
397              
398             # Make NS update
399 0           my $ns_output = $self->call(mode => 'ORDER', Type => 'Domains', Object => 'Domain', Action => 'UpdateHosts', RemoveHost => 'ALL', %options);
400              
401             # Re-lock the domain if necessary
402 0 0         if ($domain_locked) {
403 0           $self->lock_domain(Domain => $this_domain);
404             }
405 0           return $ns_output;
406             }
407              
408             sub unlock_domain {
409 0     0 1   my ($self,%options) = @_;
410 0           return $self->call(mode => 'ORDER', Type => 'Domains', Object => 'Domain', Action => 'UpdateDomainLock', DomainLock => 'Unlock', %options);
411             }
412              
413             sub lock_domain {
414 0     0 1   my ($self,%options) = @_;
415 0           return $self->call(mode => 'ORDER', Type => 'Domains', Object => 'Domain', Action => 'UpdateDomainLock', DomainLock => 'Lock', %options);
416             }
417              
418             sub create_contact {
419 0     0 1   my ($self,%options) = @_;
420 0           return $self->call(mode => 'ORDER', Type => 'Domains', Object => 'Contact', Action => 'Create', %options);
421             }
422              
423             sub update_contact {
424 0     0 1   my ($self,%options) = @_;
425 0           return $self->call(mode => 'ORDER', Type => 'Domains', Object => 'Domain', Action => 'UpdateContacts', %options);
426             }
427              
428             *update_name_servers = \&update_hosts;
429              
430             *replace_name_servers = \&replace_hosts;
431              
432             1;
433              
434             __END__