File Coverage

blib/lib/Net/OperaLink.pm
Criterion Covered Total %
statement 48 211 22.7
branch 0 40 0.0
condition 0 53 0.0
subroutine 16 46 34.7
pod 18 22 81.8
total 82 372 22.0


line stmt bran cond sub pod time code
1             #
2             # High-level API interface to Opera Link
3             #
4             # http://www.opera.com/docs/apis/linkrest/
5             #
6              
7             package Net::OperaLink;
8              
9             our $VERSION = '0.05';
10              
11 1     1   1494 use 5.010;
  1         3  
  1         36  
12 1     1   6 use feature qw(state);
  1         1  
  1         84  
13 1     1   11 use strict;
  1         2  
  1         35  
14 1     1   4 use warnings;
  1         1  
  1         33  
15              
16 1     1   4 use Carp ();
  1         1  
  1         16  
17 1     1   1911 use CGI ();
  1         15971  
  1         31  
18 1     1   4717 use Data::Dumper ();
  1         10437  
  1         36  
19 1     1   1717 use LWP::UserAgent ();
  1         47137  
  1         31  
20 1     1   1160 use Net::OAuth 0.25;
  1         747  
  1         30  
21 1     1   6 use URI ();
  1         1  
  1         13  
22 1     1   1076 use JSON::XS ();
  1         6008  
  1         20  
23              
24 1     1   520 use Net::OperaLink::Bookmark;
  1         3  
  1         31  
25 1     1   492 use Net::OperaLink::Note;
  1         3  
  1         28  
26 1     1   617 use Net::OperaLink::Speeddial;
  1         3  
  1         41  
27              
28             # Opera supports only OAuth 1.0a
29             $Net::OAuth::PROTOCOL_VERSION = &Net::OAuth::PROTOCOL_VERSION_1_0A;
30              
31             use constant {
32 1         125 LINK_SERVER => 'https://link.api.opera.com',
33             OAUTH_PROVIDER => 'auth.opera.com',
34 1     1   5 };
  1         1  
35              
36             # API/OAuth URLs
37             use constant {
38 1         2929 LINK_API_URL => LINK_SERVER . '/rest',
39             OAUTH_BASE_URL => 'https://' . OAUTH_PROVIDER . '/service/oauth',
40 1     1   4 };
  1         2  
