blib/lib/Dancer/Plugin/DebugToolbar.pm | |||
---|---|---|---|
Criterion | Covered | Total | % |
statement | 4 | 6 | 66.6 |
branch | n/a | ||
condition | n/a | ||
subroutine | 2 | 2 | 100.0 |
pod | n/a | ||
total | 6 | 8 | 75.0 |
line | stmt | bran | cond | sub | pod | time | code | |||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | package Dancer::Plugin::DebugToolbar; | |||||||||||||
2 | ||||||||||||||
3 | =head1 NAME | |||||||||||||
4 | ||||||||||||||
5 | Dancer::Plugin::DebugToolbar - A debugging toolbar for Dancer web applications | |||||||||||||
6 | ||||||||||||||
7 | =cut | |||||||||||||
8 | ||||||||||||||
9 | 1 | 1 | 20359 | use strict; | ||||||||||
1 | 2 | |||||||||||||
1 | 34 | |||||||||||||
10 | ||||||||||||||
11 | 1 | 1 | 438 | use Dancer ':syntax'; | ||||||||||
0 | ||||||||||||||
0 | ||||||||||||||
12 | use Dancer::App; | |||||||||||||
13 | use Dancer::Plugin; | |||||||||||||
14 | use Dancer::Route::Registry; | |||||||||||||
15 | use File::ShareDir; | |||||||||||||
16 | use File::Spec::Functions qw(catfile); | |||||||||||||
17 | use Module::Loaded; | |||||||||||||
18 | use Scalar::Util qw(blessed looks_like_number refaddr); | |||||||||||||
19 | use Tie::Hash::Indexed; | |||||||||||||
20 | use Time::HiRes qw(time); | |||||||||||||
21 | ||||||||||||||
22 | our $VERSION = '0.016'; | |||||||||||||
23 | ||||||||||||||
24 | # Distribution-level shared data directory | |||||||||||||
25 | my $dist_dir = File::ShareDir::dist_dir('Dancer-Plugin-DebugToolbar'); | |||||||||||||
26 | ||||||||||||||
27 | # Information to be displayed to the user | |||||||||||||
28 | my %time_start; | |||||||||||||
29 | my $views; | |||||||||||||
30 | my $dbi_trace; | |||||||||||||
31 | my $dbi_queries; | |||||||||||||
32 | ||||||||||||||
33 | my $route_pattern; | |||||||||||||
34 | my $filter_registered; | |||||||||||||
35 | ||||||||||||||
36 | my $settings = plugin_setting; | |||||||||||||
37 | ||||||||||||||
38 | # Are we on? | |||||||||||||
39 | if (!$settings->{enable}) { | |||||||||||||
40 | return 1; | |||||||||||||
41 | } | |||||||||||||
42 | ||||||||||||||
43 | # Default settings | |||||||||||||
44 | ||||||||||||||
45 | if (!defined $settings->{path_prefix}) { | |||||||||||||
46 | # Default path prefix | |||||||||||||
47 | $settings->{path_prefix} = '/dancer-debug-toolbar'; | |||||||||||||
48 | } | |||||||||||||
49 | ||||||||||||||
50 | if (!defined $settings->{show}) { | |||||||||||||
51 | # By default, we show data and routes | |||||||||||||
52 | $settings->{show} = { | |||||||||||||
53 | data => 1, | |||||||||||||
54 | routes => 1 | |||||||||||||
55 | }; | |||||||||||||
56 | } | |||||||||||||
57 | ||||||||||||||
58 | my $path_prefix = $settings->{path_prefix}; | |||||||||||||
59 | # Need leading slash | |||||||||||||
60 | if ($path_prefix !~ m!^/!) { | |||||||||||||
61 | $path_prefix = '/' . $path_prefix; | |||||||||||||
62 | } | |||||||||||||
63 | ||||||||||||||
64 | if ($settings->{show}->{database}) { | |||||||||||||
65 | require Dancer::Plugin::DebugToolbar::DBI; | |||||||||||||
66 | } | |||||||||||||
67 | ||||||||||||||
68 | sub _ordered_hash (%) { | |||||||||||||
69 | tie my %hash => 'Tie::Hash::Indexed'; | |||||||||||||
70 | %hash = @_; | |||||||||||||
71 | \%hash | |||||||||||||
72 | } | |||||||||||||
73 | ||||||||||||||
74 | sub _wrap_data { | |||||||||||||
75 | my ($var, $options, $parent_refs) = @_; | |||||||||||||
76 | my $ret = {}; | |||||||||||||
77 | ||||||||||||||
78 | $parent_refs = {} unless defined $parent_refs; | |||||||||||||
79 | ||||||||||||||
80 | if (UNIVERSAL::isa($var, "ARRAY")) { | |||||||||||||
81 | if (!$parent_refs->{refaddr($var)}) { | |||||||||||||
82 | $parent_refs->{refaddr($var)} = 1; | |||||||||||||
83 | ||||||||||||||
84 | $ret->{'type'} = 'list'; | |||||||||||||
85 | $ret->{'value'} = _ordered_hash(); | |||||||||||||
86 | my $i = 0; | |||||||||||||
87 | ||||||||||||||
88 | # List array members | |||||||||||||
89 | foreach my $item (@$var) { | |||||||||||||
90 | $ret->{'value'}->{$i++} = _wrap_data($item, $options, | |||||||||||||
91 | $parent_refs); | |||||||||||||
92 | } | |||||||||||||
93 | ||||||||||||||
94 | delete $parent_refs->{refaddr($var)}; | |||||||||||||
95 | } | |||||||||||||
96 | else { | |||||||||||||
97 | # Cyclic reference | |||||||||||||
98 | $ret->{type} = 'perl/cyclic-ref'; | |||||||||||||
99 | } | |||||||||||||
100 | ||||||||||||||
101 | $ret->{'short_value'} = 'ARRAY'; | |||||||||||||
102 | } | |||||||||||||
103 | elsif (UNIVERSAL::isa($var, "HASH")) { | |||||||||||||
104 | if (!$parent_refs->{refaddr($var)}) { | |||||||||||||
105 | $parent_refs->{refaddr($var)} = 1; | |||||||||||||
106 | ||||||||||||||
107 | $ret->{'type'} = 'map'; | |||||||||||||
108 | $ret->{'value'} = _ordered_hash(); | |||||||||||||
109 | ||||||||||||||
110 | foreach my $name ($options->{sort_keys} ? sort keys %$var : | |||||||||||||
111 | keys %$var) | |||||||||||||
112 | { | |||||||||||||
113 | $ret->{'value'}->{$name} = _wrap_data($var->{$name}, $options, | |||||||||||||
114 | $parent_refs); | |||||||||||||
115 | } | |||||||||||||
116 | ||||||||||||||
117 | if (my $class = blessed($var)) { | |||||||||||||
118 | # Blessed hash | |||||||||||||
119 | $ret->{'short_value'} = { | |||||||||||||
120 | html => ' ' . |
|||||||||||||
121 | ' 122 | '">' . $class . '' | ||||||||||||
123 | }; | |||||||||||||
124 | } | |||||||||||||
125 | else { | |||||||||||||
126 | $ret->{'short_value'} = 'HASH'; | |||||||||||||
127 | } | |||||||||||||
128 | ||||||||||||||
129 | delete $parent_refs->{refaddr($var)}; | |||||||||||||
130 | } | |||||||||||||
131 | else { | |||||||||||||
132 | # Cyclic reference | |||||||||||||
133 | $ret->{type} = 'perl/cyclic-ref'; | |||||||||||||
134 | } | |||||||||||||
135 | } | |||||||||||||
136 | elsif (looks_like_number($var)) { | |||||||||||||
137 | # Number | |||||||||||||
138 | $ret->{'type'} = 'number'; | |||||||||||||
139 | $ret->{'value'} = $var; | |||||||||||||
140 | } | |||||||||||||
141 | elsif (defined $var) { | |||||||||||||
142 | # String | |||||||||||||
143 | $ret->{'type'} = 'string'; | |||||||||||||
144 | $ret->{'value'} = '"' . $var . '"'; | |||||||||||||
145 | } | |||||||||||||
146 | elsif (!defined $var) { | |||||||||||||
147 | # Undefined | |||||||||||||
148 | $ret->{'type'} = 'perl/undefined'; | |||||||||||||
149 | } | |||||||||||||
150 | else { | |||||||||||||
151 | $ret->{'type'} = ''; | |||||||||||||
152 | $ret->{'value'} = $var; | |||||||||||||
153 | } | |||||||||||||
154 | ||||||||||||||
155 | return $ret; | |||||||||||||
156 | } | |||||||||||||
157 | ||||||||||||||
158 | { | |||||||||||||
159 | my $original = {}; | |||||||||||||
160 | ||||||||||||||
161 | no strict 'refs'; | |||||||||||||
162 | ||||||||||||||
163 | # Override the render method of all loaded Dancer::Template::* modules | |||||||||||||
164 | foreach my $module (keys %INC) { | |||||||||||||
165 | if ($module =~ m{^Dancer/Template/}) { | |||||||||||||
166 | $module =~ s{/}{::}g; | |||||||||||||
167 | $module =~ s/\.pm$//; | |||||||||||||
168 | ||||||||||||||
169 | # Save the original render method | |||||||||||||
170 | $original->{$module . '::render'} = \&{$module . '::render'}; | |||||||||||||
171 | ||||||||||||||
172 | *{$module . '::render'} = sub { | |||||||||||||
173 | my ($self, $template, $tokens) = @_; | |||||||||||||
174 | ||||||||||||||
175 | if (ref $template) { | |||||||||||||
176 | # $template is a reference to a string with the template | |||||||||||||
177 | # contents | |||||||||||||
178 | # TODO: Consider getting a substring of template contents | |||||||||||||
179 | $template = 'REF'; | |||||||||||||
180 | } | |||||||||||||
181 | elsif (index($template, setting('views')) == 0) { | |||||||||||||
182 | # If $template is a file under the application's views | |||||||||||||
183 | # directory, strip off the directory | |||||||||||||
184 | $template = substr($template, length(setting('views'))); | |||||||||||||
185 | $template =~ s{^/}{}; | |||||||||||||
186 | } | |||||||||||||
187 | ||||||||||||||
188 | # Strip off "Dancer::Template::" to get just the name of the | |||||||||||||
189 | # template engine | |||||||||||||
190 | (my $engine = blessed($self)) =~ s{.*::}{}; | |||||||||||||
191 | ||||||||||||||
192 | push(@$views, { | |||||||||||||
193 | 'template' => $template, | |||||||||||||
194 | 'engine' => $engine, | |||||||||||||
195 | 'tokens' => _wrap_data($tokens, { sort_keys => 1 }) | |||||||||||||
196 | }); | |||||||||||||
197 | ||||||||||||||
198 | return &{$original->{blessed($self) . '::render'}}(@_); | |||||||||||||
199 | }; | |||||||||||||
200 | } | |||||||||||||
201 | } | |||||||||||||
202 | } | |||||||||||||
203 | ||||||||||||||
204 | before sub { | |||||||||||||
205 | return if request->path_info =~ $route_pattern; | |||||||||||||
206 | ||||||||||||||
207 | my $request_id = request->path_info . time; | |||||||||||||
208 | request->{_debug}->{id} = $request_id; | |||||||||||||
209 | ||||||||||||||
210 | $time_start{$request_id} = time; | |||||||||||||
211 | ||||||||||||||
212 | # Clear collected views data | |||||||||||||
213 | $views = []; | |||||||||||||
214 | ||||||||||||||
215 | if ($settings->{show}->{database}) { | |||||||||||||
216 | Dancer::Plugin::DebugToolbar::DBI::reset(); | |||||||||||||
217 | } | |||||||||||||
218 | }; | |||||||||||||
219 | ||||||||||||||
220 | my $after_filter = sub { | |||||||||||||
221 | my $response = shift; | |||||||||||||
222 | my $content = $response->content; | |||||||||||||
223 | my $status = $response->status; | |||||||||||||
224 | ||||||||||||||
225 | return if $status < 200 || $status == 204 || $status == 304; | |||||||||||||
226 | return if $response->content_type !~ m!^(?:text/html|application/xhtml\+xml)!; | |||||||||||||
227 | return if request->path_info =~ $route_pattern; | |||||||||||||
228 | ||||||||||||||
229 | my $request_id = request->{_debug}->{id}; | |||||||||||||
230 | return if !$request_id; | |||||||||||||
231 | ||||||||||||||
232 | my $time_elapsed = time - $time_start{$request_id}; | |||||||||||||
233 | ||||||||||||||
234 | # | |||||||||||||
235 | # Get routes | |||||||||||||
236 | # | |||||||||||||
237 | my $routes = Dancer::App->current->registry->routes(); | |||||||||||||
238 | ||||||||||||||
239 | my $all_routes = {}; | |||||||||||||
240 | my $matching_routes = {}; | |||||||||||||
241 | ||||||||||||||
242 | foreach my $method (keys %$routes) { | |||||||||||||
243 | $all_routes->{uc $method} = []; | |||||||||||||
244 | $matching_routes->{uc $method} = []; | |||||||||||||
245 | ||||||||||||||
246 | foreach my $route (@{$routes->{$method}}) { | |||||||||||||
247 | # Exclude our own route used to access the toolbar JS/CSS files | |||||||||||||
248 | next if ($route->{'pattern'} eq $route_pattern); | |||||||||||||
249 | ||||||||||||||
250 | my $route_info = {}; | |||||||||||||
251 | my $route_data = _ordered_hash( | |||||||||||||
252 | 'Pattern' => qq{$route->{'pattern'}}, | |||||||||||||
253 | 'Compiled regexp' => qq{$route->{'_compiled_regexp'}} | |||||||||||||
254 | ); | |||||||||||||
255 | ||||||||||||||
256 | # Is this a matching route? | |||||||||||||
257 | if (lc request->method eq $method && request->path_info =~ | |||||||||||||
258 | $route->{'_compiled_regexp'}) | |||||||||||||
259 | { | |||||||||||||
260 | $route_data->{'Match data'} = $route->match_data; | |||||||||||||
261 | } | |||||||||||||
262 | ||||||||||||||
263 | $route_info = { | |||||||||||||
264 | 'pattern' => qq{$route->{'pattern'}}, | |||||||||||||
265 | 'matching' => exists $route_data->{'Match data'}, | |||||||||||||
266 | 'data' => _wrap_data($route_data) | |||||||||||||
267 | }; | |||||||||||||
268 | ||||||||||||||
269 | # Add the route to the list of all routes | |||||||||||||
270 | push(@{$all_routes->{uc $method}}, $route_info); | |||||||||||||
271 | ||||||||||||||
272 | if ($route_info->{matching}) { | |||||||||||||
273 | # Add the route to the list of matching routes | |||||||||||||
274 | push(@{$matching_routes->{uc $method}}, $route_info); | |||||||||||||
275 | } | |||||||||||||
276 | } | |||||||||||||
277 | } | |||||||||||||
278 | ||||||||||||||
279 | my $config = config; | |||||||||||||
280 | my $request = request; | |||||||||||||
281 | my $session; | |||||||||||||
282 | my $vars = vars; | |||||||||||||
283 | ||||||||||||||
284 | # Session must be defined in the configuration, otherwise it doesn't exist | |||||||||||||
285 | if (config->{'session'}) { | |||||||||||||
286 | $session = session; | |||||||||||||
287 | } | |||||||||||||
288 | ||||||||||||||
289 | # Remove private members from request object | |||||||||||||
290 | for my $name (keys %$request) { | |||||||||||||
291 | delete $request->{$name} if ($name =~ /^_/); | |||||||||||||
292 | } | |||||||||||||
293 | ||||||||||||||
294 | my $show = $settings->{'show'}; | |||||||||||||
295 | ||||||||||||||
296 | if ($show->{'database'}) { | |||||||||||||
297 | # Get the collected DBI trace and queries | |||||||||||||
298 | $dbi_trace = Dancer::Plugin::DebugToolbar::DBI::get_dbi_trace(); | |||||||||||||
299 | $dbi_queries = Dancer::Plugin::DebugToolbar::DBI::get_dbi_queries(); | |||||||||||||
300 | } | |||||||||||||
301 | ||||||||||||||
302 | my $toolbar_cfg = { | |||||||||||||
303 | 'toolbar' => { | |||||||||||||
304 | 'logo' => 1, | |||||||||||||
305 | 'buttons' => _ordered_hash( | |||||||||||||
306 | 'time' => { | |||||||||||||
307 | 'text' => sprintf("%.04fs", $time_elapsed) | |||||||||||||
308 | }, | |||||||||||||
309 | 'data' => $show->{'data'} ? { | |||||||||||||
310 | 'text' => 'data' | |||||||||||||
311 | } : undef, | |||||||||||||
312 | 'routes' => $show->{'routes'} ? { | |||||||||||||
313 | 'text' => 'routes' | |||||||||||||
314 | } : undef, | |||||||||||||
315 | 'templates' => $show->{'templates'} ? { | |||||||||||||
316 | 'text' => 'templates' | |||||||||||||
317 | } : undef, | |||||||||||||
318 | 'database' => $show->{'database'} ? { | |||||||||||||
319 | 'text' => 'database' | |||||||||||||
320 | } : undef, | |||||||||||||
321 | 'align' => 1, | |||||||||||||
322 | 'close' => 1 | |||||||||||||
323 | ) | |||||||||||||
324 | }, | |||||||||||||
325 | 'screens' => { | |||||||||||||
326 | 'data' => { | |||||||||||||
327 | 'title' => 'Data', | |||||||||||||
328 | 'pages' => _ordered_hash( | |||||||||||||
329 | 'config' => { | |||||||||||||
330 | 'name' => 'config', | |||||||||||||
331 | 'type' => 'data-structure/perl', | |||||||||||||
332 | 'data' => _wrap_data($config, { sort_keys => 1 }) | |||||||||||||
333 | }, | |||||||||||||
334 | 'request' => { | |||||||||||||
335 | 'name' => 'request', | |||||||||||||
336 | 'type' => 'data-structure/perl', | |||||||||||||
337 | 'data' => _wrap_data($request, { sort_keys => 1 }) | |||||||||||||
338 | }, | |||||||||||||
339 | 'session' => $session ? { | |||||||||||||
340 | 'name' => 'session', | |||||||||||||
341 | 'type' => 'data-structure/perl', | |||||||||||||
342 | 'data' => _wrap_data($session, { sort_keys => 1 }) | |||||||||||||
343 | } : 1, | |||||||||||||
344 | 'vars' => { | |||||||||||||
345 | 'name' => 'vars', | |||||||||||||
346 | 'type' => 'data-structure/perl', | |||||||||||||
347 | 'data' => _wrap_data($vars, { sort_keys => 1 }) | |||||||||||||
348 | } | |||||||||||||
349 | ) | |||||||||||||
350 | }, | |||||||||||||
351 | 'routes' => { | |||||||||||||
352 | 'title' => 'Routes', | |||||||||||||
353 | 'pages' => _ordered_hash( | |||||||||||||
354 | 'all' => { | |||||||||||||
355 | 'type' => 'routes', | |||||||||||||
356 | 'routes' => $all_routes | |||||||||||||
357 | }, | |||||||||||||
358 | 'matching' => { | |||||||||||||
359 | 'type' => 'routes', | |||||||||||||
360 | 'routes' => $matching_routes | |||||||||||||
361 | } | |||||||||||||
362 | ) | |||||||||||||
363 | }, | |||||||||||||
364 | # Templates | |||||||||||||
365 | 'templates' => { | |||||||||||||
366 | 'title' => 'Templates', | |||||||||||||
367 | 'pages' => _ordered_hash( | |||||||||||||
368 | 'templates' => { | |||||||||||||
369 | 'type' => 'templates', | |||||||||||||
370 | 'views' => $views | |||||||||||||
371 | } | |||||||||||||
372 | ) | |||||||||||||
373 | }, | |||||||||||||
374 | # Database | |||||||||||||
375 | 'database' => $show->{'database'} ? { | |||||||||||||
376 | 'title' => 'Database', | |||||||||||||
377 | 'pages' => _ordered_hash( | |||||||||||||
378 | 'trace' => { | |||||||||||||
379 | 'type' => 'text', | |||||||||||||
380 | 'content' => $dbi_trace | |||||||||||||
381 | }, | |||||||||||||
382 | 'queries' => { | |||||||||||||
383 | 'type' => 'database-queries', | |||||||||||||
384 | 'queries' => $dbi_queries | |||||||||||||
385 | } | |||||||||||||
386 | ) | |||||||||||||
387 | } : undef | |||||||||||||
388 | } | |||||||||||||
389 | }; | |||||||||||||
390 | ||||||||||||||
391 | my $html; | |||||||||||||
392 | open(F, "<", catfile($dist_dir, 'debugtoolbar', 'html', | |||||||||||||
393 | 'debugtoolbar.html')); | |||||||||||||
394 | { | |||||||||||||
395 | local $/; | |||||||||||||
396 | $html = |
|||||||||||||
397 | } | |||||||||||||
398 | close(F); | |||||||||||||
399 | ||||||||||||||
400 | # Encode the configuration as JSON | |||||||||||||
401 | my $cfg_json = to_json($toolbar_cfg); | |||||||||||||
402 | ||||||||||||||
403 | # Do some replacements so that the JSON data can be made into a JS string | |||||||||||||
404 | # wrapped in single quotes | |||||||||||||
405 | $cfg_json =~ s!\\!\\\\!gm; | |||||||||||||
406 | $cfg_json =~ s!\n!\\\n!gm; | |||||||||||||
407 | $cfg_json =~ s!'!\\'!gm; | |||||||||||||
408 | ||||||||||||||
409 | $html =~ s/%DEBUGTOOLBAR_CFG%/$cfg_json/m; | |||||||||||||
410 | ||||||||||||||
411 | my $uri_base = request->uri_base . $path_prefix; | |||||||||||||
412 | $html =~ s/%BASE%/$uri_base/mg; | |||||||||||||
413 | ||||||||||||||
414 | $content =~ s!(?= |
\s*