File Coverage

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*\s*$)!$html!msi;
415            
416             $response->content($content);
417             };
418              
419             after sub {
420             # Try to get the $after_filter sub executed as the very last filter (after
421             # all the other filters defined in the application)
422             return if $filter_registered;
423             after $after_filter;
424             $filter_registered = 1;
425             };
426              
427             $route_pattern = qr(^$path_prefix/.*);
428            
429             get $route_pattern => sub {
430             (my $path = request->path_info) =~ s!^$path_prefix/!!;
431            
432             send_file(catfile($dist_dir, 'debugtoolbar', split(m!/!, $path)),
433             system_path => 1);
434             };
435              
436             register_plugin;
437              
438             1; # End of Dancer::Plugin::DebugToolbar
439             __END__