41              
42             sub new {
43 0     0 1   my ($class, %opts) = @_;
44              
45 0   0       $class = ref $class || $class;
46              
47 0           for (qw(consumer_key consumer_secret)) {
48 0 0 0       if (! exists $opts{$_} || ! $opts{$_}) {
49 0           Carp::croak "Missing '$_'. Can't instance $class\n";
50             }
51             }
52              
53 0           my $self = {
54             _consumer_key => $opts{consumer_key},
55             _consumer_secret => $opts{consumer_secret},
56             _access_token => undef,
57             _access_token_secret => undef,
58             _request_token => undef,
59             _request_token_secret => undef,
60             _authorized => 0,
61             };
62              
63 0           bless $self, $class;
64              
65 0           return $self;
66             }
67              
68             sub authorized {
69 0     0 1   my ($self) = @_;
70              
71             # We assume to be authorized if we have access token and access token secret
72 0           my $acc_tok = $self->access_token();
73 0           my $acc_tok_secret = $self->access_token_secret();
74              
75             # TODO: No real check if the token is still valid
76 0 0 0       unless ($acc_tok && $acc_tok_secret) {
77 0           return;
78             }
79              
80 0           return 1;
81             }
82              
83             sub access_token {
84 0     0 1   my $self = shift;
85 0 0         if (@_) {
86 0           $self->{_access_token} = shift;
87             }
88 0           return $self->{_access_token};
89             }
90              
91             sub access_token_secret {
92 0     0 1   my $self = shift;
93 0 0         if (@_) {
94 0           $self->{_access_token_secret} = shift;
95             }
96 0           return $self->{_access_token_secret};
97             }
98              
99             sub consumer_key {
100 0     0 1   my ($self) = @_;
101 0           return $self->{_consumer_key};
102             }
103              
104             sub consumer_secret {
105 0     0 1   my ($self) = @_;
106 0           return $self->{_consumer_secret};
107             }
108              
109             sub request_token {
110 0     0 1   my $self = shift;
111 0 0         if (@_) {
112 0           $self->{_request_token} = shift;
113             }
114 0           return $self->{_request_token};
115             }
116              
117             sub request_token_secret {
118 0     0 1   my $self = shift;
119 0 0         if (@_) {
120 0           $self->{_request_token_secret} = shift;
121             }
122 0           return $self->{_request_token_secret};
123             }
124              
125             sub get_authorization_url {
126 0     0 1   my ($self) = @_;
127              
128             # TODO: Get a request token first
129             # and then build the authorize URL
130 0           my $oauth_resp = $self->request_request_token();
131              
132 0           warn 'CONTENT=' . $oauth_resp;
133              
134 0           my $req_tok = $oauth_resp->{oauth_token};
135 0           my $req_tok_secret = $oauth_resp->{oauth_token_secret};
136              
137 0 0 0       if (! $req_tok || ! $req_tok_secret) {
138 0           Carp::croak("Couldn't get a valid request token from " . OAUTH_BASE_URL);
139             }
140              
141             # Store in the object for the access-token phase later
142 0           $self->request_token($req_tok);
143 0           $self->request_token_secret($req_tok_secret);
144              
145 0           return $self->oauth_url_for('authorize', oauth_token=> $req_tok);
146             }
147              
148             sub _do_oauth_request {
149 0     0     my ($self, $url) = @_;
150              
151 0           my $ua = $self->_user_agent();
152 0           my $resp = $ua->get($url);
153              
154 0 0         if ($resp->is_success) {
155 0           my $query = CGI->new($resp->content());
156             return {
157 0           ok => 1,
158             response => $resp,
159             content => $resp->content(),
160             data => { $query->Vars },
161             };
162             }
163              
164             return {
165 0           ok => 0,
166             response => $resp,
167             content => $resp->content(),
168             errstr => $resp->status_line(),
169             }
170              
171             }
172              
173             sub _user_agent {
174 0     0     my $ua = LWP::UserAgent->new();
175 0           return $ua;
176             }
177              
178             sub oauth_url_for {
179 0     0 1   my ($self, $step, %args) = @_;
180              
181 0           $step = lc $step;
182              
183 0           my $url = URI->new(OAUTH_BASE_URL . '/' . $step);
184 0           $url->query_form(%args);
185              
186 0           return $url;
187             }
188              
189             sub request_access_token {
190 0     0 1   my ($self, %args) = @_;
191              
192 0 0         if (! exists $args{verifier}) {
193 0           Carp::croak "The 'verifier' argument is required. Check the docs.";
194             }
195              
196 0           my $verifier = $args{verifier};
197              
198 0           my %opt = (
199             step => 'access_token',
200             request_method => 'GET',
201             request_url => $self->oauth_url_for('access_token'),
202             token => $self->request_token(),
203             token_secret => $self->request_token_secret(),
204             verifier => $verifier,
205             );
206              
207 0           my $request = $self->_prepare_request(%opt);
208 0 0         if (! $request) {
209 0           Carp::croak "Unable to initialize access-token request";
210             }
211              
212 0           my $access_token_url = $request->to_url();
213              
214             #print 'access_token_url:', $access_token_url, "\n";
215              
216 0           my $response = $self->_do_oauth_request($access_token_url);
217              
218             # Check if the request-token request failed
219 0 0 0       if (! $response || ref $response ne 'HASH' || $response->{ok} == 0) {
      0        
220 0           Carp::croak "Access-token request failed. Might be a temporary problem. Please retry later.";
221             }
222              
223 0           $response = $response->{data};
224              
225             # Store access token for future requests
226 0           $self->access_token($response->{oauth_token});
227 0           $self->access_token_secret($response->{oauth_token_secret});
228            
229             # And return them as well, so user can save them to persistent storage
230             return (
231 0           $response->{oauth_token},
232             $response->{oauth_token_secret}
233             );
234             }
235              
236             sub request_request_token {
237 0     0 1   my ($self) = @_;
238              
239 0           my %opt = (
240             step => 'request_token',
241             callback => 'oob',
242             request_method => 'GET',
243             request_url => $self->oauth_url_for('request_token'),
244             );
245              
246 0           my $request = $self->_prepare_request(%opt);
247 0 0         if (! $request) {
248 0           Carp::croak "Unable to initialize request-token request";
249             }
250              
251 0           my $request_token_url = $request->to_url();
252              
253 0           my $response = $self->_do_oauth_request($request_token_url);
254              
255             # Check if the request-token request failed
256 0 0 0       if (! $response || ref $response ne 'HASH' || $response->{ok} == 0) {
      0        
257 0           Carp::croak "Request-token request failed. Might be a temporary problem. Please retry later.";
258             }
259              
260 0           return $response->{data};
261             }
262              
263             sub _fill_default_values {
264 0     0     my ($self, $req) = @_;
265              
266 0   0       $req ||= {};
267              
268 0   0       $req->{step} ||= 'request_token';
269 0   0       $req->{nonce} ||= _random_string(32);
270 0   0       $req->{request_method} ||= 'GET';
271 0   0       $req->{consumer_key} ||= $self->consumer_key();
272 0   0       $req->{consumer_secret} ||= $self->consumer_secret();
273             # Opera OAuth provider supports only HMAC-SHA1
274 0           $req->{signature_method} = 'HMAC-SHA1';
275 0   0       $req->{timestamp} ||= time();
276 0           $req->{version} = '1.0';
277              
278 0           return $req;
279             }
280              
281             sub _prepare_request {
282 0     0     my ($self, %opt) = @_;
283              
284             # Fill in the default OAuth request values
285 0           $self->_fill_default_values(\%opt);
286              
287             # Use Net::OAuth to obtain a valid request object
288 0           my $step = delete $opt{step};
289 0           my $request = Net::OAuth->request($step)->new(%opt);
290              
291             # User authorization step doesn't need signing
292 0 0         if ($step ne 'user_auth') {
293 0           $request->sign;
294             }
295              
296 0           return $request;
297             }
298              
299             sub _random_string {
300 0     0     my ($length) = @_;
301 0 0         if (! $length) { $length = 16 }
  0            
302 0           my @chars = ('a'..'z','A'..'Z','0'..'9');
303 0           my $str = '';
304 0           for (1 .. $length) {
305 0           $str .= $chars[ int rand @chars ];
306             }
307 0           return $str;
308             }
309              
310             sub api_get_request {
311 0     0 0   my ($self, $datatype, @args) = @_;
312              
313 0           my $api_url = $self->api_url_for($datatype, @args);
314              
315 0           $api_url->query_form(
316             oauth_token => $self->access_token(),
317             api_output => 'json',
318             );
319              
320             #warn "api-url: $api_url\n";
321             #print 'acc-tok:', $self->access_token(), "\n";
322             #print 'acc-tok-sec:', $self->access_token_secret(), "\n";
323              
324 0           my %opt = (
325             step => 'protected_resource',
326             request_method => 'GET',
327             request_url => $api_url,
328             token => $self->access_token(),
329             token_secret => $self->access_token_secret(),
330             );
331              
332 0           my $request = $self->_prepare_request(%opt);
333 0 0         if (! $request) {
334 0           Carp::croak('Unable to initialize api request');
335             }
336              
337 0           my $oauth_url = $request->to_url();
338 0           my $response = $self->_do_oauth_request($oauth_url);
339              
340             #warn "api-url: $oauth_url\n";
341             #warn "response: " . Data::Dumper::Dumper($response) . "\n";
342              
343 0 0 0       if (! $response || ref $response ne 'HASH' || $response->{ok} == 0) {
      0        
344 0           $self->error($response->{status});
345 0           return;
346             }
347              
348             # Given a HTTP::Response, return the data hash
349 0           return $self->api_result($response->{response});
350             }
351              
352             sub error {
353 0     0 0   my $self = shift;
354              
355 0 0         if (@_) {
356 0           $self->{error} = shift;
357             }
358              
359 0           return $self->{error};
360             }
361              
362             sub _json_decoder {
363 0     0     state $json_obj = JSON::XS->new();
364 0           return $json_obj;
365             }
366              
367             sub api_result {
368 0     0 0   my ($self, $res) = @_;
369 0           my $json_str = $res->content;
370 0           my $json_obj = $self->_json_decoder();
371 0           return $json_obj->decode($json_str);
372             }
373              
374             sub api_url_for {
375 0     0 0   my ($self, @args) = @_;
376              
377 0           my $datatype = shift @args;
378 0           my $root_url = LINK_API_URL;
379 0           my $uri;
380              
381 0           $datatype = ucfirst lc $datatype;
382              
383             # Net::OperaLink + '::' + Bookmark/Speeddial/...
384 0           my $package = join('::', ref($self), $datatype);
385              
386             #warn "package=$package\n";
387             #warn "args=".join(',',@args)."\n";
388             #warn "api_url_for=" . $package->api_url_for(@args) . "\n";
389              
390             eval {
391 0           $uri = URI->new(
392             $root_url . "/" . $package->api_url_for(@args) . "/"
393             )
394 0 0         } or do {
395 0           Carp::croak("Unknown or unsupported datatype $datatype ?");
396             };
397              
398 0           return $uri;
399             }
400              
401             sub _datatype_query_node {
402 0     0     my ($self, $datatype, $id, $query_mode) = @_;
403              
404 0 0 0       if (not defined $id or not $id) {
405 0           $self->error("Incorrect API usage: $datatype(\$id) or $datatype(\$id, \$query_mode)");
406 0           return;
407             }
408              
409 0           return $self->api_get_request($datatype, $id);
410             }
411              
412             sub _datatype_query_subtree {
413 0     0     my ($self, $datatype, $query_mode) = @_;
414              
415 0   0       $query_mode ||= 'children';
416              
417 0           return $self->api_get_request($datatype, $query_mode);
418             }
419              
420             sub bookmark {
421 0     0 1   my ($self, $id, $query_mode) = @_;
422 0           return $self->_datatype_query_node('bookmark', $id, $query_mode);
423             }
424              
425             sub bookmarks {
426 0     0 1   my ($self, $query_mode) = @_;
427 0           return $self->_datatype_query_subtree('bookmark', $query_mode);
428             }
429              
430             sub note {
431 0     0 1   my ($self, $id, $query_mode) = @_;
432 0           return $self->_datatype_query_node('note', $id, $query_mode);
433             }
434              
435             sub notes {
436 0     0 1   my ($self, $query_mode) = @_;
437 0           return $self->_datatype_query_subtree('note', $query_mode);
438             }
439              
440             sub speeddial {
441 0     0 1   my ($self, $id, $query_mode) = @_;
442 0           return $self->_datatype_query_node('speeddial', $id, $query_mode);
443             }
444              
445             sub speeddials {
446 0     0 1   my ($self, $query_mode) = @_;
447 0           return $self->_datatype_query_subtree('speeddial', $query_mode);
448             }
449              
450             1;
451              
452             __END__