File Coverage

blib/lib/Dancer2/Core/Error.pm
Criterion Covered Total %
statement 159 159 100.0
branch 59 70 84.2
condition 32 40 80.0
subroutine 25 25 100.0
pod 5 8 62.5
total 280 302 92.7


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.1.0';
4 162     162   503103 use Moo;
  162         21741  
  162         1371  
5 162     162   85993 use Carp;
  162         376  
  162         13082  
6 162     162   2081 use Dancer2::Core::Types;
  162         404  
  162         1784  
7 162     162   2311697 use Dancer2::Core::MIME;
  162         777  
  162         8065  
8 162     162   3442 use Dancer2::Core::HTTP;
  162         388  
  162         4879  
9 162     162   114812 use Data::Dumper;
  162         1623276  
  162         17767  
10 162     162   5069 use Path::Tiny ();
  162         52333  
  162         3824  
11 162     162   875 use Sub::Quote;
  162         343  
  162         13309  
12 162     162   1133 use Module::Runtime qw/ require_module use_module /;
  162         341  
  162         1465  
13 162     162   12693 use Ref::Util qw< is_hashref >;
  162         2455  
  162         9100  
14 162     162   27754 use Clone qw(clone);
  162         28222  
  162         624189  
15              
16             has app => (
17             is => 'ro',
18             isa => InstanceOf['Dancer2::Core::App'],
19             predicate => 'has_app',
20             );
21              
22             has show_stacktrace => (
23             is => 'ro',
24             isa => Bool,
25             default => sub {
26             my $self = shift;
27              
28             $self->has_app
29             and return $self->app->setting('show_stacktrace');
30             },
31             );
32              
33             has charset => (
34             is => 'ro',
35             isa => Str,
36             default => sub {'UTF-8'},
37             );
38              
39             has type => (
40             is => 'ro',
41             isa => Str,
42             default => sub {'Runtime Error'},
43             );
44              
45             has title => (
46             is => 'ro',
47             isa => Str,
48             lazy => 1,
49             builder => '_build_title',
50             );
51              
52             has censor => (
53             is => 'ro',
54             isa => CodeRef,
55             lazy => 1,
56             default => sub {
57             my $self = shift;
58              
59             if( my $custom = $self->has_app && $self->app->setting('error_censor') ) {
60              
61             if( is_hashref $custom ) {
62             die "only one key can be set for the 'error_censor' setting\n"
63             if 1 != keys %$custom;
64              
65             my( $class, $args ) = %$custom;
66              
67             my $censor = use_module($class)->new(%$args);
68              
69             return sub {
70             $censor->censor(@_);
71             }
72             }
73              
74             my $coderef = eval '\&'.$custom;
75              
76             # it's already defined? Nice! We're done
77             return $coderef if $coderef;
78              
79             my $module = $custom =~ s/::[^:]*?$//r;
80              
81             require_module($module);
82              
83             return eval '\&'.$custom;
84             }
85              
86             # reminder: update POD below if changing the config here
87             my $data_censor = use_module('Data::Censor')->new(
88             sensitive_fields => qr/pass|card.?num|pan|secret/i,
89             replacement => "Hidden (looks potentially sensitive)",
90             );
91              
92             return sub {
93             $data_censor->censor(@_);
94             };
95             }
96             );
97              
98             sub _build_title {
99 132     132   4103 my ($self) = @_;
100 132         3034 my $title = 'Error ' . $self->status;
101 132 100       3610 if ( my $msg = Dancer2::Core::HTTP->status_message($self->status) ) {
102 131         408 $title .= ' - ' . $msg;
103             }
104              
105 132         3107 return $title;
106             }
107              
108             has template => (
109             is => 'ro',
110             lazy => 1,
111             builder => '_build_error_template',
112             );
113              
114             sub _build_error_template {
115 120     120   1286 my ($self) = @_;
116              
117             # look for a template named after the status number.
118             # E.g.: views/404.tt for a TT template
119 120         2657 my $engine = $self->app->template_engine;
120 120 100       3529 return $self->status
121             if $engine->pathname_exists( $engine->view_pathname( $self->status ) );
122              
123 114         9224 return;
124             }
125              
126             has static_page => (
127             is => 'ro',
128             lazy => 1,
129             builder => '_build_static_page',
130             );
131              
132             sub _build_static_page {
133 113     113   1199 my ($self) = @_;
134              
135             # TODO there must be a better way to get it
136             my $public_dir = $ENV{DANCER_PUBLIC}
137 113   66     3065 || ( $self->has_app && $self->app->config->{public_dir} );
138              
139 113 100       1347 return if !$public_dir;
140              
141 111         425 my $file = Path::Tiny::path($public_dir)->child( $self->status . '.html' );
142 111 100       12270 return if !$file->is_file;
143              
144 2         44 return eval { $file->slurp_utf8 };
  2         7  
145             }
146              
147             sub default_error_page {
148 115     115 0 260 my $self = shift;
149              
150 115         766 require_module('Template::Tiny');
151              
152 115 100 100     5981 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     1129 my $show_fullmsg = $self->show_stacktrace && $self->status =~ /^5/;
157 115 100 100     3936 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             uri_base => $uri_base,
162             };
163              
164 115         1024 Template::Tiny->new->process( \<<"END_TEMPLATE", $opts, \my $output );
165            
166            
167            
168            
169            
170             [% title %]
171            
172            
173            
174            

