File Coverage

blib/lib/WebService/NFSN.pm
Criterion Covered Total %
statement 72 148 48.6
branch 19 34 55.8
condition 6 22 27.2
subroutine 19 60 31.6
pod 33 40 82.5
total 149 304 49.0


line stmt bran cond sub pod time code
1             #---------------------------------------------------------------------
2             package WebService::NFSN;
3             #
4             # Copyright 2010 Christopher J. Madsen
5             #
6             # Author: Christopher J. Madsen
7             # Created: 3 Apr 2007
8             #
9             # This program is free software; you can redistribute it and/or modify
10             # it under the same terms as Perl itself.
11             #
12             # This program is distributed in the hope that it will be useful,
13             # but WITHOUT ANY WARRANTY; without even the implied warranty of
14             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See either the
15             # GNU General Public License or the Artistic License for more details.
16             #
17             # ABSTRACT: Client for the NearlyFreeSpeech.NET API
18             #---------------------------------------------------------------------
19              
20 2     2   98163 use 5.006;
  2         10  
  2         165  
21 2     2   12 use strict;
  2         4  
  2         78  
22 2     2   11 use warnings;
  2         3  
  2         65  
23 2     2   11 use Carp qw(carp confess croak);
  2         3  
  2         158  
24 2     2   2233 use Digest::SHA 'sha1_hex';
  2         12898  
  2         237  
25 2     2   22 use Exporter 5.57 'import'; # exported import method
  2         41  
  2         115  
26 2     2   6706 use LWP::UserAgent 6 ();
  2         128064  
  2         80  
27 2     2   23 use Scalar::Util 1.01 'reftype';
  2         111  
  2         205  
28 2     2   2345 use Try::Tiny 0.04;
  2         3352  
  2         1688  
