File Coverage

blib/lib/Mojolicious/Plugin/Loco.pm
Criterion Covered Total %
statement 72 119 60.5
branch 18 64 28.1
condition 9 28 32.1
subroutine 12 20 60.0
pod 1 1 100.0
total 112 232 48.2


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::Loco 0.005;
2              
3             # ABSTRACT: launch a web browser; easy local GUI
4              
5 2     2   13060 use Mojo::Base 'Mojolicious::Plugin';
  2         5  
  2         11  
6              
7 2     2   676 use Browser::Open 'open_browser_cmd';
  2         636  
  2         89  
8 2     2   790 use File::ShareDir 'dist_file';
  2         39810  
  2         112  
9 2     2   13 use Mojo::ByteStream 'b';
  2         4  
  2         83  
10 2     2   10 use Mojo::Util qw(hmac_sha1_sum steady_time);
  2         4  
  2         3365  
11              
12             sub register {
13 6     6 1 89273 my ($self, $app, $o) = @_;
14 6         55 my %conf = (
15             entry => '/',
16             initial_wait => 15,
17             final_wait => 3,
18             api_path => '/hb/',
19             %$o
20             );
21             my $api =
22 6         83 Mojo::Path->new($conf{api_path})->leading_slash(1)->trailing_slash(1);
23             my ($init_path, $hb_path, $js_path) =
24 6         646 map { $api->merge($_)->to_string } qw(init hb heartbeat.js);
  18         2580  
25              
26 6         1079 my %_settable = ();
27 6         26 ++$_settable{$_} for qw(initial_wait final_wait);
28             $app->helper(
29             'loco.conf' => sub {
30 0     0   0 my $c = shift;
31              
32             # Hash
33 0 0       0 return \%conf unless @_;
34              
35             # Get
36 0 0 0     0 return $conf{ $_[0] } unless @_ > 1 || ref $_[0];
37              
38             # Set
39 0 0       0 my $values = ref $_[0] ? $_[0] : {@_};
40 0         0 for (keys %$values) {
41 0 0       0 delete $values->{$_} unless $_settable{$_};
42             }
43 0         0 @conf{ keys %$values } = values %$values;
44 0         0 return $c;
45             }
46 6         62 );
47              
48             $app->helper(
49             'loco.reply_400' => sub {
50 0     0   0 my $c = shift;
51 0 0 0     0 my %options = (
52             info => '',
53             status => $c->stash('status') // 400,
54             (@_ % 2 ? ('message') : (message => 'Bad Request')), @_
55             );
56 0         0 return $c->render(template => 'done', title => 'Error', %options);
57             }
58 6         138 );
59              
60             $app->helper(
61             'loco.csrf_fail' => sub {
62 0     0   0 my $c = shift;
63 0 0 0     0 return 1 if '400' eq ($c->res->code // '');
64 0 0       0 return $c->loco->reply_400(info => 'unexpected origin')
65             if $c->validation->csrf_protect->error('csrf_token');
66             }
67 6         88 );
68              
69             $app->helper(
70             'loco.id_fail' => sub {
71 0     0   0 my $c = shift;
72 0 0 0     0 return 1 if '400' eq ($c->res->code // '');
73 0 0       0 return $c->loco->reply_400(info => 'wrong session')
74             unless $c->loco->id;
75             }
76 6         83 );
77              
78             $app->helper(
79             'loco.quit' => sub {
80 0     0   0 my $c = shift;
81 0 0       0 return if $c->loco->csrf_fail;
82 0 0       0 $c->render(
83             template => "done",
84             format => "html",
85             title => "Finished",
86             header => "Close this window"
87             ) unless $c->res->code;
88 0         0 Mojo::IOLoop->timer(1 => sub { shift->stop });
  0         0  
89             }
90 6         86 );
91              
92             $app->hook(
93             before_server_start => sub {
94 11     11   14115 my ($server, $app) = @_;
95 11 100       47 return if $conf{browser_launched}++;
96             my ($url) = map {
97 6         37 my $u = Mojo::URL->new($_);
98 6         1166 $u->host($u->host =~ s![*]!localhost!r);
99 6         12 } @{ $server->listen };
  6         16  
100              
101 6         61 my $_test = $conf{_test_browser_launch};
102              
103             # no explicit port means this is coming from UserAgent
104             return
105 6 50 33     23 unless ($url->port || $_test);
106              
107 6         61 $conf{seed} = my $seed =
108             _make_csrf($app, $$ . steady_time . rand . 'x');
109              
110 6         315 $url->path($init_path)->query(s => $seed);
111              
112 6   100     509 my $cmd = $conf{browser} // open_browser_cmd();
113 6 100       30 unless ($cmd) {
    100          
114 2 100       24 die "Cannot find browser to execute"
115             unless defined $cmd;
116 1         4 return;
117             }
118 0         0 elsif (ref($cmd) eq 'CODE') {
119 2         8 $cmd->($url);
120             }
121             else {
122 2 50       24 if ($_test) {
123 2         37 $_test->($cmd, $url);
124 2         163 return;
125             }
126 0 0       0 if ($^O eq 'MSWin32') {
127 0 0       0 system start => (
    0          
    0          
128             $cmd =~ m/^microsoft-edge/
129             ? ("microsoft-edge:$url")
130             : (($cmd eq 'start' ? () : ($cmd)), "$url")
131             ) and die "exec '$cmd' failed";
132             }
133             else {
134 0         0 my $pid;
135 0 0       0 unless ($pid = fork) {
136 0 0       0 unless (fork) {
137 0         0 exec $cmd, $url->to_string;
138 0         0 die "exec '$cmd' failed";
139             }
140 0         0 exit 0;
141             }
142 0         0 waitpid($pid, 0);
143             }
144             }
145 2         13 _reset_timer($conf{initial_wait});
146             }
147 6         117 );
148              
149             $app->hook(
150             before_routes => sub {
151 13     13   185115 my $c = shift;
152             $c->validation->csrf_token('')
153 13 100 66     111 if ($conf{seed} || !$c->session->{'loco.id'});
154             }
155 6 50       102 ) unless $conf{allow_other_sessions};
156              
157             $app->helper(
158             'loco.id' => sub {
159 3     3   205 my $c = shift;
160             undef $c->session->{csrf_token}
161 3 50       16 if @_;
162 3         233 return $c->session('loco.id', @_);
163             }
164 6         77 );
165              
166             $app->routes->get(
167             $init_path => sub {
168 3     3   4394 my $c = shift;
169 3   33     18 my $seed = $c->param('s') // '' =~ s/[^0-9a-f]//gr;
170              
171 3 50 50     391 if (length($seed) >= 40
      33        
172             && $seed eq ($conf{seed} // ''))
173             {
174 3         9 delete $conf{seed};
175 3         33 $c->loco->id(1);
176             }
177 3         252 $c->redirect_to($conf{entry});
178             }
179 6         73 );
180              
181             $app->routes->get(
182             $hb_path => sub {
183 0     0   0 my $c = shift;
184 0         0 state $hcount = 0;
185 0 0       0 if ($c->validation->csrf_protect->error('csrf_token')) {
186              
187             # print STDERR "bad csrf: "
188             # . $c->validation->input->{csrf_token} . " vs "
189             # . $c->validation->csrf_token . "\n";
190 0         0 return $c->render(
191             json => { error => 'unexpected origin' },
192             status => 400,
193             message => 'Bad Request',
194             info => 'unexpected origin'
195             );
196             }
197 0         0 _reset_timer($conf{final_wait});
198 0         0 $c->render(json => { h => ++$hcount });
199              
200             # return $c->helpers->reply->not_found()
201             # if ($hcount > 5);
202             }
203 6         2181 );
204              
205 6         1352 $app->static->extra->{ $js_path =~ s!^/!!r } =
206             dist_file(__PACKAGE__ =~ s/::/-/gr, 'heartbeat.js');
207              
208 6         1447 push @{ $app->renderer->classes }, __PACKAGE__;
  6         19  
209              
210             $app->helper(
211             'loco.jsload' => sub {
212 0 0   0   0 my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
213 0         0 my ($c, %option) = @_;
214 0         0 my $csrf = $c->csrf_token;
215 0   0     0 my $jquery = $option{jquery} // '/mojo/jquery/jquery.js';
216             b(
217             (
218             join "",
219 0         0 map { $c->javascript($_) . "\n" }
220             (length($jquery) ? ($jquery) : ()),
221             $js_path
222             )
223             . $c->javascript(
224             sub {
225             <
226             \$.fn.heartbeat.defaults.ajax.url = '$hb_path';
227             \$.fn.heartbeat.defaults.ajax.headers['X-CSRF-Token'] = '$csrf';
228             END
229 0         0 . $c->include(
230             'ready',
231             format => 'js',
232             nofinish => 0,
233             %option, _cb => $cb
234             );
235             }
236             )
237 0 0       0 );
238             }
239 6         108 );
240              
241             }
242              
243             sub _make_csrf {
244 6     6   125 my ($app, $seed) = @_;
245 6         53 hmac_sha1_sum(pack('h*', $seed), $app->secrets->[0]);
246             }
247              
248             sub _reset_timer {
249 2     2   4 state $timer;
250 2 50       7 Mojo::IOLoop->remove($timer)
251             if defined $timer;
252 2 100       10 return unless my $wait = shift;
253             $timer = Mojo::IOLoop->timer(
254             $wait,
255             sub {
256 0     0     print STDERR "stopping...";
257 0           shift->stop;
258             }
259 1         8 );
260             }
261              
262             1;
263              
264             __DATA__