File Coverage

blib/lib/Dancer2/Core/Error.pm
Criterion Covered Total %
statement 152 152 100.0
branch 55 64 85.9
condition 32 40 80.0
subroutine 24 24 100.0
pod 5 8 62.5
total 268 288 93.0


line stmt bran cond sub pod time code
1             package Dancer2::Core::Error;
2             # ABSTRACT: Class representing fatal errors
3             $Dancer2::Core::Error::VERSION = '2.0.1';
4 158     158   510664 use Moo;
  158         21423  
  158         1332  
5 158     158   79655 use Carp;
  158         390  
  158         15023  
6 158     158   2543 use Dancer2::Core::Types;
  158         436  
  158         1759  
7 158     158   2218552 use Dancer2::Core::HTTP;
  158         599  
  158         9805  
8 158     158   105958 use Data::Dumper;
  158         1549771  
  158         16154  
9 158     158   3358 use Dancer2::FileUtils qw/path open_file/;
  158         375  
  158         12448  
10 158     158   1278 use Sub::Quote;
  158         514  
  158         11641  
11 158     158   2714 use Module::Runtime qw/ require_module use_module /;
  158         6198  
  158         1521  
12 158     158   12278 use Ref::Util qw< is_hashref >;
  158         2506  
  158         8064  
13 158     158   23895 use Clone qw(clone);
  158         27141  
  158         528754  