[% title %]

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

" . $self->type . "

";
202 7         41 $html_output .= $self->backtrace;
203 7         44 $html_output .= $self->environment;
204 7         62 return $html_output;
205             }
206              
207             has serializer => (
208             is => 'ro',
209             isa => Maybe[ConsumerOf['Dancer2::Core::Role::Serializer']],
210             builder => '_build_serializer',
211             );
212              
213             sub _build_serializer {
214 138     138   197680 my ($self) = @_;
215              
216 138 100 100     1798 $self->has_app && $self->app->has_serializer_engine
217             and return $self->app->serializer_engine;
218              
219 130         3240 return;
220             }
221              
222             sub BUILD {
223 141     141 0 4055 my ($self) = @_;
224              
225 141 100       4400 $self->has_app &&
226             $self->app->execute_hook( 'core.error.init', $self );
227             }
228              
229             has exception => (
230             is => 'ro',
231             isa => Str,
232             predicate => 1,
233             coerce => sub {
234             # Until we properly support exception objects, we shouldn't barf on
235             # them because that hides the actual error, if object overloads "",
236             # which most exception objects do, this will result in a nicer string.
237             # other references will produce a meaningless error, but that is
238             # better than a meaningless stacktrace
239             return "$_[0]"
240             }
241             );
242              
243             has response => (
244             is => 'rw',
245             lazy => 1,
246             default => sub {
247             my $self = shift;
248             my $serializer = $self->serializer;
249             # include server tokens in response ?
250             my $no_server_tokens = $self->has_app
251             ? $self->app->config->{'no_server_tokens'}
252             : defined $ENV{DANCER_NO_SERVER_TOKENS}
253             ? $ENV{DANCER_NO_SERVER_TOKENS}
254             : 0;
255             return Dancer2::Core::Response->new(
256             mime_type => $self->has_app ? $self->app->mime_type : Dancer2::Core::MIME->new(),
257             server_tokens => !$no_server_tokens,
258             ( serializer => $serializer )x!! $serializer
259             );
260             }
261             );
262              
263             has content_type => (
264             is => 'ro',
265             lazy => 1,
266             default => sub {
267             my $self = shift;
268             $self->serializer
269             ? $self->serializer->content_type
270             : 'text/html'
271             },
272             );
273              
274             has content => (
275             is => 'ro',
276             lazy => 1,
277             builder => '_build_content',
278             );
279              
280             sub _build_content {
281 134     134   2578 my $self = shift;
282              
283             # return a hashref if a serializer is available
284 134 100       665 if ( $self->serializer ) {
285 10         214 my $content = {
286             message => $self->message,
287             title => $self->title,
288             status => $self->status,
289             };
290 10 100       569 $content->{exception} = $self->exception
291             if $self->has_exception;
292 10         55 return $content;
293             }
294              
295             # otherwise we check for a template, for a static file,
296             # for configured error_template, and, if all else fails,
297             # the default error page
298 124 100 100     2956 if ( $self->has_app and $self->template ) {
299             # Render the template using apps' template engine.
300             # This may well be what caused the initial error, in which
301             # case we fall back to static page if any error was thrown.
302             # Note: this calls before/after render hooks.
303 6         663 my $content = eval {
304 6         161 $self->app->template(
305             $self->template,
306             { title => $self->title,
307             content => $self->message,
308             exception => $self->exception,
309             status => $self->status,
310             }
311             );
312             };
313 6 100       49 $@ && $self->app->engine('logger')->log( warning => $@ );
314              
315             # return rendered content unless there was an error.
316 6 100       71 return $content if defined $content;
317             }
318              
319             # It doesn't make sense to return a static page for a 500 if show_stacktrace is on
320 120 100 100     1186 if ( !($self->show_stacktrace && $self->status eq '500') ) {
321 113 100       3234 if ( my $content = $self->static_page ) {
322 2         381 return $content;
323             }
324             }
325              
326 118 100 100     6580 if ($self->has_app && $self->app->config->{error_template}) {
327 3         22 my $content = eval {
328             $self->app->template(
329             $self->app->config->{error_template},
330 3         38 { title => $self->title,
331             content => $self->message,
332             exception => $self->exception,
333             status => $self->status,
334             }
335             );
336             };
337 3 50       12 $@ && $self->app->engine('logger')->log( warning => $@ );
338              
339             # return rendered content unless there was an error.
340 3 50       18 return $content if defined $content;
341             }
342              
343 115         1474 return $self->default_error_page;
344             }
345              
346             sub throw {
347 133     133 1 2793 my $self = shift;
348 133 100       546 $self->response(shift) if @_;
349              
350 133 50       3118 $self->response
351             or croak "error has no response to throw at";
352              
353 133 100       4123 $self->has_app &&
354             $self->app->execute_hook( 'core.error.before', $self );
355              
356 133         4239 my $message = $self->content;
357              
358 133         4092 $self->response->status( $self->status );
359 133         7862 $self->response->content_type( $self->content_type );
360 133 50       3864 $self->response->charset( $self->charset ) if defined $self->charset;
361 133         10406 $self->response->content($message);
362              
363 133 100       19706 $self->has_app &&
364             $self->app->execute_hook('core.error.after', $self->response);
365              
366 133         4092 $self->response->is_halted(1);
367 133         10403 return $self->response;
368             }
369              
370             sub backtrace {
371 7     7 1 21 my ($self) = @_;
372              
373 7         178 my $message = $self->message;
374 7 50       318 if ($self->exception) {
375 7 50       35 $message .= "\n" if $message;
376 7         43 $message .= $self->exception;
377             }
378 7   50     30 $message ||= 'Wooops, something went wrong';
379              
380 7         29 my $html = '
' . _html_encode($message) . "
\n";
381              
382             # the default perl warning/error pattern
383 7         58 my ($file, $line) = $message =~ /at (\S+) line (\d+)/;
384             # the Devel::SimpleTrace pattern
385 7 100 66     55 ($file, $line) = $message =~ /at.*\((\S+):(\d+)\)/ unless $file and $line;
386              
387             # no file/line found, cannot open a file for context
388 7 100 66     58 return $html unless $file and $line;
389              
390             # file and line are located, let's read the source Luke!
391 4         42 my $path = Path::Tiny::path($file);
392 4 50       205 return $html if !$path->is_file;
393              
394 4         170 my @lines;
395 4 50       15 eval { @lines = $path->lines_utf8; 1; } or return $html;
  4         29  
  4         11720  
396              
397 4         23 $html .= qq|
$file around line $line
|;
398              
399             # get 5 lines of context
400 4 50       45 my $start = $line - 5 > 1 ? $line - 5 : 1;
401 4 50       23 my $stop = $line + 5 < @lines ? $line + 5 : @lines;
402              
403 4         13 $html .= qq|
\n|; ' : ''; \n"; 
404 4         18 for my $l ($start .. $stop) {
405 44         74 chomp $lines[$l - 1];
406              
407 44 100       111 $html .= $l == $line ? '
408 44         86 $html .= "$l" . _html_encode($lines[$l - 1]) . "
409             }
410 4         13 $html .= "
\n";
411              
412 4         124 return $html;
413             }
414              
415             sub dumper {
416 14     14 1 156 my ($self,$obj) = @_;
417              
418             # Take a copy of the data, so we can mask sensitive-looking stuff:
419 14         1339 my $data = clone($obj);
420 14         461 my $censored = $self->censor->( $data );
421              
422             #use Data::Dumper;
423 14         4681 my $dd = Data::Dumper->new( [ $data ] );
424 14         660 my $hash_separator = ' @@!%,+$$#._(-- '; # Very unlikely string to exist already
425 14         30 my $prefix_padding = ' #+#+@%.,$_-!(( '; # Very unlikely string to exist already
426 14         82 $dd->Terse(1)->Quotekeys(0)->Indent(1)->Sortkeys(1)->Pair($hash_separator)->Pad($prefix_padding);
427 14         807 my $content = _html_encode( $dd->Dump );
428 14         133 $content =~ s/^.+//; # Remove the first line
429 14         294 $content =~ s/\n.+$//; # Remove the last line
430 14         495 $content =~ s/^\Q$prefix_padding\E //gm; # Remove the padding
431 14         2023 $content =~ s{^(\s*)(.+)\Q$hash_separator}{$1$2 => }gm;
432 14 100       71 if ($censored) {
433 7         28 $content
434             .= "\n\nNote: Values of $censored sensitive-looking keys hidden\n";
435             }
436 14         625 return $content;
437             }
438              
439             sub environment {
440 13     13 1 87 my ($self) = @_;
441              
442 13         59 my $stack = $self->get_caller;
443 13   66     205 my $settings = $self->has_app && $self->app->settings;
444 13   33     478 my $session = $self->has_app && $self->app->_has_session && $self->app->session->data;
445 13   66     155 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       152 $_ = $_ ? $self->dumper($_) : 'undefined' for $settings, $session, $env;
449              
450 13         352 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         32 my @stack;
461              
462 13         30 my $deepness = 0;
463 13         146 while ( my ( $package, $file, $line ) = caller( $deepness++ ) ) {
464 206         1398 push @stack, "$package in $file l. $line";
465             }
466              
467 13         169 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 173     173   10515 my $value = shift;
475              
476 173 50       630 return if !defined $value;
477              
478 173         447 $value =~ s/&/&/g;
479 173         376 $value =~ s/
480 173         396 $value =~ s/>/>/g;
481 173         891 $value =~ s/'/'/g;
482 173         406 $value =~ s/"/"/g;
483              
484 173         1063 return $value;
485             }
486              
487             1;
488              
489             __END__