File Coverage

blib/lib/Mojolicious/Plugin/Loco.pm
Criterion Covered Total %
statement 73 121 60.3
branch 20 66 30.3
condition 9 28 32.1
subroutine 12 20 60.0
pod 1 1 100.0
total 115 236 48.7


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