14              
15             has app => (
16             is => 'ro',
17             isa => InstanceOf['Dancer2::Core::App'],
18             predicate => 'has_app',
19             );
20              
21             has show_stacktrace => (
22             is => 'ro',
23             isa => Bool,
24             default => sub {
25             my $self = shift;
26              
27             $self->has_app
28             and return $self->app->setting('show_stacktrace');
29             },
30             );
31              
32             has charset => (
33             is => 'ro',
34             isa => Str,
35             default => sub {'UTF-8'},
36             );
37              
38             has type => (
39             is => 'ro',
40             isa => Str,
41             default => sub {'Runtime Error'},
42             );
43              
44             has title => (
45             is => 'ro',
46             isa => Str,
47             lazy => 1,
48             builder => '_build_title',
49             );
50              
51             has censor => (
52             is => 'ro',
53             isa => CodeRef,
54             lazy => 1,
55             default => sub {
56             my $self = shift;
57              
58             if( my $custom = $self->has_app && $self->app->setting('error_censor') ) {
59              
60             if( is_hashref $custom ) {
61             die "only one key can be set for the 'error_censor' setting\n"
62             if 1 != keys %$custom;
63              
64             my( $class, $args ) = %$custom;
65              
66             my $censor = use_module($class)->new(%$args);
67              
68             return sub {
69             $censor->censor(@_);
70             }
71             }
72              
73             my $coderef = eval '\&'.$custom;
74              
75             # it's already defined? Nice! We're done
76             return $coderef if $coderef;
77              
78             my $module = $custom =~ s/::[^:]*?$//r;
79              
80             require_module($module);
81              
82             return eval '\&'.$custom;
83             }
84              
85             # reminder: update POD below if changing the config here
86             my $data_censor = use_module('Data::Censor')->new(
87             sensitive_fields => qr/pass|card.?num|pan|secret/i,
88             replacement => "Hidden (looks potentially sensitive)",
89             );
90              
91             return sub {
92             $data_censor->censor(@_);
93             };
94             }
95             );
96              
97             sub _build_title {
98 132     132   4622 my ($self) = @_;
99 132         2988 my $title = 'Error ' . $self->status;
100 132 100       3458 if ( my $msg = Dancer2::Core::HTTP->status_message($self->status) ) {
101 131         424 $title .= ' - ' . $msg;
102             }
103              
104 132         3165 return $title;
105             }
106              
107             has template => (
108             is => 'ro',
109             lazy => 1,
110             builder => '_build_error_template',
111             );
112              
113             sub _build_error_template {
114 119     119   1426 my ($self) = @_;
115              
116             # look for a template named after the status number.
117             # E.g.: views/404.tt for a TT template
118 119         2729 my $engine = $self->app->template_engine;
119 119 100       3439 return $self->status
120             if $engine->pathname_exists( $engine->view_pathname( $self->status ) );
121              
122 113         1071 return;
123             }
124              
125             has static_page => (
126             is => 'ro',
127             lazy => 1,
128             builder => '_build_static_page',
129             );
130              
131             sub _build_static_page {
132 113     113   1336 my ($self) = @_;
133              
134             # TODO there must be a better way to get it
135             my $public_dir = $ENV{DANCER_PUBLIC}
136 113   66     3287 || ( $self->has_app && $self->app->config->{public_dir} );
137              
138 113         3302 my $filename = sprintf "%s/%d.html", $public_dir, $self->status;
139              
140 113 100       7100 open my $fh, '<', $filename or return;
141              
142 2         18 local $/ = undef; # slurp time
143              
144 2         246 return <$fh>;
145             }
146              
147             sub default_error_page {
148 115     115 0 274 my $self = shift;
149              
150 115         1470 require_module('Template::Tiny');
151              
152 115 100 100     6168 my $uri_base = $self->has_app && $self->app->has_request ?
153             $self->app->request->uri_base : '';
154              
155             # GH#1001 stack trace if show_stacktrace is true and this is a 'server' error (5xx)
156 115   100     1365 my $show_fullmsg = $self->show_stacktrace && $self->status =~ /^5/;
157 115 100 100     3821 my $opts = {
158             title => $self->title,
159             charset => $self->charset,
160             content => $show_fullmsg ? $self->full_message : _html_encode($self->message) || 'Wooops, something went wrong',
161             version => Dancer2->VERSION,
162             uri_base => $uri_base,
163             };
164              
165 115         1127 Template::Tiny->new->process( \<<"END_TEMPLATE", $opts, \my $output );
166            
167            
168            
169            
170            
171             [% title %]
172            
173            
174            
175            

[% title %]

176            
177             [% content %]
178            
179            
180             Powered by Dancer2 [% version %]
181            
182            
183            
184             END_TEMPLATE
185              
186 115         90903 return $output;
187             }
188              
189             # status and message are 'rw' to permit modification in core.error.before hooks
190             has status => (
191             is => 'rw',
192             default => sub {500},
193             isa => Num,
194             );
195              
196             has message => (
197             is => 'rw',
198             isa => Str,
199             lazy => 1,
200             default => sub { '' },
201             );
202              
203             sub full_message {
204 7     7 0 326 my ($self) = @_;
205 7         43 my $html_output = "

" . $self->type . "

";
206 7         32 $html_output .= $self->backtrace;
207 7         41 $html_output .= $self->environment;
208 7         87 return $html_output;
209             }
210              
211             has serializer => (
212             is => 'ro',
213             isa => Maybe[ConsumerOf['Dancer2::Core::Role::Serializer']],
214             builder => '_build_serializer',
215             );
216              
217             sub _build_serializer {
218 138     138   7448 my ($self) = @_;
219              
220 138 100 100     2022 $self->has_app && $self->app->has_serializer_engine
221             and return $self->app->serializer_engine;
222              
223 130         3247 return;
224             }
225              
226             sub BUILD {
227 141     141 0 4281 my ($self) = @_;
228              
229 141 100       4668 $self->has_app &&
230             $self->app->execute_hook( 'core.error.init', $self );
231             }
232              
233             has exception => (
234             is => 'ro',
235             isa => Str,
236             predicate => 1,
237             coerce => sub {
238             # Until we properly support exception objects, we shouldn't barf on
239             # them because that hides the actual error, if object overloads "",
240             # which most exception objects do, this will result in a nicer string.
241             # other references will produce a meaningless error, but that is
242             # better than a meaningless stacktrace
243             return "$_[0]"
244             }
245             );
246              
247             has response => (
248             is => 'rw',
249             lazy => 1,
250             default => sub {
251             my $self = shift;
252             my $serializer = $self->serializer;
253             # include server tokens in response ?
254             my $no_server_tokens = $self->has_app
255             ? $self->app->config->{'no_server_tokens'}
256             : defined $ENV{DANCER_NO_SERVER_TOKENS}
257             ? $ENV{DANCER_NO_SERVER_TOKENS}
258             : 0;
259             return Dancer2::Core::Response->new(
260             server_tokens => !$no_server_tokens,
261             ( serializer => $serializer )x!! $serializer
262             );
263             }
264             );
265              
266             has content_type => (
267             is => 'ro',
268             lazy => 1,
269             default => sub {
270             my $self = shift;
271             $self->serializer
272             ? $self->serializer->content_type
273             : 'text/html'
274             },
275             );
276              
277             has content => (
278             is => 'ro',
279             lazy => 1,
280             builder => '_build_content',
281             );
282              
283             sub _build_content {
284 134     134   2585 my $self = shift;
285              
286             # return a hashref if a serializer is available
287 134 100       725 if ( $self->serializer ) {
288 10         232 my $content = {
289             message => $self->message,
290             title => $self->title,
291             status => $self->status,
292             };
293 10 100       696 $content->{exception} = $self->exception
294             if $self->has_exception;
295 10         56 return $content;
296             }
297              
298             # otherwise we check for a template, for a static file,
299             # for configured error_template, and, if all else fails,
300             # the default error page
301 124 100 100     3172 if ( $self->has_app and $self->template ) {
302             # Render the template using apps' template engine.
303             # This may well be what caused the initial error, in which
304             # case we fall back to static page if any error was thrown.
305             # Note: this calls before/after render hooks.
306 6         100 my $content = eval {
307 6         157 $self->app->template(
308             $self->template,
309             { title => $self->title,
310             content => $self->message,
311             exception => $self->exception,
312             status => $self->status,
313             }
314             );
315             };
316 6 100       77 $@ && $self->app->engine('logger')->log( warning => $@ );
317              
318             # return rendered content unless there was an error.
319 6 100       71 return $content if defined $content;
320             }
321              
322             # It doesn't make sense to return a static page for a 500 if show_stacktrace is on
323 120 100 100     1310 if ( !($self->show_stacktrace && $self->status eq '500') ) {
324 113 100       3107 if ( my $content = $self->static_page ) {
325 2         21 return $content;
326             }
327             }
328              
329 118 100 100     3506 if ($self->has_app && $self->app->config->{error_template}) {
330 3         22 my $content = eval {
331             $self->app->template(
332             $self->app->config->{error_template},
333 3         35 { title => $self->title,
334             content => $self->message,
335             exception => $self->exception,
336             status => $self->status,
337             }
338             );
339             };
340 3 50       13 $@ && $self->app->engine('logger')->log( warning => $@ );
341              
342             # return rendered content unless there was an error.
343 3 50       22 return $content if defined $content;
344             }
345              
346 115         1435 return $self->default_error_page;
347             }
348              
349             sub throw {
350 133     133 1 2919 my $self = shift;
351 133 100       543 $self->response(shift) if @_;
352              
353 133 50       3146 $self->response
354             or croak "error has no response to throw at";
355              
356 133 100       4402 $self->has_app &&
357             $self->app->execute_hook( 'core.error.before', $self );
358              
359 133         4583 my $message = $self->content;
360              
361 133         5724 $self->response->status( $self->status );
362 133         8492 $self->response->content_type( $self->content_type );
363 133         3393 $self->response->content($message);
364              
365 133 100       19447 $self->has_app &&
366             $self->app->execute_hook('core.error.after', $self->response);
367              
368 133         4298 $self->response->is_halted(1);
369 133         10604 return $self->response;
370             }
371              
372             sub backtrace {
373 7     7 1 22 my ($self) = @_;
374              
375 7         202 my $message = $self->message;
376 7 50       244 if ($self->exception) {
377 7 50       30 $message .= "\n" if $message;
378 7         32 $message .= $self->exception;
379             }
380 7   50     25 $message ||= 'Wooops, something went wrong';
381              
382 7         50 my $html = '
' . _html_encode($message) . "
\n";
383              
384             # the default perl warning/error pattern
385 7         57 my ($file, $line) = $message =~ /at (\S+) line (\d+)/;
386             # the Devel::SimpleTrace pattern
387 7 100 66     75 ($file, $line) = $message =~ /at.*\((\S+):(\d+)\)/ unless $file and $line;
388              
389             # no file/line found, cannot open a file for context
390 7 100 66     87 return $html unless $file and $line;
391              
392             # file and line are located, let's read the source Luke!
393 4 50       14 my $fh = eval { open_file('<', $file) } or return $html;
  4         31  
394 4         144 my @lines = <$fh>;
395 4         1184 close $fh;
396              
397 4         20 $html .= qq|
$file around line $line
|;
398              
399             # get 5 lines of context
400 4 50       28 my $start = $line - 5 > 1 ? $line - 5 : 1;
401 4 50       19 my $stop = $line + 5 < @lines ? $line + 5 : @lines;
402              
403 4         15 $html .= qq|
\n|; ' : ''; \n"; 
404 4         19 for my $l ($start .. $stop) {
405 44         95 chomp $lines[$l - 1];
406              
407 44 100       204 $html .= $l == $line ? '
408 44         109 $html .= "$l" . _html_encode($lines[$l - 1]) . "
409             }
410 4         14 $html .= "
\n";
411              
412 4         135 return $html;
413             }
414              
415             sub dumper {
416 13     13 1 169 my ($self,$obj) = @_;
417              
418             # Take a copy of the data, so we can mask sensitive-looking stuff:
419 13         1297 my $data = clone($obj);
420 13         549 my $censored = $self->censor->( $data );
421              
422             #use Data::Dumper;
423 13         5214 my $dd = Data::Dumper->new( [ $data ] );
424 13         625 my $hash_separator = ' @@!%,+$$#._(-- '; # Very unlikely string to exist already
425 13         31 my $prefix_padding = ' #+#+@%.,$_-!(( '; # Very unlikely string to exist already
426 13         72 $dd->Terse(1)->Quotekeys(0)->Indent(1)->Sortkeys(1)->Pair($hash_separator)->Pad($prefix_padding);
427 13         803 my $content = _html_encode( $dd->Dump );
428 13         121 $content =~ s/^.+//; # Remove the first line
429 13         346 $content =~ s/\n.+$//; # Remove the last line
430 13         495 $content =~ s/^\Q$prefix_padding\E //gm; # Remove the padding
431 13         2134 $content =~ s{^(\s*)(.+)\Q$hash_separator}{$1$2 => }gm;
432 13 100       75 if ($censored) {
433 7         21 $content
434             .= "\n\nNote: Values of $censored sensitive-looking keys hidden\n";
435             }
436 13         677 return $content;
437             }
438              
439             sub environment {
440 13     13 1 84 my ($self) = @_;
441              
442 13         57 my $stack = $self->get_caller;
443 13   66     142 my $settings = $self->has_app && $self->app->settings;
444 13   33     434 my $session = $self->has_app && $self->app->_has_session && $self->app->session->data;
445 13   66     139 my $env = $self->has_app && $self->app->has_request && $self->app->request->env;
446              
447             # Get a sanitised dump of the settings, session and environment
448 13 100       132 $_ = $_ ? $self->dumper($_) : 'undefined' for $settings, $session, $env;
449              
450 13         335 return <<"END_HTML";
451            
Stack
$stack
452            
Settings
$settings
453            
Session
$session
454            
Environment
$env
455             END_HTML
456             }
457              
458             sub get_caller {
459 13     13 1 37 my ($self) = @_;
460 13         28 my @stack;
461              
462 13         29 my $deepness = 0;
463 13         134 while ( my ( $package, $file, $line ) = caller( $deepness++ ) ) {
464 206         1340 push @stack, "$package in $file l. $line";
465             }
466              
467 13         133 return join( "\n", reverse(@stack) );
468             }
469              
470             # private
471              
472             # Replaces the entities that are illegal in (X)HTML.
473             sub _html_encode {
474 172     172   10684 my $value = shift;
475              
476 172 50       607 return if !defined $value;
477              
478 172         499 $value =~ s/&/&/g;
479 172         359 $value =~ s/
480 172         488 $value =~ s/>/>/g;
481 172         699 $value =~ s/'/'/g;
482 172         442 $value =~ s/"/"/g;
483              
484 172         1368 return $value;
485             }
486              
487             1;
488              
489             __END__