File Coverage

blib/lib/Catalyst/Action/RenderView/ErrorHandler.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1             package Catalyst::Action::RenderView::ErrorHandler;
2             {
3             $Catalyst::Action::RenderView::ErrorHandler::VERSION = '0.100166';
4             }
5             # ABSTRACT: Custom errorhandling in deployed applications
6              
7 1     1   24428 use warnings;
  1         2  
  1         41  
8 1     1   6 use strict;
  1         2  
  1         36  
9 1     1   5 use Carp;
  1         2  
  1         105  
10 1     1   1057 use MRO::Compat;
  1         3377  
  1         31  
11              
12 1     1   1027 use Class::Inspector;
  1         4380  
  1         35  
13              
14 1     1   503 use Moose;
  0            
  0            
15              
16             extends 'Catalyst::Action::RenderView';
17              
18             has 'handlers' => (is => 'rw', isa => 'ArrayRef', default => sub { [] });
19             has 'actions' => (is => 'rw', isa => 'HashRef', default => sub { {} });
20              
21             sub action {
22             my $self = shift;
23             my $id = shift;
24             return $self->actions->{$id};
25             }
26             sub execute {
27             my $self = shift;
28             my ($controller, $c) = @_;
29              
30             my $rv = $self->maybe::next::method(@_);
31             return 1 unless (scalar(@{ $c->error }) or $c->res->status =~ /^4\d\d/);
32             return 1 if ($c->debug and !$c->config->{'error_handler'}->{'enable'});
33             $self->actions({});
34             $self->handlers([]);
35             $self->_parse_config($c);
36             $self->handle($c);
37             }
38              
39             sub handle {
40             my $self = shift;
41             my $c = shift;
42              
43             my $code = $c->res->status;
44             my $has_errors = scalar( @{ $c->error } );
45             if ($code == 200 and $has_errors) {
46             $code = 500; # We default to 500 for errors unless something else has been set.
47             $c->res->status($code);
48             }
49             my $body;
50             foreach my $h (@{ $self->handlers }) {
51             if ($code =~ $h->{decider}) {
52             eval {
53             $body = $self->render($c, $h->{render});
54             };
55             if ($@ and $@ =~ m/Error rendering/) {
56             # we continue to next step
57             next;
58             } elsif ($@) {
59             croak $@;
60             }
61             # We have successfully rendered something, so we clear errors
62             # and set content
63             $c->res->body($body);
64             if($h->{actions}) {
65             foreach my $a (@{ $h->{actions} }) {
66             next unless defined $a;
67             next if $a->ignore($c->req->path);
68             $a->perform($c);
69             }
70             }
71             $c->clear_errors;
72              
73             # We have some actions to perform
74             last ;
75             }
76             }
77             if ($has_errors and $code =~ m/[23]\d{2}/) {
78             $c->res->status(500); # We overwrite 2xx and 3xx with 500 if we had errors
79             }
80             }
81             sub render {
82             my $self = shift;
83             my $c = shift;
84             my $args = shift;
85              
86             if ($args->{static}) {
87             my $file = ($args->{static} !~ m|^/|)
88             ? $c->path_to($args->{static})
89             : $args->{static}
90             ;
91             open(my $fh, "<", $file) or croak "cannot read: $file";
92             return $fh;
93             } elsif ($args->{template}) {
94             # We try to render it using the view, but will catch errors we hope
95             my $content;
96             my %data = $self->_render_stash( $c );
97             $data{additional_template_paths} ||= [ $c->path_to('root') ]; #compatible w/ earlier version
98             eval {
99             $content = $c->view->render( $c, $args->{template}, \%data );
100             };
101             unless ($@) {
102             return $content;
103             } else {
104             croak "Error rendering - TT error on template " . $args->{template};
105             }
106             } else {
107             croak "Error rendering - no template or static";
108             }
109             }
110              
111              
112             sub _render_stash {
113             my $self = shift;
114             my $c = shift;
115             return () if !exists $c->config->{'error_handler'}->{'expose_stash'};
116             my $es = $c->config->{'error_handler'}->{'expose_stash'};
117             return map { $_ =~ $es ? ($_ => $c->stash->{$_}) : () } keys %{$c->stash}
118             if ( ref($es) eq 'Regexp' );
119             return map { $_ => $c->stash->{$_} } @{$es} if ( ref($es) eq 'ARRAY' );
120             return ( $es => $c->stash->{$es} ) if ( !ref($es) );
121             }
122              
123             sub _parse_config {
124             my $self = shift;
125             my $c = shift;
126              
127             $self->_parse_actions($c, $c->config->{'error_handler'}->{'actions'});
128             $self->_parse_handlers($c, $c->config->{'error_handler'}->{'handlers'});
129             }
130              
131             sub _parse_actions {
132             my $self = shift;
133             my $c = shift;
134              
135             my $actions = shift;
136             unless ($actions and scalar(@$actions)) {
137             # We dont have any actions, lets create a default log action.
138             my $action = {
139             type => 'Log',
140             id => 'default-log-error',
141             level => 'error',
142             };
143             push @$actions, $action;
144             }
145             foreach my $action (@$actions) {
146             $self->_expand($c, $action );
147             my $class;
148             if ($action->{'type'} and $action->{'type'} =~ /^\+/) {
149             $class = $action->{'type'};
150             $class =~ s/^\+//;
151             } elsif($action->{'type'}) {
152             $class = ref($self) . "::Action::" . $action->{'type'};
153             } else {
154             croak "No type specified";
155             }
156              
157             unless(Class::Inspector->loaded($class)) {
158             eval "require $class";
159             if ($@) {
160             croak "Could not load '$class': $@";
161             }
162             }
163             my $act = $class->new(%$action);
164             $self->actions->{$act->id} = $act;
165             }
166             }
167              
168             sub _parse_handlers {
169             my $self = shift;
170             my $c = shift;
171             my $handlers = shift;
172             my $codes = {};
173             my $blocks = {};
174             my $fallback = {
175             render => { static => 'root/static/error.html' },
176             decider => qr/./,
177             actions => [ $self->action('default-log-error') ? $self->action('default-log-error') : undef ]
178             };
179             foreach my $status (keys %$handlers) {
180             my $handler = {
181             actions => [map { $self->action($_) } @{ $handlers->{$status}->{actions}}],
182             render => ($handlers->{$status}->{template}
183             ? { template => $handlers->{$status}->{template} }
184             : { static => $handlers->{$status}->{static} }
185             ),
186             };
187              
188             if ($status =~ m/\dxx/) {
189             #codegroup
190             my $decider = $status;
191             $decider =~ s/x/\\d/g;
192             $handler->{decider} = qr/$decider/;
193             $blocks->{$status} = $handler;
194             } elsif ($status =~ m/\d{3}/) {
195             $handler->{decider} = qr/$status/;
196             $codes->{$status} = $handler;
197             } elsif ($status eq 'fallback') {
198             $handler->{decider} = qr/./;
199             $fallback = $handler;
200             } else {
201             carp "Wrong status: $status specified";
202             }
203             }
204             my @handlers;
205             push(@handlers, values(%$codes), values(%$blocks), $fallback);
206             $self->handlers(\@handlers);
207             }
208              
209             sub _expand {
210             my $self = shift;
211             my $c = shift;
212             my $h = shift;
213              
214             foreach my $k (keys %$h) {
215             my $v = $h->{$k};
216             my $name = $c->config->{name};
217             $v =~ s/__MYAPP__/$name/g;
218             $h->{$k} = $v;
219             }
220             }
221             1; # Magic true value required at end of module
222              
223             __END__
224              
225             =pod
226              
227             =encoding utf-8
228              
229             =head1 NAME
230              
231             Catalyst::Action::RenderView::ErrorHandler - Custom errorhandling in deployed applications
232              
233             =head1 VERSION
234              
235             version 0.100166
236              
237             =head1 SYNOPSIS
238              
239             sub end : ActionClass('RenderView::ErrorHandler') {}
240              
241             =head1 DESCRIPTION
242              
243             We all dread the Please come back later screen. Its uninformative, non-
244             helpful, and in general an awfull default thing to do.
245              
246             This module lets the developer configure what happens in case of emergency.
247              
248             =over 4
249              
250             =item If you want the errors emailed to you? You can have it.
251              
252             =item Want them logged somewhere as well? suresure, we will do it.
253              
254             =item Custom errorpage that fits your design you say? Aw come on :)
255              
256             =back
257              
258             =head1 CONFIGURATION AND ENVIRONMENT
259              
260             We take our configuration from C<< $c->config->{'error_handler'} >>. If you do no
261             configuration, the default is to look for the file 'root/static/error.html',
262             and serve that as a static file. If all you want is to show a custom, static,
263             error page, all you have to do is install the module and add it to your end
264             action.
265              
266             =head2 OPTIONS
267              
268             =head3 actions
269              
270             Is an array of actions you want taken. Each value should be an hashref
271             with atleast the following keys:
272              
273             =head4 enable
274              
275             If this is true, we will act even in debug mode. Great for getting debug logs AND
276             error-handler templates rendered.
277              
278             =head4 ignorePath (Optional)
279              
280             If this regex matches C<< $c->req->path >>, the action will be skipped. Useful
281             if you want to exclude some paths from triggering emails for instance. Can be
282             used to ignore hacking attempts against PhpMyAdmin and such.
283              
284             =head4 type
285              
286             Can be Log for builtin, or you can prefix it with a +, then
287             we will use it as a fully qualified class name.
288              
289             The most excellent Stefan Profanter wrote
290             L<Catalyst::Action::RenderView::ErrorHandler::Action::Email>, which lets you send
291             templated emails on errors.
292              
293             =head4 id
294              
295             The id you want to have for this action
296              
297             =head4 expose_stash
298              
299             Either a list of keys, a regexp for matching keys, or a simple string to denote
300             a single stash value.
301              
302             stash keys that match will be available to template handlers
303              
304             =head3 handlers
305              
306             Configuration as to what to do when an error occurs. We always need
307             to show something to the user, so thats a given. Each handler represents
308             an error state, and a given handler can perform any given number of actions
309             in addition to rendering or sending something to the browser/client.
310              
311             =over 4
312              
313             =item HTTP status codes (404, 500 etc)
314              
315             =item HTTP status code groups (4xx, 5xx etc)
316              
317             =item "fallback" - default action taken on error.
318              
319             =back
320              
321             The action is decided in that order.
322              
323             =head4 template
324              
325             Will be sent to your default_view for processing. Can use c.errors as needed
326              
327             =head4 static
328              
329             Will be read and served as a static file. This is the only option for fallback,
330             since fallback will be used in case rendering a template failed for some reason.
331              
332             If the given string begins with an '/', we treat it as an absolute path and try
333             to read it directly. If not, we pass it trough C<< $c->path_to() >> to get an
334             absolute path to read from.
335              
336             =head2 EXAMPLE
337              
338             error_handler:
339             actions:
340             # Check out
341             # L<Catalyst::Action::RenderView::ErrorHandler::Action::Email> if
342             # you want to send email from an action
343             - type: Email
344             id: email-devel
345             to: andreas@example.com
346             subject: __MYAPP__ errors:
347             - type: Log
348             id: log-server
349             level: error
350             handlers:
351             5xx:
352             template: root/error/5xx.tt
353             actions:
354             - email-devel
355             - log-server
356             500:
357             template: root/error/500.tt
358             actions:
359             - log-server
360             fallback:
361             static: root/static/error.html
362             actions:
363             - email-devel
364              
365             =head1 INTERFACE
366              
367             =head2 IMPLEMENTED METHODS
368              
369             =head3 execute
370              
371             Implemented to comply with L<Catalyst::Action> interface.
372              
373             It checks if there are errors, if not it it simply returns, assuming
374             L<Catalyst::Action::RenderView> has handled the job. If there are errors
375             we parse the configuration and try to build our handlers.
376              
377             Then it calls C<< $self->handle >>.
378              
379             =head2 METHODS
380              
381             =head3 handle
382              
383             Handles a request, by finding the propper handler.
384              
385             Once a handler is found, it calls render if there are statics or
386             templates, and then performs all actions (if any).
387              
388             =head3 render
389              
390             Given either a static file or a template, it will attempt to render
391             it and send it to C<< $context->res->body >>.
392              
393             =head2 PRIVATE METHODS
394              
395             =head3 _render_stash
396              
397             return a list of rendering data in template from exposed stash
398              
399             =head2 INHERITED METHODS
400              
401             =head3 meta
402              
403             Inherited from L<Moose>
404              
405             =head1 DEPENDENCIES
406              
407             L<Catalyst::Action::RenderView>
408              
409             =head1 SEE ALSO
410              
411             =over 4
412              
413             =item L<Catalyst::Action::RenderView::ErrorHandler::Action::Email>
414              
415             =back
416              
417             =head1 Thanks
418              
419             =over 4
420              
421             =item zdk L<https://github.com/zdk>
422              
423             =item Stefan Profanter L<https://metacpan.org/author/PROFANTER>
424              
425             =back
426              
427             =head1 AUTHOR
428              
429             Andreas Marienborg <andremar@cpan.org>
430              
431             =head1 CONTRIBUTORS
432              
433             =over 4
434              
435             =item *
436              
437             Andreas Marienborg <andreas.marienborg@gmail.com>
438              
439             =item *
440              
441             Pinnapong Silpsakulsuk <ping@abctech-thailand.com>
442              
443             =item *
444              
445             zdk <nx2zdk@gmail.com>
446              
447             =back
448              
449             =head1 COPYRIGHT AND LICENSE
450              
451             This software is copyright (c) 2013 by Andreas Marienborg.
452              
453             This is free software; you can redistribute it and/or modify it under
454             the same terms as the Perl 5 programming language system itself.
455              
456             =cut