File Coverage

blib/lib/Net/Jifty.pm
Criterion Covered Total %
statement 155 269 57.6
branch 42 98 42.8
condition 10 36 27.7
subroutine 25 38 65.7
pod 30 30 100.0
total 262 471 55.6


line stmt bran cond sub pod time code
1             package Net::Jifty;
2 7     7   390389 use Any::Moose;
  7         51103  
  7         46  
3              
4             our $VERSION = '0.14';
5              
6 7     7   16817 use LWP::UserAgent;
  7         484053  
  7         241  
7 7     7   93 use URI;
  7         15  
  7         163  
8              
9 7     7   5747 use YAML;
  7         61518  
  7         469  
10              
11 7     7   26620 use Encode;
  7         103534  
  7         674  
12 7     7   107 use Fcntl qw(:mode);
  7         14  
  7         31774  
13              
14             has site => (
15             is => 'rw',
16             isa => 'Str',
17             required => 1,
18             documentation => "The URL of your application",
19             trigger => sub {
20             # this canonicalizes localhost to 127.0.0.1 because of an (I think)
21             # HTTP::Cookies bug. cookies aren't sent out for localhost.
22             my ($self, $site) = @_;
23              
24             if ($site =~ s/\blocalhost\b/127.0.0.1/) {
25             $self->site($site);
26             }
27             },
28             );
29              
30             has cookie_name => (
31             is => 'rw',
32             isa => 'Str',
33             required => 1,
34             documentation => "The name of the session ID cookie. This can be found in your config under Framework/Web/SessinCookieName",
35             );
36              
37             has appname => (
38             is => 'rw',
39             isa => 'Str',
40             documentation => "The name of the application, as it is known to Jifty",
41             );
42              
43             has email => (
44             is => 'rw',
45             isa => 'Str',
46             documentation => "The email address to use to log in",
47             );
48              
49             has password => (
50             is => 'rw',
51             isa => 'Str',
52             documentation => "The password to use to log in",
53             );
54              
55             has sid => (
56             is => 'rw',
57             isa => 'Str',
58             documentation => "The session ID, from the cookie_name cookie. You can use this to bypass login",
59             trigger => sub {
60             my $self = shift;
61              
62             my $uri = URI->new($self->site);
63             $self->ua->cookie_jar->set_cookie(0, $self->cookie_name,
64             $self->sid, '/',
65             $uri->host, $uri->port,
66             0, 0, undef, 1);
67             },
68             );
69              
70             has ua => (
71             is => 'rw',
72             isa => 'LWP::UserAgent',
73             default => sub {
74             my $args = shift;
75              
76             my $ua = LWP::UserAgent->new;
77              
78             $ua->cookie_jar({});
79             push @{ $ua->requests_redirectable }, qw( POST PUT DELETE );
80              
81             # Load the user's proxy settings from %ENV
82             $ua->env_proxy;
83              
84             return $ua;
85             },
86             );
87              
88             has config_file => (
89             is => 'rw',
90             isa => 'Str',
91             default => "$ENV{HOME}/.jifty",
92             predicate => 'has_config_file',
93             documentation => "The place to look for the user's config file",
94             );
95              
96             has use_config => (
97             is => 'rw',
98             isa => 'Bool',
99             default => 0,
100             documentation => "Whether or not to use the user's config",
101             );
102              
103             has config => (
104             is => 'rw',
105             isa => 'HashRef',
106             default => sub { {} },
107             documentation => "Storage for the user's config",
108             );
109              
110             has use_filters => (
111             is => 'rw',
112             isa => 'Bool',
113             default => 1,
114             documentation => "Whether or not to use config files in the user's directory tree",
115             );
116              
117             has filter_file => (
118             is => 'rw',
119             isa => 'Str',
120             default => ".jifty",
121             documentation => "The filename to look for in each parent directory",
122             );
123              
124             has strict_arguments => (
125             is => 'rw',
126             isa => 'Bool',
127             default => 0,
128             documentation => "Check to make sure mandatory arguments are provided, and no unknown arguments are included",
129             );
130              
131             has action_specs => (
132             is => 'rw',
133             isa => 'HashRef',
134             default => sub { {} },
135             documentation => "The cache for action specifications",
136             );
137              
138             has model_specs => (
139             is => 'rw',
140             isa => 'HashRef',
141             default => sub { {} },
142             documentation => "The cache for model specifications",
143             );
144              
145             sub BUILD {
146 9     9 1 3031 my $self = shift;
147              
148 9 50 33     91 $self->load_config
149             if $self->use_config && $self->has_config_file;
150              
151 9 100       122 $self->login
152             unless $self->sid;
153             }
154              
155             sub login {
156 7     7 1 17 my $self = shift;
157              
158 7 50       33 return if $self->sid;
159              
160 7 50 33     84 confess "Unable to log in without an email and password."
161             unless $self->email && $self->password;
162              
163 7 50       58 confess 'Your email did not contain an "@" sign. Did you accidentally use double quotes?'
164             if $self->email !~ /@/;
165              
166 7         64 my $result = $self->call(Login =>
167             address => $self->email,
168             password => $self->password);
169              
170 7 50       138547 confess "Unable to log in."
171             if $result->{failure};
172              
173 7         50 $self->get_sid;
174 7         2523 return 1;
175             }
176              
177             sub call {
178 7     7 1 15 my $self = shift;
179 7         12 my $action = shift;
180 7         32 my %args = @_;
181 7         18 my $moniker = 'fnord';
182              
183 14         173 my $res = $self->ua->post(
184             $self->site . "/__jifty/webservices/yaml",
185             { "J:A-$moniker" => $action,
186 7         67 map { ( "J:A:F-$_-$moniker" => $args{$_} ) } keys %args
187             }
188             );
189              
190 7 50       641 if ( $res->is_success ) {
191 7         386 return YAML::Load( Encode::decode_utf8($res->content) )->{$moniker};
192             } else {
193 0         0 confess $res->status_line;
194             }
195             }
196              
197             sub form_url_encoded_args {
198 8     8 1 12 my $self = shift;
199              
200 8         16 my $uri = '';
201 8         34 while (my ($key, $value) = splice @_, 0, 2) {
202 12         21 $uri .= join('=', map { $self->escape($_) } $key, $value) . '&';
  24         41  
203             }
204 8         17 chop $uri;
205              
206 8         33 return $uri;
207             }
208              
209             sub form_form_data_args {
210 5     5 1 7 my $self = shift;
211              
212 5         5 my @res;
213 5         18 while (my ($key, $value) = splice @_, 0, 2) {
214 6         89 my $disposition = 'form-data; name="'. Encode::encode( 'MIME-Q', $key ) .'"';
215 6 100       7846 unless ( ref $value ) {
216 1         7 push @res, HTTP::Message->new(
217             ['Content-Disposition' => $disposition ],
218             $value,
219             );
220 1         60 next;
221             }
222            
223 5 50       14 if ( $value->{'filename'} ) {
224 5         16 $value->{'filename'} = Encode::encode( 'MIME-Q', $value->{'filename'} );
225 5         352 $disposition .= '; filename="'. delete ( $value->{'filename'} ) .'"';
226             }
227 5   100     44 push @res, HTTP::Message->new(
228             [
229             'Content-Type' => $value->{'content_type'} || 'application/octet-stream',
230             'Content-Disposition' => $disposition,
231             ],
232             delete $value->{content},
233             );
234             }
235 5         366 return @res;
236             }
237              
238             sub method {
239 37     37 1 55 my $self = shift;
240 37         78 my $method = lc(shift);
241 37         52 my $url = shift;
242 37         71 my @args = @_;
243              
244 37 100       239 $url = $self->join_url(@$url)
245             if ref($url) eq 'ARRAY';
246              
247             # remove trailing /
248 37         146 $url =~ s{/+$}{};
249              
250 37         181 my $uri = $self->site . '/=/' . $url . '.yml';
251              
252 37         61 my $res;
253              
254 37 100 66     206 if ($method eq 'get' || $method eq 'head') {
255 26 100       87 $uri .= '?' . $self->form_url_encoded_args(@args)
256             if @args;
257              
258 26         202 $res = $self->ua->$method($uri);
259             }
260             else {
261 11         82 my $req = HTTP::Request->new(
262             uc($method) => $uri,
263             );
264              
265 11 100       1314 if (@args) {
266 10 100       44 if ( grep ref $_, @args ) {
267 5         23 $req->header('Content-type' => 'multipart/form-data');
268 5         240 $req->add_part( $_ ) foreach $self->form_form_data_args(@args);
269             } else {
270 5         43 $req->header('Content-type' => 'application/x-www-form-urlencoded');
271 5         347 $req->content( $self->form_url_encoded_args(@args) );
272             }
273             }
274              
275 11         2710 $res = $self->ua->request($req);
276              
277             # XXX Compensation for a bug in Jifty::Plugin::REST... it doesn't
278             # remember to add .yml when redirecting after an update, so we will
279             # try to do that ourselves... fixed in a Jifty coming to stores near
280             # you soon!
281 11 50 33     627 if ($res->is_success && $res->content_type eq 'text/html') {
282 0         0 $req = $res->request->clone;
283 0         0 $req->uri($req->uri . '.yml');
284 0         0 $res = $self->ua->request($req);
285             }
286             }
287              
288 37 50       2893 if ($res->is_success) {
289 37         1811 return YAML::Load( Encode::decode_utf8($res->content) );
290             } else {
291 0         0 confess $res->status_line;
292             }
293             }
294              
295             sub post {
296 9     9 1 40573 my $self = shift;
297 9         37 $self->method('post', @_);
298             }
299              
300             sub get {
301 26     26 1 68914 my $self = shift;
302 26         100 $self->method('get', @_);
303             }
304              
305             sub act {
306 2     2 1 15135 my $self = shift;
307 2         50 my $action = shift;
308              
309 2 100       332 $self->validate_action_args($action => @_)
310             if $self->strict_arguments;
311              
312 1         8 return $self->post(["action", $action], @_);
313             }
314              
315             sub create {
316 3     3 1 5496 my $self = shift;
317 3         4 my $model = shift;
318              
319 3 100       27 $self->validate_action_args([create => $model] => @_)
320             if $self->strict_arguments;
321              
322 3         22 return $self->post(["model", $model], @_);
323             }
324              
325             sub delete {
326 1     1 1 8225 my $self = shift;
327 1         3 my $model = shift;
328 1         1 my $key = shift;
329 1         2 my $value = shift;
330              
331 1 50       7 $self->validate_action_args([delete => $model] => $key => $value)
332             if $self->strict_arguments;
333              
334 1         7 return $self->method(delete => ["model", $model, $key, $value]);
335             }
336              
337             sub update {
338 1     1 1 7336 my $self = shift;
339 1         3 my $model = shift;
340 1         3 my $key = shift;
341 1         2 my $value = shift;
342              
343 1 50       9 $self->validate_action_args([update => $model] => $key => $value, @_)
344             if $self->strict_arguments;
345              
346 1         6 return $self->method(put => ["model", $model, $key, $value], @_);
347             }
348              
349             sub read {
350 1     1 1 8337 my $self = shift;
351 1         3 my $model = shift;
352 1         2 my $key = shift;
353 1         3 my $value = shift;
354              
355 1         7 return $self->get(["model", $model, $key, $value]);
356             }
357              
358             sub search {
359 9     9 1 71709 my $self = shift;
360 9         19 my $model = shift;
361 9         13 my @args;
362              
363 9         46 while (@_) {
364 26 100       62 if (@_ == 1) {
365 6         21 push @args, shift;
366             }
367             else {
368             # id => [1,2,3] maps to id/1/id/2/id/3
369 20 100       51 if (ref($_[1]) eq 'ARRAY') {
370 7         13 push @args, map { $_[0] => $_ } @{ $_[1] };
  14         34  
  7         19  
371 7         22 splice @_, 0, 2;
372             }
373             else {
374 13         44 push @args, splice @_, 0, 2;
375             }
376             }
377             }
378              
379 9         53 return $self->get(["search", $model, @args]);
380             }
381              
382             sub validate_action_args {
383 7     7 1 3605 my $self = shift;
384 7         11 my $action = shift;
385 7         23 my %args = @_;
386              
387 7         11 my $name;
388 7 100       23 if (ref($action) eq 'ARRAY') {
389 5         13 my ($operation, $model) = @$action;
390              
391             # drop MyApp::Model::
392 5         29 $model =~ s/.*:://;
393              
394 5 50       28 confess "Invalid model operation: $operation. Expected 'create', 'update', or 'delete'." unless $operation =~ m{^(?:create|update|delete)$}i;
395              
396 5         22 $name = ucfirst(lc $operation) . $model;
397             }
398             else {
399 2         5 $name = $action;
400             }
401              
402 7         28 my $action_spec = $self->get_action_spec($name);
403              
404 7         32 for my $arg (keys %$action_spec) {
405 14 100 100     329 confess "Mandatory argument '$arg' not given for action $name."
406             if $action_spec->{$arg}{mandatory} && !defined($args{$arg});
407 13         30 delete $args{$arg};
408             }
409              
410 6 100       19 if (keys %args) {
411 2         367 confess "Unknown arguments given for action $name: "
412             . join(', ', keys %args);
413             }
414              
415 4         12 return 1;
416             }
417              
418             sub get_action_spec {
419 7     7 1 12 my $self = shift;
420 7         12 my $name = shift;
421              
422 7 100       34 unless ($self->action_specs->{$name}) {
423 6         24 $self->action_specs->{$name} = $self->get("action/$name");
424             }
425              
426 7         11900 return $self->action_specs->{$name};
427             }
428              
429             sub get_model_spec {
430 0     0 1 0 my $self = shift;
431 0         0 my $name = shift;
432              
433 0 0       0 unless ($self->model_specs->{$name}) {
434 0         0 $self->model_specs->{$name} = $self->get("model/$name");
435             }
436              
437 0         0 return $self->model_specs->{$name};
438             }
439              
440             sub get_sid {
441 0     0 1 0 my $self = shift;
442 0         0 my $cookie = $self->cookie_name;
443              
444 0         0 my $sid;
445 0 0       0 $sid = $1
446             if $self->ua->cookie_jar->as_string =~ /\Q$cookie\E=([^;]+)/;
447              
448 0         0 $self->sid($sid);
449             }
450              
451             sub join_url {
452 27     27 1 43 my $self = shift;
453              
454 27         102 return join '/', map { $self->escape($_) } grep { defined } @_
  122         249  
  122         258  
455             }
456              
457             sub escape {
458 146     146 1 146 my $self = shift;
459              
460 146         824 return map { s/([^a-zA-Z0-9_.!~*'()-])/uc sprintf("%%%02X", ord $1)/eg; $_ }
  34         153  
  146         445  
  146         573  
461 146         180 map { Encode::encode_utf8($_) }
462             @_
463             }
464              
465             sub load_date {
466 0     0 1 0 my $self = shift;
467 0         0 my $ymd = shift;
468              
469 0 0       0 my ($y, $m, $d) = $ymd =~ /^(\d\d\d\d)-(\d\d)-(\d\d)(?: 00:00:00)?$/
470             or confess "Invalid date passed to load_date: $ymd. Expected yyyy-mm-dd.";
471              
472 0         0 require DateTime;
473 0         0 return DateTime->new(
474             time_zone => 'floating',
475             year => $y,
476             month => $m,
477             day => $d,
478             );
479             }
480              
481             sub email_eq {
482 0     0 1 0 my $self = shift;
483 0         0 my $a = shift;
484 0         0 my $b = shift;
485              
486             # if one's defined and the other isn't, return 0
487 0 0       0 return 0 unless (defined $a ? 1 : 0)
    0          
    0          
488             == (defined $b ? 1 : 0);
489              
490 0 0 0     0 return 1 if !defined($a) && !defined($b);
491              
492             # so, both are defined
493              
494 0         0 require Email::Address;
495              
496 0         0 for ($a, $b) {
497 0 0 0     0 $_ = 'nobody@localhost' if $_ eq 'nobody' || //;
498 0         0 my ($email) = Email::Address->parse($_);
499 0         0 $_ = lc($email->address);
500             }
501              
502 0         0 return $a eq $b;
503             }
504              
505             sub is_me {
506 0     0 1 0 my $self = shift;
507 0         0 my $email = shift;
508              
509 0 0       0 return 0 if !defined($email);
510              
511 0         0 return $self->email_eq($self->email, $email);
512             }
513              
514             sub load_config {
515 0     0 1 0 my $self = shift;
516              
517 0         0 $self->config_permissions;
518 0         0 $self->read_config_file;
519              
520             # allow config to override everything. this may need to be less free in
521             # the future
522 0         0 while (my ($key, $value) = each %{ $self->config }) {
  0         0  
523 0 0       0 $self->$key($value)
524             if $self->can($key);
525             }
526              
527             $self->prompt_login_info
528 0 0 0     0 unless $self->config->{email} || $self->config->{sid};
529              
530             # update config if we are logging in manually
531 0 0       0 unless ($self->config->{sid}) {
532              
533             # if we have user/pass in the config then we still need to log in here
534 0 0       0 unless ($self->sid) {
535 0         0 $self->login;
536             }
537              
538             # now write the new config
539 0         0 $self->config->{sid} = $self->sid;
540 0         0 $self->write_config_file;
541             }
542              
543 0         0 return $self->config;
544             }
545              
546             sub config_permissions {
547 0     0 1 0 my $self = shift;
548 0         0 my $file = $self->config_file;
549              
550 0 0       0 return if $^O eq 'MSWin32';
551 0 0       0 return unless -e $file;
552 0         0 my @stat = stat($file);
553 0         0 my $mode = $stat[2];
554 0 0 0     0 if ($mode & S_IRGRP || $mode & S_IROTH) {
555 0         0 warn "Config file $file is readable by users other than you, fixing.";
556 0         0 chmod 0600, $file;
557             }
558             }
559              
560             sub read_config_file {
561 0     0 1 0 my $self = shift;
562 0         0 my $file = $self->config_file;
563              
564 0 0       0 return unless -e $file;
565              
566 0   0     0 $self->config(YAML::LoadFile($self->config_file) || {});
567              
568 0 0       0 if ($self->config->{site}) {
569             # Somehow, localhost gets normalized to localhost.localdomain,
570             # and messes up HTTP::Cookies when we try to set cookies on
571             # localhost, since it doesn't send them to
572             # localhost.localdomain.
573 0         0 $self->config->{site} =~ s/localhost/127.0.0.1/;
574             }
575             }
576              
577             sub write_config_file {
578 0     0 1 0 my $self = shift;
579 0         0 my $file = $self->config_file;
580              
581 0         0 YAML::DumpFile($file, $self->config);
582 0         0 chmod 0600, $file;
583             }
584              
585             sub prompt_login_info {
586 0     0 1 0 my $self = shift;
587              
588 0         0 print << "END_WELCOME";
589 0         0 Before we get started, please enter your @{[ $self->site ]}
  0         0  
590             username and password.
591              
592             This information will be stored in @{[ $self->config_file ]},
593             should you ever need to change it.
594              
595             END_WELCOME
596              
597 0         0 local $| = 1; # Flush buffers immediately
598              
599 0         0 while (1) {
600 0         0 print "First, what's your email address? ";
601 0         0 $self->config->{email} = ;
602 0         0 chomp($self->config->{email});
603              
604             my $read_mode = eval {
605             require Term::ReadKey;
606             \&Term::ReadKey::ReadMode;
607 0   0 0   0 } || sub {};
  0         0  
608              
609 0         0 print "And your password? ";
610 0         0 $read_mode->('noecho');
611 0         0 $self->config->{password} = ;
612 0         0 chomp($self->config->{password});
613 0         0 $read_mode->('restore');
614              
615 0         0 print "\n";
616              
617 0         0 $self->email($self->config->{email});
618 0         0 $self->password($self->config->{password});
619              
620 0 0       0 last if eval { $self->login };
  0         0  
621              
622 0         0 $self->email('');
623 0         0 $self->password('');
624              
625 0         0 print "That combination doesn't seem to be correct. Try again?\n";
626             }
627             }
628              
629             sub filter_config {
630 0     0 1 0 my $self = shift;
631              
632 0 0       0 return {} unless $self->use_filters;
633              
634 0         0 my $all_config = {};
635              
636 0         0 require Path::Class;
637 0         0 require Cwd;
638 0   0     0 my $dir = Path::Class::dir(shift || Cwd::getcwd());
639              
640 0         0 require Hash::Merge;
641 0         0 my $old_behavior = Hash::Merge::get_behavior();
642 0         0 Hash::Merge::set_behavior('RIGHT_PRECEDENT');
643              
644 0         0 while (1) {
645 0         0 my $file = $dir->file( $self->filter_file )->stringify;
646              
647 0 0       0 if (-r $file) {
648 0         0 my $this_config = YAML::LoadFile($file);
649 0         0 $all_config = Hash::Merge::merge($this_config, $all_config);
650             }
651              
652 0         0 my $parent = $dir->parent;
653 0 0       0 last if $parent eq $dir;
654 0         0 $dir = $parent;
655             }
656              
657 0         0 Hash::Merge::set_behavior($old_behavior);
658              
659 0         0 return $all_config;
660             }
661              
662             sub email_of {
663 0     0 1 0 my $self = shift;
664 0         0 my $id = shift;
665              
666 0         0 my $user = $self->read(User => id => $id);
667 0         0 return $user->{email};
668             }
669              
670             __PACKAGE__->meta->make_immutable;
671 7     7   66 no Any::Moose;
  7         14  
  7         69  
672              
673             1;
674              
675             __END__