File Coverage

blib/lib/Flickr/API.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Flickr::API;
2              
3 14     14   477857 use strict;
  14         30  
  14         385  
4 14     14   72 use warnings;
  14         25  
  14         391  
5 14     14   14331 use LWP::UserAgent;
  14         742391  
  14         496  
6 14     14   11962 use XML::Parser::Lite::Tree;
  14         83898  
  14         467  
7 14     14   11955 use XML::LibXML::Simple;
  0            
  0            
8             use Flickr::API::Request;
9             use Flickr::API::Response;
10             use Net::OAuth;
11             use Digest::MD5 qw(md5_hex);
12             use Scalar::Util qw(blessed);
13             use Encode qw(encode_utf8);
14             use Carp;
15             use Storable qw(store_fd retrieve_fd);
16              
17             our @ISA = qw(LWP::UserAgent);
18              
19             our $VERSION = '1.26';
20              
21             sub new {
22             my $class = shift;
23             my $options = shift;
24              
25             my $self;
26             if ($options->{lwpobj}){
27             my $lwpobj = $options->{lwpobj};
28             if (defined($lwpobj)){
29             my $lwpobjtype = Scalar::Util::blessed($lwpobj);
30             if (defined($lwpobjtype)){
31             $self = $lwpobj;
32             @ISA = ($lwpobjtype);
33             }
34             }
35             }
36             $self = LWP::UserAgent->new unless $self;
37              
38             #
39             # If the options have consumer_key, handle as oauth
40             #
41             if (defined($options->{consumer_key})) {
42              
43             $self->{api_type} = 'oauth';
44             $self->{rest_uri} = $options->{rest_uri} || 'https://api.flickr.com/services/rest/';
45             $self->{auth_uri} = $options->{auth_uri} || 'https://api.flickr.com/services/oauth/authorize';
46              
47             if (defined($options->{consumer_secret})) {
48              
49             #
50             # for the flickr api object
51             #
52             $self->{oauth_request} = 'consumer';
53             $self->{consumer_key} = $options->{consumer_key};
54             $self->{consumer_secret} = $options->{consumer_secret};
55             $self->{unicode} = $options->{unicode} || 0;
56             #
57             # for Net::OAuth Consumer Requests
58             #
59             $self->{oauth}->{request_method} = $options->{request_method} || 'GET';
60             $self->{oauth}->{request_url} = $self->{rest_uri};
61             $self->{oauth}->{consumer_secret} = $options->{consumer_secret};
62             $self->{oauth}->{consumer_key} = $options->{consumer_key};
63             $self->{oauth}->{nonce} = $options->{nonce} || _make_nonce();
64             $self->{oauth}->{signature_method} = $options->{signature_method} ||'HMAC-SHA1';
65             $self->{oauth}->{timestamp} = $options->{timestamp} || time;
66             $self->{oauth}->{version} = '1.0';
67             $self->{oauth}->{callback} = $options->{callback};
68              
69             }
70             else {
71              
72             carp "OAuth calls must have at least a consumer_key and a consumer_secret";
73             $self->_set_status(0,"OAuth call without consumer_secret");
74              
75             }
76              
77             if (defined($options->{token}) && defined($options->{token_secret})) {
78              
79             #
80             # If we have token/token secret then we are for protected resources
81             #
82             $self->{oauth}->{token_secret} = $options->{token_secret};
83             $self->{oauth}->{token} = $options->{token};
84             $self->{oauth_request} = 'protected resource';
85              
86             }
87              
88             #
89             # Preserve request and access tokens
90             #
91             if (defined($options->{request_token}) and
92             ref($options->{request_token}) eq 'Net::OAuth::V1_0A::RequestTokenResponse') {
93              
94             $self->{oauth}->{request_token} = $options->{request_token};
95              
96             }
97             if (defined($options->{access_token}) and
98             ref($options->{access_token}) eq 'Net::OAuth::AccessTokenResponse') {
99              
100             $self->{oauth}->{access_token} = $options->{access_token};
101              
102             }
103             }
104              
105             else {
106              
107             $self->{api_type} = 'flickr';
108             $self->{api_key} = $options->{key};
109             $self->{api_secret} = $options->{secret};
110             $self->{rest_uri} = $options->{rest_uri} || 'https://api.flickr.com/services/rest/';
111             $self->{auth_uri} = $options->{auth_uri} || 'https://api.flickr.com/services/auth/';
112             $self->{unicode} = $options->{unicode} || 0;
113              
114             $self->{fauth}->{frob} = $options->{frob};
115             $self->{fauth}->{key} = $options->{key};
116             $self->{fauth}->{secret} = $options->{secret};
117             $self->{fauth}->{token} = $options->{token};
118              
119             carp "You must pass an API key or a Consumer key to the constructor" unless defined $self->{api_key};
120              
121             }
122              
123             eval {
124             require Compress::Zlib;
125              
126             $self->default_header('Accept-Encoding' => 'gzip');
127             };
128              
129             bless $self, $class;
130             $self->_clear_status();
131             $self->_initialize();
132             return $self;
133             }
134              
135              
136              
137             #
138             # Execution Methods
139             #
140              
141             sub execute_method {
142             my ($self, $method, $args) = @_;
143             my $request;
144              
145             if ($self->is_oauth) {
146              
147             #
148             # Consumer Request Params
149             #
150             my $oauth = {};
151              
152             $oauth->{nonce} = _make_nonce();
153             $oauth->{consumer_key} = $self->{oauth}->{consumer_key};
154             $oauth->{consumer_secret} = $self->{oauth}->{consumer_secret};
155             $oauth->{timestamp} = time;
156             $oauth->{signature_method} = $self->{oauth}->{signature_method};
157             $oauth->{version} = $self->{oauth}->{version};
158              
159             if (defined($args->{'token'}) or defined($args->{'token_secret'})) {
160              
161             carp "\ntoken and token_secret must be specified in Flickr::API->new() and are being discarded\n";
162             undef $args->{'token'};
163             undef $args->{'token_secret'};
164             }
165              
166             if (defined($args->{'consumer_key'}) or defined($args->{'consumer_secret'})) {
167              
168             carp "\nconsumer_key and consumer_secret must be specified in Flickr::API->new() and are being discarded\n";
169             undef $args->{'consumer_key'};
170             undef $args->{'consumer_secret'};
171             }
172              
173              
174             $oauth->{extra_params} = $args;
175             $oauth->{extra_params}->{method} = $method;
176              
177             #
178             # Protected resource params
179             #
180             if (defined($self->{oauth}->{token})) {
181              
182             $oauth->{token} = $self->{oauth}->{token};
183             $oauth->{token_secret} = $self->{oauth}->{token_secret};
184              
185             }
186              
187             $request = Flickr::API::Request->new({
188             'api_type' => 'oauth',
189             'method' => $method,
190             'args' => $oauth,
191             'rest_uri' => $self->{rest_uri},
192             'unicode' => $self->{unicode},
193             });
194             }
195             else {
196              
197             $request = Flickr::API::Request->new({
198             'api_type' => 'flickr',
199             'method' => $method,
200             'args' => $args,
201             'rest_uri' => $self->{rest_uri},
202             'unicode' => $self->{unicode},
203             });
204             }
205              
206             return $self->execute_request($request);
207              
208             }
209              
210             sub execute_request {
211             my ($self, $request) = @_;
212              
213             $request->{api_args}->{method} = $request->{api_method};
214              
215             unless ($self->is_oauth) { $request->{api_args}->{api_key} = $self->{api_key}; }
216              
217             if (defined($self->{api_secret}) && length($self->{api_secret})) {
218              
219             unless ($self->is_oauth) { $request->{api_args}->{api_sig} = $self->_sign_args($request->{api_args}); }
220              
221             }
222              
223             unless ($self->is_oauth) { $request->encode_args(); }
224              
225             my $response = $self->request($request);
226             bless $response, 'Flickr::API::Response';
227              
228             $response->init_flickr();
229              
230             if ($response->{_rc} != 200){
231             $response->set_fail(0, "API returned a non-200 status code ($response->{_rc})");
232             return $response;
233             }
234              
235             my $content = $response->decoded_content();
236             $content = $response->content() unless defined $content;
237              
238             my $xls = XML::LibXML::Simple->new(ForceArray => 0);
239             my $tree = XML::Parser::Lite::Tree::instance()->parse($content);
240              
241             my $hashref = $xls->XMLin($content,KeyAttr => []);
242              
243             my $rsp_node = $self->_find_tag($tree->{children});
244              
245             if ($rsp_node->{name} ne 'rsp'){
246             $response->set_fail(0, "API returned an invalid response");
247             return $response;
248             }
249              
250             if ($rsp_node->{attributes}->{stat} eq 'fail'){
251             my $fail_node = $self->_find_tag($rsp_node->{children});
252             if ($fail_node->{name} eq 'err'){
253             $response->set_fail($fail_node->{attributes}->{code}, $fail_node->{attributes}->{msg});
254             }
255             else {
256             $response->set_fail(0, "Method failed but returned no error code");
257             }
258             return $response;
259             }
260              
261             if ($rsp_node->{attributes}->{stat} eq 'ok'){
262             $response->set_ok($rsp_node,$hashref);
263             return $response;
264             }
265              
266             $response->set_fail(0, "API returned an invalid status code");
267             return $response;
268             }
269              
270              
271             #
272             # Persistent config methods
273             #
274              
275              
276             #
277             # Method to return hash of important Flickr or OAuth parameters.
278             # OAuth can also export meaningful subsets of parameters based
279             # on OAuth message type.
280             #
281             sub export_config {
282             my $self = shift;
283             my $type = shift;
284             my $params = shift;
285              
286             if ($self->is_oauth) {
287              
288             unless($params) { $params='do_it'; }
289              
290             my %oauth;
291              
292             if (defined($type)) {
293             if ($params =~ m/^m.*/i) {
294             %oauth = map { ($_) => undef } @{Net::OAuth->request($type)->all_message_params()};
295             }
296             elsif ($params =~ m/^a.*/i) {
297             %oauth = map { ($_) => undef } @{Net::OAuth->request($type)->all_api_params()};
298             }
299             else {
300             %oauth = map { ($_) => undef } @{Net::OAuth->request($type)->all_params()};
301             }
302             foreach my $param (keys %oauth) {
303             if (defined ($self->{oauth}->{$param})) { $oauth{$param} = $self->{oauth}->{$param}; }
304             }
305             return %oauth;
306             }
307             else {
308             return %{$self->{oauth}};
309             }
310             }
311             else {
312             return %{$self->{fauth}};
313             }
314              
315             }
316              
317             #
318             # Use perl core Storable to save important parameters.
319             #
320             sub export_storable_config {
321              
322             my $self = shift;
323             my $file = shift;
324              
325             open my $EXPORT, '>', $file or croak "\nCannot open $file for write: $!\n";
326             my %config = $self->export_config();
327             store_fd(\%config, $EXPORT);
328             close $EXPORT;
329             return;
330             }
331              
332             #
333             # Use perl core Storable for re-vivifying an API object from saved parameters
334             #
335             sub import_storable_config {
336              
337             my $class = shift;
338             my $file = shift;
339              
340             open my $IMPORT, '<', $file or croak "\nCannot open $file for read: $!\n";
341             my $config_ref = retrieve_fd($IMPORT);
342             close $IMPORT;
343             my $api = $class->new($config_ref);
344             return $api;
345             }
346              
347              
348              
349             #
350             # Preauthorization Methods
351             #
352             # Handle request token requests (process: REQUEST TOKEN, authorize, access token)
353             #
354             sub oauth_request_token {
355              
356             my $self = shift;
357             my $options = shift;
358             my %args = %{$self->{oauth}};
359              
360             unless ($self->is_oauth) {
361             carp "\noauth_request_token called for Non-OAuth Flickr::API object\n";
362             return undef;
363             }
364             unless ($self->get_oauth_request_type() eq 'consumer') {
365             croak "\noauth_request_token called using protected resource Flickr::API object\n";
366             }
367              
368             $self->{oauth_request} = 'Request Token';
369             $args{request_url} = $options->{request_token_url} || 'https://api.flickr.com/services/oauth/request_token';
370             $args{callback} = $options->{callback} || 'https://127.0.0.1';
371              
372             $Net::OAuth::PROTOCOL_VERSION = Net::OAuth::PROTOCOL_VERSION_1_0A;
373              
374             my $request = Net::OAuth->request('Request Token')->new(%args);
375              
376             $request->sign;
377              
378             my $response = $self->get($request->to_url);
379              
380             my $content = $response->decoded_content();
381             $content = $response->content() unless defined $content;
382              
383             if ($content =~ m/^oauth_problem=(.+)$/) {
384              
385             carp "\nRequest token not granted: '",$1,"'\n";
386             $self->{oauth}->{request_token} = $1;
387             return $1;
388             }
389              
390             $self->{oauth}->{request_token} = Net::OAuth->response('request token')->from_post_body($content);
391             $self->{oauth}->{callback} = $args{callback};
392             return 'ok';
393             }
394              
395              
396             #
397             # Participate in authorization (process: request token, AUTHORIZE, access token)
398             #
399             sub oauth_authorize_uri {
400              
401             my $self = shift;
402             my $options = shift;
403              
404             unless ($self->is_oauth) {
405             carp "oauth_authorize_uri called for Non-OAuth Flickr::API object";
406             return undef;
407             }
408             my %args = %{$self->{oauth}};
409              
410             $self->{oauth_request} = 'User Authentication';
411             $args{perms} = lc($options->{perms}) || 'read';
412              
413             carp "\nThe 'perms' parameter must be one of: read, write, delete\n"
414             and return unless defined($args{perms}) && $args{perms} =~ /^(read|write|delete)$/;
415              
416             $Net::OAuth::PROTOCOL_VERSION = Net::OAuth::PROTOCOL_VERSION_1_0A;
417              
418             return $self->{auth_uri} .
419             '?oauth_token=' . $args{'request_token'}{'token'} .
420             '&perms=' . $args{perms};
421              
422             }
423              
424             #
425             # flickr preauthorization
426             #
427              
428             sub request_auth_url {
429             my $self = shift;
430             my $perms = shift;
431             my $frob = shift;
432              
433             if ($self->is_oauth) {
434              
435             carp "request_auth_url called for an OAuth instantiated Flickr::API";
436             return undef;
437              
438             }
439              
440             $perms = lc($perms);
441              
442             carp "\nThe 'perms' parameter must be one of: read, write, delete\n"
443             and return unless defined($perms) && $perms =~ /^(read|write|delete)$/;
444              
445             return undef unless defined $self->{api_secret} && length $self->{api_secret};
446              
447             my %args = (
448             'api_key' => $self->{api_key},
449             'perms' => $perms
450             );
451              
452             if ($frob) {
453             $args{frob} = $frob;
454             }
455              
456             my $sig = $self->_sign_args(\%args);
457             $args{api_sig} = $sig;
458              
459             my $uri = URI->new($self->{auth_uri});
460             $uri->query_form(%args);
461              
462             return $uri;
463             }
464              
465              
466             #
467             # Access Token (post authorization) Methods
468             #
469             # Handle access token requests (process: request token, authorize, ACCESS TOKEN)
470             #
471             sub oauth_access_token {
472              
473             my $self = shift;
474             my $options = shift;
475              
476             unless ($self->is_oauth) {
477             carp "oauth_access_token called for Non-OAuth Flickr::API object";
478             return undef;
479             }
480             if ($options->{token} ne $self->{oauth}->{request_token}->{token}) {
481              
482             carp "Request token in API does not match token for access token request";
483             return undef;
484              
485             }
486              
487             #
488             # Stuff the values for the Net::OAuth factory
489             #
490             $self->{oauth}->{verifier} = $options->{verifier};
491             $self->{oauth}->{token} = $options->{token};
492             $self->{oauth}->{token_secret} = $self->{oauth}->{request_token}->{token_secret};
493              
494             my %args = %{$self->{oauth}};
495              
496             $args{request_url} = $options->{access_token_url} || 'https://api.flickr.com/services/oauth/access_token';
497              
498             $Net::OAuth::PROTOCOL_VERSION = Net::OAuth::PROTOCOL_VERSION_1_0A;
499              
500             my $request = Net::OAuth->request('Access Token')->new(%args);
501              
502             $request->sign;
503              
504             my $response = $self->get($request->to_url);
505              
506             my $content = $response->decoded_content();
507             $content = $response->content() unless defined $content;
508              
509             if ($content =~ m/^oauth_problem=(.+)$/) {
510              
511             carp "\nAccess token not granted: '",$1,"'\n";
512             $self->{oauth}->{access_token} = $1;
513              
514             delete $self->{oauth}->{token}; # Not saving problematic request token
515             delete $self->{oauth}->{token_secret}; # token secret
516             delete $self->{oauth}->{verifier}; # and verifier copies
517              
518             return $1;
519              
520             }
521              
522             $self->{oauth}->{access_token} = Net::OAuth->response('access token')->from_post_body($content);
523             $self->{oauth}->{token} = $self->{oauth}->{access_token}->token();
524             $self->{oauth}->{token_secret} = $self->{oauth}->{access_token}->token_secret();
525              
526             delete $self->{oauth}->{request_token}; #No longer valid, anyway
527             delete $self->{oauth}->{verifier};
528              
529             return 'ok';
530              
531             }
532              
533              
534              
535             sub flickr_access_token {
536             my $self = shift;
537             my $frob = shift;
538              
539             my $rsp = $self->execute_method('flickr.auth.getToken', {api_key => $self->{api_key}, frob => $frob });
540             my $response_ref = $rsp->as_hash();
541              
542             $self->{fauth}->{frob} = $frob;
543              
544             $self->{token} = $response_ref->{auth}->{token};
545             $self->{fauth}->{token} = $response_ref->{auth}->{token};
546              
547             $self->{fauth}->{user} = $response_ref->{auth}->{user};
548              
549             return $response_ref->{stat};
550              
551             }
552              
553              
554             #
555             # Utility methods
556             #
557              
558              
559             sub is_oauth {
560             my $self = shift;
561             if (defined $self->{api_type} and $self->{api_type} eq 'oauth') {
562             return 1;
563             }
564             else {
565             return 0;
566             }
567             }
568              
569              
570             sub get_oauth_request_type {
571             my $self = shift;
572              
573             if (defined $self->{api_type} and $self->{api_type} eq 'oauth') {
574             return $self->{oauth_request};
575             }
576             else {
577             return undef;
578             }
579             }
580              
581             sub api_success {
582             my $self = shift;
583              
584             return $self->{flickr}->{status}->{api_success};
585              
586             }
587             sub api_message {
588             my $self = shift;
589              
590             return $self->{flickr}->{status}->{api_message};
591             }
592              
593              
594             #
595             # Private methods
596             #
597              
598             sub _sign_args {
599             my $self = shift;
600             my $args = shift;
601              
602             if ($self->is_oauth) {
603              
604             carp "_sign_args called for an OAuth instantiated Flickr::API";
605             return undef;
606              
607             }
608              
609             my $sig = $self->{api_secret};
610              
611             foreach my $key (sort {$a cmp $b} keys %{$args}) {
612              
613             my $value = (defined($args->{$key})) ? $args->{$key} : "";
614             $sig .= $key . $value;
615             }
616              
617             return md5_hex(encode_utf8($sig)) if $self->{unicode};
618             return md5_hex($sig);
619             }
620              
621             sub _find_tag {
622             my ($self, $children) = @_;
623             for my $child(@{$children}){
624             return $child if $child->{type} eq 'element';
625             }
626             return {};
627             }
628              
629             sub _make_nonce {
630              
631             return md5_hex(rand);
632              
633             }
634             sub _export_api {
635             my $self = shift;
636             my $api = {};
637              
638             $api->{oauth} = $self->{oauth};
639             $api->{fauth} = $self->{fauth};
640             $api->{flickr} = $self->{flickr};
641              
642             $api->{api_type} = $self->{api_type};
643             $api->{api_key} = $self->{api_key};
644             $api->{api_secret} = $self->{api_secret};
645             $api->{rest_uri} = $self->{rest_uri};
646             $api->{unicode} = $self->{unicode};
647             $api->{auth_uri} = $self->{auth_uri};
648              
649             return $api;
650             }
651              
652              
653             sub _initialize {
654              
655             my $self = shift;
656             $self->_set_status(1,'Base API initialized');
657              
658             }
659              
660             sub _full_status {
661              
662             my $self = shift;
663             return $self->{flickr}->{status};
664             }
665              
666             sub _clear_status {
667              
668             my $self = shift;
669              
670             # the API status
671             $self->_set_status(1,'');
672             # the propagated response status
673             $self->{flickr}->{status}->{_rc} = 0;
674             $self->{flickr}->{status}->{success} = 1; # initialize as successful
675             $self->{flickr}->{status}->{error_code} = 0;
676             $self->{flickr}->{status}->{error_message} = '';
677              
678             return;
679              
680             }
681              
682             sub _set_status {
683              
684             my $self = shift;
685             my $good = shift;
686             my $msg = shift;
687              
688             if ($good != 0) { $good = 1; }
689              
690             $self->{flickr}->{status}->{api_success} = $good;
691             $self->{flickr}->{status}->{api_message} = $msg;
692              
693             return;
694             }
695              
696              
697              
698             1;
699              
700             __END__