| 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*