PageRenderTime 57ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/Dancer2/Core/Error.pm

https://github.com/mickeyn/Dancer2
Perl | 579 lines | 484 code | 83 blank | 12 comment | 18 complexity | 483988c44a628a573f07b9e52b0a91db MD5 | raw file
  1. # ABSTRACT: Class representing fatal errors
  2. package Dancer2::Core::Error;
  3. use Moo;
  4. use Carp;
  5. use Dancer2::Core::Types;
  6. use Dancer2::Core::HTTP;
  7. use Data::Dumper;
  8. use Dancer2::FileUtils 'path';
  9. =head1 SYNOPSIS
  10. # taken from send_file:
  11. use Dancer2::Core::Error;
  12. my $error = Dancer2::Core::Error->new(
  13. status => 404,
  14. message => "No such file: `$path'"
  15. );
  16. Dancer2::Core::Response->set($error->render);
  17. =head1 DESCRIPTION
  18. With Dancer2::Core::Error you can throw reasonable-looking errors to the user
  19. instead of crashing the application and filling up the logs.
  20. This is usually used in debugging environments, and it's what Dancer2 uses as
  21. well under debugging to catch errors and show them on screen.
  22. =method my $error=new Dancer2::Core::Error(status => 404, message => "No such file: `$path'");
  23. Create a new Dancer2::Core::Error object. For available arguments see ATTRIBUTES.
  24. =cut
  25. =method supported_hooks ();
  26. =cut
  27. =attr show_errors
  28. =cut
  29. has show_errors => (
  30. is => 'ro',
  31. isa => Bool,
  32. default => sub {
  33. $_[0]->context->app->setting('show_errors') if $_[0]->has_context;
  34. },
  35. );
  36. =attr charset
  37. =cut
  38. has charset => (
  39. is => 'ro',
  40. isa => Str,
  41. default => sub {'UTF-8'},
  42. );
  43. =attr type
  44. The error type.
  45. =cut
  46. has type => (
  47. is => 'ro',
  48. isa => Str,
  49. default => sub {'Runtime Error'},
  50. );
  51. =attr title
  52. The title of the error page.
  53. This is only an attribute getter, you'll have to set it at C<new>.
  54. =cut
  55. has title => (
  56. is => 'ro',
  57. isa => Str,
  58. lazy => 1,
  59. builder => '_build_title',
  60. );
  61. sub _build_title {
  62. my ($self) = @_;
  63. my $title = 'Error ' . $self->status;
  64. if ( my $msg = Dancer2::Core::HTTP->status_message($self->status) ) {
  65. $title .= ' - ' . $msg;
  66. }
  67. return $title;
  68. }
  69. has template => (
  70. is => 'ro',
  71. # isa => sub { ref($_[0]) eq 'SCALAR' || ReadableFilePath->(@_) },
  72. lazy => 1,
  73. builder => '_build_error_template',
  74. );
  75. sub _build_error_template {
  76. my ($self) = @_;
  77. # look for a template named after the status number.
  78. # E.g.: views/404.tt for a TT template
  79. return $self->status
  80. if -f $self->context->app->engine('template')
  81. ->view_pathname( $self->status );
  82. return undef;
  83. }
  84. has static_page => (
  85. is => 'ro',
  86. lazy => 1,
  87. builder => '_build_static_page',
  88. );
  89. sub _build_static_page {
  90. my ($self) = @_;
  91. # TODO there must be a better way to get it
  92. my $public_dir = $ENV{DANCER_PUBLIC}
  93. || ( $self->has_context
  94. && path( $self->context->app->config_location, 'public' ) );
  95. my $filename = sprintf "%s/%d.html", $public_dir, $self->status;
  96. open my $fh, $filename or return undef;
  97. local $/ = undef; # slurp time
  98. return <$fh>;
  99. }
  100. sub default_error_page {
  101. my $self = shift;
  102. require Template::Tiny;
  103. my $uri_base = $self->has_context ?
  104. $self->context->request->uri_base : '';
  105. my $opts = {
  106. title => $self->title,
  107. charset => $self->charset,
  108. content => $self->message,
  109. version => Dancer2->VERSION,
  110. uri_base => $uri_base,
  111. };
  112. Template::Tiny->new->process( \<<"END_TEMPLATE", $opts, \my $output );
  113. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  114. <html>
  115. <head>
  116. <title>[% title %]</title>
  117. <link rel="stylesheet" href="[% uri_base %]/css/error.css" />
  118. <meta http-equiv="Content-type" content="text/html; charset='[% charset %]'" />
  119. </head>
  120. <body>
  121. <h1>[% title %]</h1>
  122. <div id="content">
  123. [% content %]
  124. </div>
  125. <div id="footer">
  126. Powered by <a href="http://perldancer.org/">Dancer2</a> [% version %]
  127. </div>
  128. </body>
  129. </html>
  130. END_TEMPLATE
  131. return $output;
  132. }
  133. =attr status
  134. The status that caused the error.
  135. This is only an attribute getter, you'll have to set it at C<new>.
  136. =cut
  137. has status => (
  138. is => 'ro',
  139. default => sub {500},
  140. isa => Num,
  141. );
  142. =attr message
  143. The message of the error page.
  144. =cut
  145. has message => (
  146. is => 'ro',
  147. isa => Str,
  148. );
  149. sub full_message {
  150. my ($self) = @_;
  151. my $html_output = "<h2>" . $self->type . "</h2>";
  152. $html_output .= $self->backtrace;
  153. $html_output .= $self->environment;
  154. return $html_output;
  155. }
  156. has serializer => (
  157. is => 'ro',
  158. isa => ConsumerOf ['Dancer2::Core::Role::Serializer'],
  159. predicate => 1,
  160. );
  161. has session => (
  162. is => 'ro',
  163. isa => ConsumerOf ['Dancer2::Core::Role::Session'],
  164. );
  165. has context => (
  166. is => 'ro',
  167. isa => InstanceOf ['Dancer2::Core::Context'],
  168. predicate => 1,
  169. );
  170. sub BUILD {
  171. my ($self) = @_;
  172. $self->has_context &&
  173. $self->context->app->execute_hook( 'core.error.init', $self );
  174. }
  175. has exception => (
  176. is => 'ro',
  177. isa => Str,
  178. predicate => 1,
  179. );
  180. has response => (
  181. is => 'rw',
  182. lazy => 1,
  183. default => sub {
  184. $_[0]->has_context
  185. ? $_[0]->context->response
  186. : Dancer2::Core::Response->new;
  187. },
  188. );
  189. has content_type => (
  190. is => 'ro',
  191. lazy => 1,
  192. default => sub {
  193. my $self = shift;
  194. $self->has_serializer
  195. ? $self->serializer->content_type
  196. : 'text/html'
  197. },
  198. );
  199. has content => (
  200. is => 'ro',
  201. lazy => 1,
  202. default => sub {
  203. my $self = shift;
  204. # Apply serializer
  205. if ( $self->has_serializer ) {
  206. my $content = {
  207. message => $self->message,
  208. title => $self->title,
  209. status => $self->status,
  210. };
  211. $content->{exception} = $self->exception
  212. if $self->has_exception;
  213. return $self->serializer->serialize($content);
  214. }
  215. # Otherwise we check for a template, for a static file and,
  216. # if all else fail, the default error page
  217. if ( $self->has_context and $self->template ) {
  218. return $self->context->app->template(
  219. $self->template,
  220. { title => $self->title,
  221. content => $self->message,
  222. exception => $self->exception,
  223. status => $self->status,
  224. }
  225. );
  226. }
  227. if ( my $content = $self->static_page ) {
  228. return $content;
  229. }
  230. return $self->default_error_page;
  231. },
  232. );
  233. =method throw($response)
  234. Populates the content of the response with the error's information.
  235. If I<$response> is not given, acts on the I<context>
  236. attribute's response.
  237. =cut
  238. sub throw {
  239. my $self = shift;
  240. $self->response(shift) if @_;
  241. croak "error has no response to throw at" unless $self->response;
  242. $self->has_context &&
  243. $self->context->app->execute_hook( 'core.error.before', $self );
  244. my $message = $self->content;
  245. $message .= "\n\n" . $self->exception
  246. if $self->show_errors && defined $self->exception;
  247. $self->response->status( $self->status );
  248. $self->response->content_type( $self->content_type );
  249. $self->response->content($message);
  250. $self->has_context &&
  251. $self->context->app->execute_hook('core.error.after', $self->response);
  252. $self->response->halt(1);
  253. return $self->response;
  254. }
  255. =method backtrace
  256. Create a backtrace of the code where the error is caused.
  257. This method tries to find out where the error appeared according to the actual
  258. error message (using the C<message> attribute) and tries to parse it (supporting
  259. the regular/default Perl warning or error pattern and the L<Devel::SimpleTrace>
  260. output) and then returns an error-highlighted C<message>.
  261. =cut
  262. sub backtrace {
  263. my ($self) = @_;
  264. my $message =
  265. qq|<pre class="error">| . _html_encode( $self->message ) . "</pre>";
  266. # the default perl warning/error pattern
  267. my ( $file, $line ) = ( $message =~ /at (\S+) line (\d+)/ );
  268. # the Devel::SimpleTrace pattern
  269. ( $file, $line ) = ( $message =~ /at.*\((\S+):(\d+)\)/ )
  270. unless $file and $line;
  271. # no file/line found, cannot open a file for context
  272. return $message unless ( $file and $line );
  273. # file and line are located, let's read the source Luke!
  274. my $fh = open_file( '<', $file ) or return $message;
  275. my @lines = <$fh>;
  276. close $fh;
  277. my $backtrace = $message;
  278. $backtrace
  279. .= qq|<div class="title">| . "$file around line $line" . "</div>";
  280. $backtrace .= qq|<pre class="content">|;
  281. $line--;
  282. my $start = ( ( $line - 3 ) >= 0 ) ? ( $line - 3 ) : 0;
  283. my $stop =
  284. ( ( $line + 3 ) < scalar(@lines) ) ? ( $line + 3 ) : scalar(@lines);
  285. for ( my $l = $start; $l <= $stop; $l++ ) {
  286. chomp $lines[$l];
  287. if ( $l == $line ) {
  288. $backtrace
  289. .= qq|<span class="nu">|
  290. . tabulate( $l + 1, $stop + 1 )
  291. . qq|</span> <span style="color: red;">|
  292. . _html_encode( $lines[$l] )
  293. . "</span>\n";
  294. }
  295. else {
  296. $backtrace
  297. .= qq|<span class="nu">|
  298. . tabulate( $l + 1, $stop + 1 )
  299. . "</span> "
  300. . _html_encode( $lines[$l] ) . "\n";
  301. }
  302. }
  303. $backtrace .= "</pre>";
  304. return $backtrace;
  305. }
  306. =method tabulate
  307. Small subroutine to help output nicer.
  308. =cut
  309. sub tabulate {
  310. my ( $number, $max ) = @_;
  311. my $len = length($max);
  312. return $number if length($number) == $len;
  313. return " $number";
  314. }
  315. =head2 dumper
  316. This uses L<Data::Dumper> to create nice content output with a few predefined
  317. options.
  318. =cut
  319. sub dumper {
  320. my $obj = shift;
  321. # Take a copy of the data, so we can mask sensitive-looking stuff:
  322. my %data = %$obj;
  323. my $censored = _censor( \%data );
  324. #use Data::Dumper;
  325. my $dd = Data::Dumper->new( [ \%data ] );
  326. $dd->Terse(1)->Quotekeys(0)->Indent(1);
  327. my $content = $dd->Dump();
  328. $content =~ s{(\s*)(\S+)(\s*)=>}{$1<span class="key">$2</span>$3 =&gt;}g;
  329. if ($censored) {
  330. $content
  331. .= "\n\nNote: Values of $censored sensitive-looking keys hidden\n";
  332. }
  333. return $content;
  334. }
  335. =method environment
  336. A main function to render environment information: the caller (using
  337. C<get_caller>), the settings and environment (using C<dumper>) and more.
  338. =cut
  339. sub environment {
  340. my ($self) = @_;
  341. my $request = $self->has_context ? $self->context->request : 'TODO';
  342. my $r_env = {};
  343. $r_env = $request->env if defined $request;
  344. my $env =
  345. qq|<div class="title">Environment</div><pre class="content">|
  346. . dumper($r_env)
  347. . "</pre>";
  348. my $settings =
  349. qq|<div class="title">Settings</div><pre class="content">|
  350. . dumper( $self->app->settings )
  351. . "</pre>";
  352. my $source =
  353. qq|<div class="title">Stack</div><pre class="content">|
  354. . $self->get_caller
  355. . "</pre>";
  356. my $session = "";
  357. if ( $self->session ) {
  358. $session =
  359. qq[<div class="title">Session</div><pre class="content">]
  360. . dumper( $self->session->data )
  361. . "</pre>";
  362. }
  363. return "$source $settings $session $env";
  364. }
  365. =method get_caller
  366. Creates a stack trace of callers.
  367. =cut
  368. sub get_caller {
  369. my ($self) = @_;
  370. my @stack;
  371. my $deepness = 0;
  372. while ( my ( $package, $file, $line ) = caller( $deepness++ ) ) {
  373. push @stack, "$package in $file l. $line";
  374. }
  375. return join( "\n", reverse(@stack) );
  376. }
  377. # private
  378. # Given a hashref, censor anything that looks sensitive. Returns number of
  379. # items which were "censored".
  380. =func _censor
  381. An private function that tries to censor out content which should be protected.
  382. C<dumper> calls this method to censor things like passwords and such.
  383. =cut
  384. sub _censor {
  385. my $hash = shift;
  386. if ( !$hash || ref $hash ne 'HASH' ) {
  387. carp "_censor given incorrect input: $hash";
  388. return;
  389. }
  390. my $censored = 0;
  391. for my $key ( keys %$hash ) {
  392. if ( ref $hash->{$key} eq 'HASH' ) {
  393. $censored += _censor( $hash->{$key} );
  394. }
  395. elsif ( $key =~ /(pass|card?num|pan|secret)/i ) {
  396. $hash->{$key} = "Hidden (looks potentially sensitive)";
  397. $censored++;
  398. }
  399. }
  400. return $censored;
  401. }
  402. =func my $string=_html_encode ($string);
  403. Private function that replaces illegal entities in (X)HTML with their
  404. escaped representations.
  405. html_encode() doesn't do any UTF black magic.
  406. =cut
  407. # Replaces the entities that are illegal in (X)HTML.
  408. sub _html_encode {
  409. my $value = shift;
  410. $value =~ s/&/&amp;/g;
  411. $value =~ s/</&lt;/g;
  412. $value =~ s/>/&gt;/g;
  413. $value =~ s/'/&#39;/g;
  414. $value =~ s/"/&quot;/g;
  415. return $value;
  416. }
  417. sub _render_html {
  418. my $self = shift;
  419. # error_template defaults to something, always
  420. my $template_name = $self->error_template;
  421. my $ops = {
  422. title => $self->title,
  423. content => $self->message,
  424. status => $self->status,
  425. defined $self->exception ? ( exception => $self->exception ) : (),
  426. };
  427. my $content = $self->template->apply_renderer( $template_name, $ops );
  428. $self->response->status( $self->status );
  429. $self->response->header( 'Content-Type' => 'text/html' );
  430. return $content;
  431. }
  432. 1;