blib/lib/Dancer/Error.pm | |||
---|---|---|---|
Criterion | Covered | Total | % |
statement | 158 | 165 | 95.7 |
branch | 46 | 54 | 85.1 |
condition | 11 | 16 | 68.7 |
subroutine | 32 | 33 | 96.9 |
pod | 11 | 12 | 91.6 |
total | 258 | 280 | 92.1 |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | package Dancer::Error; | ||||||
2 | our $AUTHORITY = 'cpan:SUKRIA'; | ||||||
3 | #ABSTRACT: class for representing fatal errors | ||||||
4 | $Dancer::Error::VERSION = '1.3520'; | ||||||
5 | 168 | 168 | 68350 | use strict; | |||
168 | 414 | ||||||
168 | 5564 | ||||||
6 | 168 | 168 | 911 | use warnings; | |||
168 | 395 | ||||||
168 | 4412 | ||||||
7 | 168 | 168 | 872 | use Carp; | |||
168 | 376 | ||||||
168 | 9651 | ||||||
8 | 168 | 168 | 1189 | use Scalar::Util qw(blessed); | |||
168 | 453 | ||||||
168 | 9325 | ||||||
9 | |||||||
10 | 168 | 168 | 1187 | use base 'Dancer::Object'; | |||
168 | 469 | ||||||
168 | 20858 | ||||||
11 | |||||||
12 | 168 | 168 | 1750 | use Dancer::Response; | |||
168 | 475 | ||||||
168 | 4913 | ||||||
13 | 168 | 168 | 78370 | use Dancer::Renderer; | |||
168 | 481 | ||||||
168 | 5719 | ||||||
14 | 168 | 168 | 1291 | use Dancer::Config 'setting'; | |||
168 | 445 | ||||||
168 | 7241 | ||||||
15 | 168 | 168 | 1047 | use Dancer::Logger; | |||
168 | 427 | ||||||
168 | 3111 | ||||||
16 | 168 | 168 | 875 | use Dancer::Factory::Hook; | |||
168 | 363 | ||||||
168 | 3176 | ||||||
17 | 168 | 168 | 921 | use Dancer::Session; | |||
168 | 376 | ||||||
168 | 4836 | ||||||
18 | 168 | 168 | 1018 | use Dancer::FileUtils qw(open_file); | |||
168 | 454 | ||||||
168 | 7504 | ||||||
19 | 168 | 168 | 1050 | use Dancer::Engine; | |||
168 | 460 | ||||||
168 | 4003 | ||||||
20 | 168 | 168 | 1001 | use Dancer::Exception qw(:all); | |||
168 | 438 | ||||||
168 | 189422 | ||||||
21 | |||||||
22 | Dancer::Factory::Hook->instance->install_hooks( | ||||||
23 | qw/before_error_render after_error_render before_error_init/); | ||||||
24 | |||||||
25 | sub init { | ||||||
26 | 55 | 55 | 1 | 166 | my ($self) = @_; | ||
27 | |||||||
28 | 55 | 241 | Dancer::Factory::Hook->instance->execute_hooks('before_error_init', $self); | ||||
29 | |||||||
30 | 55 | 304 | $self->attributes_defaults( | ||||
31 | title => 'Error ' . $self->code, | ||||||
32 | type => 'runtime error', | ||||||
33 | ); | ||||||
34 | |||||||
35 | 55 | 100 | 217 | $self->has_serializer | |||
36 | and return; | ||||||
37 | |||||||
38 | 48 | 205 | my $html_output = "" . $self->{type} . ""; |
||||
39 | 48 | 193 | $html_output .= $self->backtrace; | ||||
40 | 48 | 204 | $html_output .= $self->environment; | ||||
41 | |||||||
42 | 48 | 273 | $self->{message} = $html_output; | ||||
43 | } | ||||||
44 | |||||||
45 | 55 | 55 | 0 | 190 | sub has_serializer { setting('serializer') } | ||
46 | 162 | 162 | 1 | 812 | sub code { $_[0]->{code} } | ||
47 | 63 | 63 | 1 | 880 | sub title { $_[0]->{title} } | ||
48 | 76 | 76 | 1 | 264 | sub message { $_[0]->{message} } | ||
49 | 96 | 96 | 1 | 374 | sub exception { $_[0]->{exception} } | ||
50 | |||||||
51 | sub backtrace { | ||||||
52 | 48 | 48 | 1 | 113 | my ($self) = @_; | ||
53 | |||||||
54 | 48 | 100 | 188 | $self->{message} ||= ""; | |||
55 | my $message = | ||||||
56 | 48 | 162 | qq|| . _html_encode($self->{message}) . ""; |
||||
57 | |||||||
58 | # the default perl warning/error pattern | ||||||
59 | 48 | 371 | my ($file, $line) = ($message =~ /at (\S+) line (\d+)/); | ||||
60 | |||||||
61 | # the Devel::SimpleTrace pattern | ||||||
62 | 48 | 100 | 66 | 268 | ($file, $line) = ($message =~ /at.*\((\S+):(\d+)\)/) | ||
63 | unless $file and $line; | ||||||
64 | |||||||
65 | # no file/line found, cannot open a file for context | ||||||
66 | 48 | 100 | 66 | 265 | return $message unless ($file and $line); | ||
67 | |||||||
68 | # file and line are located, let's read the source Luke! | ||||||
69 | 36 | 50 | 138 | my $fh = open_file('<', $file) or return $message; | |||
70 | 36 | 1159 | my @lines = <$fh>; | ||||
71 | 36 | 4753 | close $fh; | ||||
72 | |||||||
73 | 36 | 131 | my $backtrace = $message; | ||||
74 | |||||||
75 | 36 | 146 | $backtrace | ||||
76 | .= qq| | . "$file around line $line" . " "; |
||||||
77 | |||||||
78 | 36 | 73 | $backtrace .= qq||; |
||||
79 | |||||||
80 | 36 | 97 | $line--; | ||||
81 | 36 | 50 | 123 | my $start = (($line - 3) >= 0) ? ($line - 3) : 0; | |||
82 | 36 | 50 | 111 | my $stop = (($line + 3) < scalar(@lines)) ? ($line + 3) : scalar(@lines); | |||
83 | |||||||
84 | 36 | 127 | for (my $l = $start; $l <= $stop; $l++) { | ||||
85 | 252 | 436 | chomp $lines[$l]; | ||||
86 | |||||||
87 | 252 | 100 | 419 | if ($l == $line) { | |||
88 | 36 | 117 | $backtrace | ||||
89 | .= qq|| | ||||||
90 | . tabulate($l + 1, $stop + 1) | ||||||
91 | . qq| | | ||||||
92 | . _html_encode($lines[$l]) | ||||||
93 | . "\n"; | ||||||
94 | } | ||||||
95 | else { | ||||||
96 | 216 | 420 | $backtrace | ||||
97 | .= qq|| | ||||||
98 | . tabulate($l + 1, $stop + 1) | ||||||
99 | . " " | ||||||
100 | . _html_encode($lines[$l]) . "\n"; | ||||||
101 | } | ||||||
102 | } | ||||||
103 | 36 | 78 | $backtrace .= ""; | ||||
104 | |||||||
105 | |||||||
106 | 36 | 444 | return $backtrace; | ||||
107 | } | ||||||
108 | |||||||
109 | sub tabulate { | ||||||
110 | 252 | 252 | 1 | 430 | my ($number, $max) = @_; | ||
111 | 252 | 335 | my $len = length($max); | ||||
112 | 252 | 100 | 774 | return $number if length($number) == $len; | |||
113 | 10 | 34 | return " $number"; | ||||
114 | } | ||||||
115 | |||||||
116 | sub dumper { | ||||||
117 | 144 | 144 | 1 | 750 | my $obj = shift; | ||
118 | 144 | 50 | 555 | return "Unavailable without Data::Dumper" | |||
119 | unless Dancer::ModuleLoader->load('Data::Dumper'); | ||||||
120 | |||||||
121 | |||||||
122 | # Take a copy of the data, so we can mask sensitive-looking stuff: | ||||||
123 | 144 | 50 | 401 | my $data = Dancer::ModuleLoader->load('Clone') ? | |||
124 | Clone::clone($obj) | ||||||
125 | : eval Data::Dumper->new([$obj])->Purity(1)->Terse(1)->Deepcopy(1)->Dump; | ||||||
126 | |||||||
127 | 144 | 100 | 1045 | $data = {%$data} if blessed($data); | |||
128 | |||||||
129 | 144 | 372 | my $censored = _censor($data); | ||||
130 | |||||||
131 | #use Data::Dumper; | ||||||
132 | 144 | 816 | my $dd = Data::Dumper->new([$data]); | ||||
133 | 144 | 4374 | $dd->Terse(1)->Quotekeys(0)->Indent(1)->Sortkeys(1); | ||||
134 | 144 | 3733 | my $content = $dd->Dump(); | ||||
135 | 144 | 119033 | $content =~ s{(\s*)(\S+)(\s*)=>}{$1$2$3 =>}g; | ||||
136 | 144 | 100 | 542 | if ($censored) { | |||
137 | 47 | 50 | 258 | $content | |||
138 | .= "\n\nNote: Values of $censored sensitive-looking key" | ||||||
139 | . ($censored == 1 ? '' : 's') | ||||||
140 | . " hidden\n"; | ||||||
141 | } | ||||||
142 | 144 | 3176 | return $content; | ||||
143 | } | ||||||
144 | |||||||
145 | # Given a hashref, censor anything that looks sensitive. Returns number of | ||||||
146 | # items which were "censored". | ||||||
147 | sub _censor { | ||||||
148 | 391 | 391 | 2546 | my ( $hash, $recursecount ) = @_; | |||
149 | 391 | 100 | 1128 | $recursecount ||= 0; | |||
150 | |||||||
151 | # we're checking recursion ourselves, no need to warn | ||||||
152 | 168 | 168 | 1600 | no warnings 'recursion'; | |||
168 | 505 | ||||||
168 | 213570 | ||||||
153 | |||||||
154 | 391 | 100 | 752 | if ( $recursecount++ > 100 ) { | |||
155 | 1 | 57 | warn "Data exceeding 100 levels, truncating\n"; | ||||
156 | 1 | 8 | return $hash; | ||||
157 | } | ||||||
158 | |||||||
159 | 390 | 50 | 33 | 1322 | if (!$hash || ref $hash ne 'HASH') { | ||
160 | 0 | 0 | carp "_censor given incorrect input: $hash"; | ||||
161 | 0 | 0 | return; | ||||
162 | } | ||||||
163 | |||||||
164 | 390 | 548 | my $censored = 0; | ||||
165 | 390 | 1193 | for my $key (keys %$hash) { | ||||
166 | 3412 | 100 | 12074 | if (ref $hash->{$key} eq 'HASH') { | |||
100 | |||||||
167 | 246 | 748 | $censored += _censor( $hash->{$key}, $recursecount ); | ||||
168 | } | ||||||
169 | elsif ($key =~ /(pass|card?num|pan|cvv2?|ccv|secret|private_key|cookie_key)/i) { | ||||||
170 | 324 | 526 | $hash->{$key} = "Hidden (looks potentially sensitive)"; | ||||
171 | 324 | 462 | $censored++; | ||||
172 | } | ||||||
173 | } | ||||||
174 | |||||||
175 | 390 | 906 | return $censored; | ||||
176 | } | ||||||
177 | |||||||
178 | # Replaces the entities that are illegal in (X)HTML. | ||||||
179 | sub _html_encode { | ||||||
180 | 300 | 300 | 493 | my $value = shift; | |||
181 | |||||||
182 | 300 | 646 | $value =~ s/&/&/g; | ||||
183 | 300 | 452 | $value =~ s/</g; | ||||
184 | 300 | 599 | $value =~ s/>/>/g; | ||||
185 | 300 | 627 | $value =~ s/'/'/g; | ||||
186 | 300 | 541 | $value =~ s/"/"/g; | ||||
187 | |||||||
188 | 300 | 1080 | return $value; | ||||
189 | } | ||||||
190 | |||||||
191 | sub render { | ||||||
192 | 53 | 53 | 1 | 688 | my $self = shift; | ||
193 | |||||||
194 | 53 | 204 | my $serializer = setting('serializer'); | ||||
195 | 53 | 100 | 193 | my $ops = { title => $self->title, | |||
196 | message => $self->message, | ||||||
197 | code => $self->code, | ||||||
198 | defined $self->exception ? ( exception => $self->exception ) : (), | ||||||
199 | }; | ||||||
200 | 53 | 324 | Dancer::Factory::Hook->instance->execute_hooks('before_error_render', $self, $ops); | ||||
201 | 53 | 116 | my $response; | ||||
202 | try { | ||||||
203 | 53 | 100 | 53 | 1911 | $response = $serializer ? $self->_render_serialized($ops) : $self->_render_html($ops); | ||
204 | } continuation { | ||||||
205 | 0 | 0 | 0 | my ($continuation) = @_; | |||
206 | # If we have a Route continuation, run the after hook, then | ||||||
207 | # propagate the continuation | ||||||
208 | 0 | 0 | Dancer::Factory::Hook->instance->execute_hooks('after_error_render', $response); | ||||
209 | 0 | 0 | $continuation->rethrow(); | ||||
210 | 53 | 558 | }; | ||||
211 | 53 | 1202 | Dancer::Factory::Hook->instance->execute_hooks('after_error_render', $response); | ||||
212 | 53 | 670 | $response; | ||||
213 | } | ||||||
214 | |||||||
215 | sub _render_serialized { | ||||||
216 | 7 | 7 | 25 | my $self = shift; | |||
217 | |||||||
218 | 7 | 100 | 23 | my $message = | |||
219 | !ref $self->message ? {error => $self->message} : $self->message; | ||||||
220 | |||||||
221 | 7 | 100 | 66 | 36 | if (ref $message eq 'HASH' && defined $self->exception) { | ||
222 | 2 | 50 | 6 | if (blessed($self->exception)) { | |||
223 | 0 | 0 | $message->{exception} = ref($self->exception); | ||||
224 | 0 | 0 | $message->{exception} =~ s/^Dancer::Exception:://; | ||||
225 | } else { | ||||||
226 | 2 | 4 | $message->{exception} = $self->exception; | ||||
227 | } | ||||||
228 | } | ||||||
229 | |||||||
230 | 7 | 100 | 27 | if (setting('show_errors')) { | |||
231 | 6 | 20 | Dancer::Response->new( | ||||
232 | status => $self->code, | ||||||
233 | content => Dancer::Serializer->engine->serialize($message), | ||||||
234 | headers => ['Content-Type' => Dancer::Serializer->engine->content_type] | ||||||
235 | ); | ||||||
236 | } | ||||||
237 | |||||||
238 | # if show_errors is disabled, we don't expose the real error message to the | ||||||
239 | # outside world | ||||||
240 | else { | ||||||
241 | 1 | 4 | Dancer::Response->new( | ||||
242 | status => $self->code, | ||||||
243 | content => "An internal error occured", | ||||||
244 | ); | ||||||
245 | } | ||||||
246 | |||||||
247 | } | ||||||
248 | |||||||
249 | sub _render_html { | ||||||
250 | 46 | 46 | 94 | my $self = shift; | |||
251 | 46 | 72 | my $ops = shift; | ||||
252 | |||||||
253 | # I think it is irrelevant to look into show_errors. In the | ||||||
254 | # template the user can hide them if she desires so. | ||||||
255 | 46 | 100 | 130 | if (setting("error_template")) { | |||
256 | 18 | 39 | my $template_name = setting("error_template"); | ||||
257 | 18 | 92 | my $content = Dancer::Engine->engine("template")->apply_renderer($template_name, $ops); | ||||
258 | 18 | 56 | return Dancer::Response->new( | ||||
259 | status => $self->code, | ||||||
260 | headers => ['Content-Type' => 'text/html'], | ||||||
261 | content => $content); | ||||||
262 | } else { | ||||||
263 | 28 | 100 | 97 | return Dancer::Response->new( | |||
264 | status => $self->code, | ||||||
265 | headers => ['Content-Type' => 'text/html'], | ||||||
266 | content => | ||||||
267 | Dancer::Renderer->html_page($self->title, $self->message, 'error') | ||||||
268 | ) if setting('show_errors'); | ||||||
269 | |||||||
270 | 19 | 66 | return Dancer::Renderer->render_error($self->code); | ||||
271 | } | ||||||
272 | } | ||||||
273 | |||||||
274 | sub environment { | ||||||
275 | 48 | 48 | 1 | 108 | my ($self) = @_; | ||
276 | |||||||
277 | 48 | 264 | my $request = Dancer::SharedData->request; | ||||
278 | 48 | 114 | my $r_env = {}; | ||||
279 | 48 | 100 | 245 | $r_env = $request->env if defined $request; | |||
280 | |||||||
281 | 48 | 188 | my $env = | ||||
282 | qq| Environment | |
||||||
283 | . dumper($r_env) | ||||||
284 | . ""; | ||||||
285 | 48 | 451 | my $settings = | ||||
286 | qq| Settings | |
||||||
287 | . dumper(Dancer::Config->settings) | ||||||
288 | . ""; | ||||||
289 | 48 | 222 | my $source = | ||||
290 | qq| Stack | |
||||||
291 | . $self->get_caller | ||||||
292 | . ""; | ||||||
293 | 48 | 115 | my $session = ""; | ||||
294 | |||||||
295 | 48 | 100 | 178 | if (setting('session')) { | |||
296 | 47 | 333 | $session = | ||||
297 | qq[ Session ] |
||||||
298 | . dumper(Dancer::Session->get) | ||||||
299 | . ""; | ||||||
300 | } | ||||||
301 | 48 | 1635 | return "$source $settings $session $env"; | ||||
302 | } | ||||||
303 | |||||||
304 | sub get_caller { | ||||||
305 | 48 | 48 | 1 | 128 | my ($self) = @_; | ||
306 | 48 | 94 | my @stack; | ||||
307 | |||||||
308 | 48 | 84 | my $deepness = 0; | ||||
309 | 48 | 468 | while (my ($package, $file, $line) = caller($deepness++)) { | ||||
310 | 633 | 3754 | push @stack, "$package in $file l. $line"; | ||||
311 | } | ||||||
312 | |||||||
313 | 48 | 520 | return join("\n", reverse(@stack)); | ||||
314 | } | ||||||
315 | |||||||
316 | 1; | ||||||
317 | |||||||
318 | __END__ |