29              
30             #=====================================================================
31             # Package Global Variables:
32              
33             our $VERSION = '1.03';
34              
35             our @EXPORT_OK = qw(_eval _eval_or_die);
36              
37             our $saltAlphabet
38             = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
39              
40             our $ua = LWP::UserAgent->new(
41             agent => "WebService-NFSN/$VERSION ",
42             ssl_opts => {
43             verify_hostname => 1,
44             },
45             );
46              
47             our @throw_parameters = (
48             show_trace => 1,
49             ignore_package => __PACKAGE__,
50             ignore_class => 'WebService::NFSN::Object'
51             );
52              
53             #=====================================================================
54             # Helper subs to safely handle string eval without clobbering $@:
55              
56             sub _eval
57             {
58 9     9   23 my ($code) = @_;
59              
60 9         15 my ($error, $success);
61             {
62 9         14 local $@;
  9         14  
63              
64 2     2 1 2355 $success = eval "$code ; 'OK'"; ## no critic ProhibitStringyEval
  2     0 1 15843  
  2     0 1 31  
  9     0 1 5398  
  0     0 1 0  
  0     0 1 0  
  0     0 1 0  
  0     0 1 0  
  0     0 1 0  
  0     0 1 0  
  0     0 1 0  
  0     0 1 0  
  0     0 1 0  
  0     0 1 0  
  0     0 1 0  
  0     0 1 0  
  0     0 1 0  
  0     0 0 0  
  0     0 0 0  
  0     0 0 0  
  0     0 0 0  
  0     0 0 0  
  0     0 1 0  
  0     0 1 0  
  0     0 1 0  
  0     0 1 0  
  0     0 1 0  
  0     0 1 0  
  0     0 1 0  
  0     0 1 0  
  0     0 1 0  
  0     0 1 0  
  0     0 1 0  
  0     0 1 0  
  0     0 1 0  
  0     0   0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
65              
66 9         24 $error = $@;
67             }
68              
69 9 50 33     142 return if $success && $success eq 'OK';
70              
71 0   0     0 return $error || "eval died with false \$\@";
72             } # end _eval
73              
74             sub _eval_or_die
75             {
76 7     7   16 my $error = &_eval; # Pass our @_ to _eval
77              
78 7 50       1292 return unless $error;
79              
80             # Number lines in eval'd code:
81 0         0 my $code = shift;
82 0         0 my $lineNum = ($code =~ tr/\n//);
83 0         0 my $fmt = '%' . length($lineNum) . 'd: ';
84 0         0 $lineNum = 0;
85 0         0 $code =~ s/^/sprintf $fmt, ++$lineNum/gem;
  0         0  
86              
87 0         0 confess "$code\n$error";
88             } # end _eval_or_die
89              
90             #---------------------------------------------------------------------
91             # Helper sub to identify a hashref:
92              
93 3   50 3   33 sub _is_hash { (reftype($_[0]) || '') eq 'HASH' }
94              
95             #=====================================================================
96             # Load a JSON package and define our decode_json function:
97              
98             BEGIN
99             {
100 2 50   2   76 if (_eval "use JSON::XS ()") {
101             # Can't find JSON::XS, try JSON (2.0 or later):
102 0         0 _eval_or_die "use JSON qw(decode_json)";
103             } else {
104 2 50       10 if ($JSON::XS::VERSION >= 2) {
105 2         125 *decode_json = \&JSON::XS::decode_json;
106             } else {
107 0         0 *decode_json = \&JSON::XS::from_json; # old name for decode_json
108             } # end else found JSON::XS prior to version 2.0
109             } # end else we were able to load JSON::XS
110             } # end BEGIN
111              
112             #=====================================================================
113             # Define exceptions:
114              
115             use Exception::Class (
116 2         29 'WebService::NFSN::HTTPError' => {
117             fields => [ qw(request response) ],
118             },
119              
120             'WebService::NFSN::LWPError' => {
121             isa => 'WebService::NFSN::HTTPError',
122             },
123              
124             'WebService::NFSN::NFSNError' => {
125             isa => 'WebService::NFSN::HTTPError',
126             fields => [ qw(debug nfsn) ],
127             },
128 2     2   2526 );
  2         23913  
129              
130             #---------------------------------------------------------------------
131             # Include both the error & debug fields:
132              
133             sub WebService::NFSN::NFSNError::full_message
134             {
135 0     0   0 my ($self) = @_;
136              
137 0         0 $self->error . "\n" . $self->debug;
138             } # end WebService::NFSN::NFSNError::full_message
139              
140             #=====================================================================
141             # Package WebService::NFSN:
142              
143             sub new
144             {
145 7     7 1 18031 my ($class, $login, $apiKey) = @_;
146              
147             # If we didn't get login information, try reading it from .nfsn-api:
148 7 100       32 if (@_ == 1) {
149 5         36 require File::Spec;
150              
151             # Try the current directory, then the home directory:
152 5         9 my $filename = '.nfsn-api';
153 5 50 66     315 $filename = File::Spec->catfile($ENV{HOME}, $filename)
      66        
154             if (not -e $filename and $ENV{HOME} and -d $ENV{HOME});
155              
156             # If we found it, read the file:
157 5 100       89 if (not -e $filename) {
158 1         176 carp("Unable to locate $filename");
159             } else {
160             # Read in the file:
161 4         6 local $_;
162 4 50       224 open(my $in, '<', $filename) or croak("Can't open $filename: $!");
163 4         7 my $contents = '';
164 4         113 $contents .= $_ while <$in>;
165 4 50       45 close $in or croak("Error closing $filename: $!");
166              
167             # Parse the JSON object:
168             my $hashRef = try {
169 4     4   250 decode_json($contents)
170             } catch {
171 1     1   120 croak("Error parsing $filename: $_");
172 4         42 };
173              
174 3 50       57 croak("$filename did not contain a JSON object")
175             unless _is_hash($hashRef);
176              
177 3 100       132 croak(qq'$filename did not define "login"')
178             unless defined ($login = $hashRef->{login});
179 2 50       15 croak(qq'$filename did not define "api-key"')
180             unless defined ($apiKey = $hashRef->{'api-key'});
181             } # end else -e $filename
182             } # end if login & apiKey were not supplied
183              
184             # Make sure we have all our parameters:
185 5 100       99 croak("You must supply a login") unless defined $login;
186 4 100       332 croak("You must supply an API key") unless defined $apiKey;
187              
188 3         22 return bless { login => $login,
189             apiKey => $apiKey,
190             url => 'https://api.nearlyfreespeech.net',
191             }, $class;
192             } # end new
193              
194             #---------------------------------------------------------------------
195             BEGIN {
196             # Create access methods for each object type:
197             # (Member is not auto-generated, because it has a default value)
198              
199 2     2   5580 my $code = '';
200              
201 2         6 foreach my $class (qw(Account DNS Email Site)) {
202              
203 8         17 my $sub = lc $class;
204              
205 8         28 $code .= <<"END CHILD CONSTRUCTOR";
206             sub $sub
207             {
208             require WebService::NFSN::$class;
209              
210             WebService::NFSN::$class->new(\@_);
211             }
212             END CHILD CONSTRUCTOR
213              
214             } # end foreach class
215              
216 2         8 _eval_or_die $code;
217             } # end BEGIN
218              
219             #---------------------------------------------------------------------
220             sub member
221             {
222 0     0 1   my ($self, $member) = @_;
223              
224 0           require WebService::NFSN::Member;
225              
226 0   0       WebService::NFSN::Member->new($self, $member || $self->{login});
227             } # end member
228              
229             #---------------------------------------------------------------------
230             sub make_request
231             {
232 0     0 0   my ($self, $req) = @_;
233              
234             # Collect member name & request URI:
235 0           my $login = $self->{login};
236 0           my $uri = $req->uri->path;
237              
238             # Generate a random 16 character salt value:
239 0           my $salt = join('', map {
240 0           substr($saltAlphabet, int(rand(length $saltAlphabet)), 1)
241             } 1 .. 16);
242              
243             # Generate the NFSN authentication hash:
244 0           my $body_hash = sha1_hex($req->content);
245              
246 0           my $time = time;
247              
248 0           my $hash = sha1_hex("$login;$time;$salt;$self->{apiKey};$uri;$body_hash");
249              
250 0           $req->header('X-NFSN-Authentication' => "$login;$time;$salt;$hash");
251              
252             # Send the request to the NFSN API server:
253 0           my $res = $self->{last_response} = $ua->request($req);
254              
255             # Throw an exception if there was an error:
256 0 0         if ($res->is_error) {
257 0     0     my $param = try { decode_json($res->content) };
  0            
258              
259             # Throw NFSNError if we decoded the response successfully:
260 0 0 0       if (_is_hash($param) and defined $param->{error}) {
261             # If bad timestamp, list the dates:
262 0           my $debug = delete $param->{debug};
263 0 0 0       if ($debug and
264             $debug eq "The authentication timestamp is out of range.") {
265 0           $debug .= ("\n Client request date: " . gmtime($time) .
266             "\n Server response date: " . $res->header('Date'));
267             } # end if authentication timestamp out of range
268              
269             WebService::NFSN::NFSNError->throw(
270 0           error => delete($param->{error}),
271             debug => $debug,
272             nfsn => $param,
273             request => $req,
274             response => $res,
275             @throw_parameters
276             );
277             } # end if throwing NFSNError
278              
279             # Otherwise, throw LWPError:
280             WebService::NFSN::LWPError->throw(
281 0           error => sprintf('%s: %s', $res->code, $res->message),
282             request => $req,
283             response => $res,
284             @throw_parameters
285             );
286             } # end if error
287              
288             # Return the successful response:
289 0           return $res;
290             } # end make_request
291              
292             #---------------------------------------------------------------------
293 0     0 1   sub last_response { $_[0]{last_response} }
294 0     0 0   sub root_url { $_[0]{url} }
295              
296             #=====================================================================
297             # Package Return Value:
298              
299             1;
300              
301             __END__