File Coverage

blib/lib/Dancer2/Core/App.pm
Criterion Covered Total %
statement 696 721 96.5
branch 231 270 85.5
condition 85 140 60.7
subroutine 105 112 93.7
pod 15 44 34.0
total 1132 1287 87.9


line stmt bran cond sub pod time code
1             # ABSTRACT: encapsulation of Dancer2 packages
2             package Dancer2::Core::App;
3             $Dancer2::Core::App::VERSION = '2.1.0';
4 160     160   1053082 use Moo;
  160         1131587  
  160         969  
5 160     160   244027 use Carp qw;
  160         338  
  160         11101  
6 160     160   1140 use Scalar::Util 'blessed';
  160         318  
  160         8101  
7 160     160   960 use List::Util ();
  160         313  
  160         3812  
8 160     160   5995 use Module::Runtime 'is_module_name';
  160         11643  
  160         1274  
9 160     160   75862 use Safe::Isa;
  160         86541  
  160         26216  
10 160     160   73409 use Sub::Quote;
  160         1064819  
  160         13941  
11 160     160   115288 use Path::Tiny ();
  160         2043850  
  160         7154  
12 160     160   1597 use Module::Runtime qw< require_module use_module >;
  160         322  
  160         1433  
13 160     160   73755 use Ref::Util qw< is_ref is_arrayref is_globref is_scalarref is_regexpref >;
  160         271710  
  160         17789  
14 160     160   1236 use Sub::Util qw/ set_subname subname /;
  160         419  
  160         11482  
15              
16 160     160   84981 use Plack::App::File;
  160         3392890  
  160         7241  
17 160     160   79288 use Plack::Middleware::FixMissingBodyInRedirect;
  160         1433779  
  160         7517  
18 160     160   77243 use Plack::Middleware::Head;
  160         59500  
  160         6120  
19 160     160   86095 use Plack::Middleware::Conditional;
  160         50709  
  160         5920  
20 160     160   76459 use Plack::Middleware::ConditionalGET;
  160         122466  
  160         6723  
21              
22 160     160   90978 use Dancer2::ConfigReader;
  160         908  
  160         8282  
23 160     160   1501 use Dancer2::Core;
  160         350  
  160         5930  
24 160     160   95567 use Dancer2::Core::Cookie;
  160         852  
  160         8587  
25 160     160   102691 use Dancer2::Core::HTTP;
  160         700  
  160         11029  
26 160     160   94097 use Dancer2::Core::Error;
  160         1788  
  160         8783  
27 160     160   1910 use Dancer2::Core::Types;
  160         366  
  160         1886  
28 160     160   2294732 use Dancer2::Core::Route;
  160         911  
  160         8548  
29 160     160   94786 use Dancer2::Core::Hook;
  160         781  
  160         7451  
30 160     160   103824 use Dancer2::Core::Request;
  160         924  
  160         9062  
31 160     160   1027 use Dancer2::Core::Factory;
  160         353  
  160         1699488  
32              
33             our $EVAL_SHIM; $EVAL_SHIM ||= sub {
34             my $code = shift;
35             $code->(@_);
36             };
37              
38              
39             # we have hooks here
40             with qw<
41             Dancer2::Core::Role::Hookable
42             Dancer2::Core::Role::HasConfig
43             Dancer2::Core::Role::HasLocation
44             Dancer2::Core::Role::HasEnvironment
45             >;
46              
47 288     288 0 2614 sub supported_engines { [ qw ] }
48              
49             sub with_plugins {
50 39     39 1 316 my ( $self, @plugins ) = @_;
51 39         209 return map $self->_with_plugin($_), @plugins;
52              
53             }
54              
55             sub _with_plugin {
56 39     39   276 my( $self, $plugin ) = @_;
57              
58 39 100       201 if ( is_ref($plugin) ) {
59             # passing the plugin as an already-created object
60              
61             # already loaded?
62 1 50       4 if( my ( $already ) = grep { ref($plugin) eq ref $_; } @{ $self->plugins } ) {
  2         19  
  1         28  
63 0 0       0 die "trying to load two different objects for plugin ". ref $plugin
64             if refaddr($plugin) != refaddr $already ;
65              
66             }
67             else {
68 1         3 push @{ $self->plugins }, $plugin;
  1         25  
69             }
70              
71 1         12 return $plugin;
72             }
73              
74             # short plugin names get Dancer2::Plugin:: prefix
75             # plugin names starting with a '+' are full package names
76 38 100       232 if ( $plugin !~ s/^\+// ) {
77 7         45 $plugin =~ s/^(?!Dancer2::Plugin::)/Dancer2::Plugin::/;
78             }
79              
80             # check if it's already there
81 38 100       312 if( my ( $already ) = grep { $plugin eq ref $_ } @{ $self->plugins } ) {
  12         136  
  38         1275  
82 2         13 return $already;
83             }
84              
85 36         382 push @{ $self->plugins },
  36         1215  
86             $plugin = use_module($plugin)->new( app => $self );
87              
88 36         933 return $plugin;
89             }
90              
91             sub with_plugin {
92 39     39 1 10011 my( $self, $plugin ) = @_;
93              
94 39 50       712 croak "expected a single argument"
95             unless @_ == 2;
96              
97 39         212 ( $self->with_plugins($plugin) )[0];
98             }
99              
100             has _factory => (
101             is => 'ro',
102             isa => InstanceOf['Dancer2::Core::Factory'],
103             lazy => 1,
104             default => sub { Dancer2::Core::Factory->new },
105             );
106              
107             has logger_engine => (
108             is => 'ro',
109             isa => ConsumerOf['Dancer2::Core::Role::Logger'],
110             lazy => 1,
111             builder => '_build_logger_engine',
112             writer => 'set_logger_engine',
113             );
114              
115             has session_engine => (
116             is => 'ro',
117             isa => ConsumerOf['Dancer2::Core::Role::SessionFactory'],
118             lazy => 1,
119             builder => '_build_session_engine',
120             writer => 'set_session_engine',
121             );
122              
123             has template_engine => (
124             is => 'ro',
125             isa => ConsumerOf['Dancer2::Core::Role::Template'],
126             lazy => 1,
127             builder => '_build_template_engine',
128             writer => 'set_template_engine',
129             );
130              
131             has serializer_engine => (
132             is => 'ro',
133             isa => ConsumerOf['Dancer2::Core::Role::Serializer'],
134             lazy => 1,
135             builder => '_build_serializer_engine',
136             writer => 'set_serializer_engine',
137             predicate => 'has_serializer_engine',
138             );
139              
140             has _appdir_path => (
141             is => 'ro',
142             lazy => 1,
143             builder => '_build_appdir_path',
144             init_arg => undef,
145             );
146              
147             has _views_path => (
148             is => 'ro',
149             lazy => 1,
150             builder => '_build_views_path',
151             init_arg => undef,
152             );
153              
154             has _public_dir_path => (
155             is => 'ro',
156             lazy => 1,
157             builder => '_build_public_dir_path',
158             init_arg => undef,
159             );
160              
161             has '+local_triggers' => (
162             default => sub {
163             my $self = shift;
164             my $triggers = {
165             # general triggers we want to allow, besides engines
166             views => sub {
167             my $self = shift;
168             my $value = shift;
169             $self->template_engine->views($value);
170             },
171              
172             layout => sub {
173             my $self = shift;
174             my $value = shift;
175             $self->template_engine->layout($value);
176             },
177              
178             layout_dir => sub {
179             my $self = shift;
180             my $value = shift;
181             $self->template_engine->layout_dir($value);
182             },
183              
184             log => sub {
185             my ( $self, $value, $config ) = @_;
186              
187             # This will allow to set the log level
188             # using: set log => warning
189             $self->logger_engine->log_level($value);
190             },
191              
192             default_mime_type => sub {
193             my $self = shift;
194             my $value = shift;
195             $self->mime_type->default($value);
196             },
197             };
198              
199             foreach my $engine ( @{ $self->supported_engines } ) {
200             $triggers->{$engine} = sub {
201             my $self = shift;
202             my $value = shift;
203             my $config = shift;
204              
205             is_ref($value) and return $value;
206              
207             my $build_method = "_build_${engine}_engine";
208             my $setter_method = "set_${engine}_engine";
209             my $engine_instance = $self->$build_method( $value, $config );
210              
211             # set the engine with the new value from the builder
212             $self->$setter_method($engine_instance);
213             if ( $engine eq 'serializer' && $self->has_response ) {
214             $self->response->serializer($engine_instance);
215             }
216              
217             return $engine_instance;
218             };
219             }
220              
221             return $triggers;
222             },
223             );
224              
225             has 'mime_type' => (
226             'is' => 'ro',
227             'isa' => InstanceOf['Dancer2::Core::MIME'],
228             'default' => sub { Dancer2::Core::MIME->new() },
229             );
230              
231             sub _build_logger_engine {
232 183     183   6856 my $self = shift;
233 183         785 my $value = shift;
234 183         978 my $config = shift;
235              
236 183 100       5012 defined $config or $config = $self->config;
237 183 100       2281 defined $value or $value = $config->{logger};
238              
239 183 100       875 is_ref($value) and return $value;
240              
241             # XXX This is needed for the tests that create an app without
242             # a runner.
243 182 50       669 defined $value or $value = 'console';
244              
245 182 100       1157 is_module_name($value)
246             or croak "Cannot load logger engine '$value': illegal module name";
247              
248 181         5180 my $engine_options =
249             $self->_get_config_for_engine( logger => $value, $config );
250              
251             my $logger = $self->_factory->create(
252             logger => $value,
253 181         8154 %{$engine_options},
  181         7196  
254             location => $self->config_reader->config_location,
255             environment => $self->environment,
256             app_name => $self->name,
257             postponed_hooks => $self->postponed_hooks
258             );
259              
260 181 100       7419 exists $config->{log} and $logger->log_level($config->{log});
261              
262 181         6725 return $logger;
263             }
264              
265             sub _build_session_engine {
266 191     191   33892 my $self = shift;
267 191         1328 my $value = shift;
268 191         965 my $config = shift;
269              
270 191 100       4897 defined $config or $config = $self->config;
271 191 100 100     3644 defined $value or $value = $config->{'session'} || 'simple';
272              
273 191 100       1329 is_ref($value) and return $value;
274              
275 188 100       1112 is_module_name($value)
276             or croak "Cannot load session engine '$value': illegal module name";
277              
278 187         4202 my $engine_options =
279             $self->_get_config_for_engine( session => $value, $config );
280              
281 187         644 Scalar::Util::weaken( my $weak_self = $self );
282              
283             # Note that engine options will replace the default session_dir (if provided).
284             return $self->_factory->create(
285             session => $value,
286             session_dir => $self->_appdir_path->child('sessions')->stringify,
287 187         32997 %{$engine_options},
288             postponed_hooks => $self->postponed_hooks,
289              
290 3     3   42 log_cb => sub { $weak_self->log(@_) },
291 187         5596 );
292             }
293              
294             sub _build_template_engine {
295 180     180   5965 my $self = shift;
296 180         483 my $value = shift;
297 180         468 my $config = shift;
298              
299 180 100       4438 defined $config or $config = $self->config;
300 180 100       3232 defined $value or $value = $config->{'template'};
301              
302 180 50       747 defined $value or return;
303 180 50       768 is_ref($value) and return $value;
304              
305 180 100       1045 is_module_name($value)
306             or croak "Cannot load template engine '$value': illegal module name";
307              
308 179         4431 my $engine_options =
309             $self->_get_config_for_engine( template => $value, $config );
310              
311             my $engine_attrs = {
312             config => $engine_options,
313             layout => $config->{layout},
314             layout_dir => ( $config->{layout_dir} || 'layouts' ),
315             views => $config->{views},
316             charset => $config->{charset},
317 179   50     2818 };
318              
319 179         714 Scalar::Util::weaken( my $weak_self = $self );
320              
321             return $self->_factory->create(
322             template => $value,
323 179         4360 %{$engine_attrs},
324             postponed_hooks => $self->postponed_hooks,
325              
326 7     7   73 log_cb => sub { $weak_self->log(@_) },
327 179         5346 );
328             }
329              
330             sub _build_serializer_engine {
331 22     22   55 my $self = shift;
332 22         53 my $value = shift;
333 22         47 my $config = shift;
334              
335 22 50       81 defined $config or $config = $self->config;
336 22 50       106 defined $value or $value = $config->{serializer};
337              
338 22 50       193 defined $value or return;
339 22 50       103 is_ref($value) and return $value;
340              
341 22         117 my $engine_options =
342             $self->_get_config_for_engine( serializer => $value, $config );
343 22   33     249 $engine_options->{strict_utf8} //= $config->{strict_utf8};
344              
345 22         65 Scalar::Util::weaken( my $weak_self = $self );
346              
347             return $self->_factory->create(
348             serializer => $value,
349             config => $engine_options,
350             postponed_hooks => $self->postponed_hooks,
351              
352 6     6   63 log_cb => sub { $weak_self->log(@_) },
353 22         734 );
354             }
355              
356             sub _get_config_for_engine {
357 575     575   2859 my $self = shift;
358 575         1407 my $engine = shift;
359 575         1329 my $name = shift;
360 575         1259 my $config = shift;
361              
362 575 100 100     4224 defined $config->{'engines'} && defined $config->{'engines'}{$engine}
363             or return {};
364              
365             # try name, camelized name and fully-qualified name (without plus)
366 25         60 my $full_name = $name;
367 25         321 my $was_fully_qualified = ( $full_name =~ s/^\+// ); # strip any leading '+'
368 25         67 my $engine_config = {};
369 25 100       132 foreach my $engine_name (
370             $name,
371             Dancer2::Core::camelize($name),
372             ( $was_fully_qualified ? $full_name : () )
373             ) {
374 34 100       171 if ( defined $config->{'engines'}{$engine}{$engine_name} ) {
375 21         74 $engine_config = $config->{'engines'}{$engine}{$engine_name};
376 21         66 last;
377             }
378             }
379              
380 25         95 return $engine_config;
381             }
382              
383             has postponed_hooks => (
384             is => 'ro',
385             isa => HashRef,
386             default => sub { {} },
387             );
388              
389             # TODO I'd be happier with a HashRef, really
390             has plugins => (
391             is => 'rw',
392             isa => ArrayRef,
393             default => sub { [] },
394             );
395              
396             has route_handlers => (
397             is => 'rw',
398             isa => ArrayRef,
399             default => sub { [] },
400             );
401              
402             has name => (
403             is => 'ro',
404             isa => Str,
405             default => sub { (caller(1))[0] },
406             );
407              
408             has request => (
409             is => 'ro',
410             isa => InstanceOf['Dancer2::Core::Request'],
411             writer => '_set_request',
412             clearer => 'clear_request',
413             predicate => 'has_request',
414             );
415              
416             sub set_request {
417 724     724 0 2189 my ($self, $request, $defined_engines) = @_;
418             # typically this is passed in as an optimization within the
419             # dispatch loop but may be called elsewhere
420 724   66     2538 $defined_engines ||= $self->defined_engines;
421             # populate request in app and all engines
422 724         23011 $self->_set_request($request);
423 724         27413 Scalar::Util::weaken( my $weak_request = $request );
424 724         1420 $_->set_request( $weak_request ) for @{$defined_engines};
  724         17880  
425             }
426              
427             has response => (
428             is => 'ro',
429             isa => InstanceOf['Dancer2::Core::Response'],
430             lazy => 1,
431             writer => 'set_response',
432             clearer => 'clear_response',
433             builder => '_build_response',
434             predicate => 'has_response',
435             );
436              
437             has with_return => (
438             is => 'ro',
439             predicate => 1,
440             writer => 'set_with_return',
441             clearer => 'clear_with_return',
442             );
443              
444             has session => (
445             is => 'ro',
446             isa => InstanceOf['Dancer2::Core::Session'],
447             lazy => 1,
448             builder => '_build_session',
449             writer => 'set_session',
450             clearer => 'clear_session',
451             predicate => '_has_session',
452             );
453              
454             has config_reader => (
455             is => 'ro',
456             isa => InstanceOf['Dancer2::ConfigReader'],
457             lazy => 0,
458             builder => '_build_config_reader',
459             );
460              
461             sub _build_config_reader {
462 259     259   4627587 my ($self) = @_;
463             my $cfgr = Dancer2::ConfigReader->new(
464             environment => $self->environment,
465 259   66     7602 location => $ENV{DANCER_CONFDIR} || $self->location,
466             default_config => $self->_build_default_config(),
467             );
468 259         828503 return $cfgr;
469             }
470              
471             has '+config' => (
472             is => 'ro',
473             isa => HashRef,
474             lazy => 1,
475             builder => '_build_config',
476             );
477              
478             sub _build_config {
479 259     259   3632 my ($self) = @_;
480              
481 259         1470 my $config_reader = $self->config_reader;
482 259         6450 my $config = $config_reader->config;
483              
484 257 100 66     11404 if ( $config && $config->{'engines'} ) {
485 2         29 $self->_validate_engine($_) for keys %{ $config->{'engines'} };
  2         15  
486             }
487              
488 256         6894 return $config;
489             }
490              
491             sub _build_response {
492 689     689   9129 my $self = shift;
493             return Dancer2::Core::Response->new(
494             mime_type => $self->mime_type,
495             server_tokens => !$self->config->{'no_server_tokens'},
496             charset => $self->config->{charset},
497             strict_utf8 => $self->config->{strict_utf8},
498 689 100       15699 do {
499 689         12861 Scalar::Util::weaken( my $weak_self = $self );
500 689 0   0   21202 log_cb => sub { $weak_self && $weak_self->log(@_) };
  0         0  
501             },
502             $self->has_serializer_engine
503             ? ( serializer => $self->serializer_engine )
504             : (),
505             );
506             }
507              
508             sub _build_session {
509 93     93   1008 my $self = shift;
510 93         163 my $session;
511              
512             # Find the session engine
513 93         1620 my $engine = $self->session_engine;
514              
515             # find the session cookie if any
516 93 100       890 if ( !$self->has_destroyed_session ) {
517 88         175 my $session_id;
518 88         497 my $session_cookie = $self->cookie( $engine->cookie_name );
519 88 100       1371 defined $session_cookie and
520             $session_id = $session_cookie->value;
521              
522             # if we have a session cookie, try to retrieve the session
523 88 100       245 if ( defined $session_id ) {
524             eval {
525             $EVAL_SHIM->(sub {
526 53     53   342 $session = $engine->retrieve( id => $session_id );
527 53         434 });
528 50         324 1;
529             }
530 53 100       106 or do {
531 3   50     12 my $err = $@ || "Zombie Error";
532 3 50       19 if ( $err !~ /Unable to retrieve session/ ) {
533 0         0 croak "Failed to retrieve session: $err"
534             } else {
535             # XXX we throw away the error entirely? Why?
536             }
537             };
538             }
539             }
540              
541             # create the session if none retrieved
542 93   66     1329 return $session ||= $engine->create();
543             }
544              
545             sub has_session {
546 947     947 1 1699 my $self = shift;
547              
548 947         16175 my $engine = $self->session_engine;
549              
550 947   66     15872 return $self->_has_session
551             || ( $self->cookie( $engine->cookie_name )
552             && !$self->has_destroyed_session );
553             }
554              
555             has destroyed_session => (
556             is => 'ro',
557             isa => InstanceOf ['Dancer2::Core::Session'],
558             predicate => 1,
559             writer => 'set_destroyed_session',
560             clearer => 'clear_destroyed_session',
561             );
562              
563             has 'prep_apps' => (
564             'is' => 'ro',
565             'isa' => ArrayRef,
566             'default' => sub { [] },
567             );
568              
569             sub find_plugin {
570 2     2 0 6 my ( $self, $name ) = @_;
571 2     2   11 my $plugin = List::Util::first { ref($_) eq $name } @{ $self->plugins };
  2         24  
  2         67  
572 2 100       31 $plugin or return;
573 1         4 return $plugin;
574             }
575              
576             sub destroy_session {
577 17     17 1 42 my $self = shift;
578              
579             # Find the session engine
580 17         315 my $engine = $self->session_engine;
581              
582             # Expire session, set the expired cookie and destroy the session
583             # Setting the cookie ensures client gets an expired cookie unless
584             # a new session is created and supercedes it
585 17         391 my $session = $self->session;
586 17         747 $session->expires(-86400); # yesterday
587 17         1892 $engine->destroy( id => $session->id );
588              
589             # Invalidate session cookie in request
590             # and clear session in app and engines
591 17         359 $self->set_destroyed_session($session);
592 17         889 $self->clear_session;
593 17         93 $_->clear_session for @{ $self->defined_engines };
  17         64  
594              
595 17         1915 return;
596             }
597              
598             sub setup_session {
599 118     118 0 207 my $self = shift;
600              
601 118         202 for my $engine ( @{ $self->defined_engines } ) {
  118         379  
602 354 50       15890 $self->has_session ?
603             $engine->set_session( $self->session ) :
604             $engine->clear_session;
605             }
606             }
607              
608             sub change_session_id {
609 5     5 1 13 my $self = shift;
610              
611 5         108 my $session = $self->session;
612              
613             # Find the session engine
614 5         273 my $engine = $self->session_engine;
615              
616 5 100       91 if ($engine->can('_change_id')) {
617              
618             # session engine can change session ID
619 3         21 $engine->change_id( session => $session );
620             }
621             else {
622              
623             # Method order is important in here...
624             #
625             # On session build if there is no destroyed session then the session
626             # builder tries to recreate the session using the existing session
627             # cookie. We really don't want to do that in this case so it is
628             # important to create the new session before the
629             # clear_destroyed_session method is called.
630             #
631             # This sucks.
632             #
633             # Sawyer suggested:
634             #
635             # What if you take the session cookie logic out of that attribute into
636             # another attribute and clear that attribute?
637             # That would force the session rebuilt to rebuilt the attribute and
638             # get a different cookie value, no?
639             #
640             # TODO: think about this some more.
641              
642             # grab data, destroy session and store data again
643 2         6 my %data = %{$session->data};
  2         45  
644              
645             # destroy existing session
646 2         32 $self->destroy_session;
647              
648             # get new session
649 2         58 $session = $self->session;
650              
651             # write data from old session into new
652             # Some engines add session id to data so skip id.
653 2         64 while (my ($key, $value) = each %data ) {
654 2 50       16 $session->write($key => $value) unless $key eq 'id';
655             }
656              
657             # clear out destroyed session - no longer relevant
658 2         63 $self->clear_destroyed_session;
659             }
660              
661 5         155 return $session->id;
662             }
663              
664             has prefix => (
665             is => 'rw',
666             isa => Maybe [Dancer2Prefix],
667             predicate => 1,
668             coerce => sub {
669             my $prefix = shift;
670             defined($prefix) and $prefix eq "/" and return;
671             return $prefix;
672             },
673             );
674              
675             # routes registry, stored by method:
676             has routes => (
677             is => 'rw',
678             isa => HashRef,
679             default => sub {
680             { get => [],
681             head => [],
682             post => [],
683             put => [],
684             del => [],
685             options => [],
686             };
687             },
688             );
689              
690             has 'route_names' => (
691             'is' => 'rw',
692             'isa' => HashRef,
693             'default' => sub { {} },
694             );
695              
696             # add_hook will add the hook to the first "hook candidate" it finds that support
697             # it. If none, then it will try to add the hook to the current application.
698             around add_hook => sub {
699             my $orig = shift;
700             my $self = shift;
701              
702             # saving caller information
703             my ( $package, $file, $line ) = caller(4); # deep to 4 : user's app code
704             my $add_hook_caller = [ $package, $file, $line ];
705              
706             my ($hook) = @_;
707             my $name = $hook->name;
708             my $hook_aliases = $self->all_hook_aliases;
709              
710             # look for an alias
711             defined $hook_aliases->{$name} and $name = $hook_aliases->{$name};
712             $hook->name($name);
713              
714             # if that hook belongs to the app, register it now and return
715             $self->has_hook($name) and return $self->$orig(@_);
716              
717             # at this point the hook name must be formatted like:
718             # '$type.$candidate.$name', eg: 'engine.template.before_render' or
719             # 'plugin.database.before_dbi_connect'
720             my ( $hookable_type, $hookable_name, $hook_name ) = split( /\./, $name );
721              
722             ( defined $hookable_name && defined $hook_name )
723             or croak "Invalid hook name `$name'";
724              
725             grep /^$hookable_type$/, qw(core engine handler plugin)
726             or croak "Unknown hook type `$hookable_type'";
727              
728             # register the hooks for existing hookable candidates
729             foreach my $hookable ( $self->hook_candidates ) {
730             $hookable->has_hook($name) and $hookable->add_hook(@_);
731             }
732              
733             # we register the hook for upcoming objects;
734             # that way, each components that can claim the hook will have a chance
735             # to register it.
736              
737             my $postponed_hooks = $self->postponed_hooks;
738              
739             # Hmm, so the hook was not claimed, at this point we'll cache it and
740             # register it when the owner is instantiated
741             $postponed_hooks->{$hookable_type}{$hookable_name} ||= {};
742             $postponed_hooks->{$hookable_type}{$hookable_name}{$name} ||= {};
743             $postponed_hooks->{$hookable_type}{$hookable_name}{$name}{hook} = $hook;
744             $postponed_hooks->{$hookable_type}{$hookable_name}{$name}{caller} =
745             $add_hook_caller;
746              
747             };
748              
749             around execute_hook => sub {
750             my $orig = shift;
751             my $self = shift;
752              
753             local $Dancer2::Core::Route::REQUEST = $self->request;
754             local $Dancer2::Core::Route::RESPONSE = $self->response;
755              
756             my ( $hook, @args ) = @_;
757             if ( !$self->has_hook($hook) ) {
758             foreach my $cand ( $self->hook_candidates ) {
759             $cand->has_hook($hook) and return $cand->execute_hook(@_);
760             }
761             }
762              
763             return $self->$orig(@_);
764             };
765              
766             sub _build_default_config {
767 259     259   65842 my $self = shift;
768              
769 259   33     8488 my $public = $ENV{DANCER_PUBLIC} || $self->_location_path->child('public')->stringify;
770             return {
771             content_type => ( $ENV{DANCER_CONTENT_TYPE} || 'text/html' ),
772             charset => ( $ENV{DANCER_CHARSET} || 'UTF-8' ),
773             strict_utf8 => ( $ENV{DANCER_STRICT_UTF8} || 0 ),
774             logger => ( $ENV{DANCER_LOGGER} || 'console' ),
775             views => ( $ENV{DANCER_VIEWS}
776 259   50     40215 || $self->_location_path->child('views')->stringify ),
      50        
      50        
      50        
      33        
777             environment => $self->environment,
778             appdir => $self->location,
779             public_dir => $public,
780             template => 'Tiny',
781             strict_config => 0, # We can look at changing this in a future version
782             route_handlers => [
783             [
784             AutoPage => 1
785             ],
786             ],
787             };
788             }
789              
790             sub _build_appdir_path {
791 177     177   7286 my $self = shift;
792 177   33     3456 return Path::Tiny::path( $self->config->{appdir} || $self->location );
793             }
794              
795             sub _build_views_path {
796 0     0   0 my $self = shift;
797 0         0 return Path::Tiny::path( $self->config->{views} );
798             }
799              
800             sub _build_public_dir_path {
801 86     86   912 my $self = shift;
802             my $dir = $ENV{DANCER_PUBLIC}
803             || $self->config->{public_dir}
804 86   33     1852 || $self->_location_path->child('public')->stringify;
805 86         1237 return Path::Tiny::path($dir);
806             }
807              
808             sub _init_hooks {
809 256     256   661 my $self = shift;
810              
811             # Hook to flush the session at the end of the request,
812             # this way, we're sure we flush only once per request
813             #
814             # Note: we create a weakened copy $self
815             # before closing over the weakened copy
816             # to avoid circular memory refs.
817 256         813 Scalar::Util::weaken(my $app = $self);
818              
819             $self->add_hook(
820             Dancer2::Core::Hook->new(
821             name => 'core.app.after_request',
822             code => sub {
823 501     501   1255 my $response = $Dancer2::Core::Route::RESPONSE;
824              
825             # make sure an engine is defined, if not, nothing to do
826 501         11087 my $engine = $app->session_engine;
827 501 50       4745 defined $engine or return;
828              
829             # if a session has been instantiated or we already had a
830             # session, first flush the session so cookie-based sessions can
831             # update the session ID if needed, then set the session cookie
832             # in the response
833             #
834             # if there is NO session object but the request has a cookie with
835             # a session key, create a dummy session with the same ID (without
836             # actually retrieving and flushing immediately) and generate the
837             # cookie header from the dummy session. Lazy Sessions FTW!
838              
839 501 100       1946 if ( $app->has_session ) {
    100          
840 76         194 my $session;
841 76 100       273 if ( $app->_has_session ) { # Session object exists
842 73         1306 $session = $app->session;
843 73 100       1644 $session->is_dirty and $engine->flush( session => $session );
844             }
845             else { # Cookie header exists. Create a dummy session object
846 3         15 my $cookie = $app->cookie( $engine->cookie_name );
847 3         58 my $session_id = $cookie->value;
848 3         95 $session = Dancer2::Core::Session->new( id => $session_id );
849             }
850 76         831 $engine->set_cookie_header(
851             response => $response,
852             session => $session
853             );
854             }
855             elsif ( $app->has_destroyed_session ) {
856 12         47 my $session = $app->destroyed_session;
857 12         74 $engine->set_cookie_header(
858             response => $response,
859             session => $session,
860             destroyed => 1
861             );
862             }
863             },
864             )
865 256         6482 );
866             }
867              
868             sub supported_hooks {
869 481     481 0 6271 qw/
870             core.app.before_request
871             core.app.after_request
872             core.app.route_exception
873             core.app.hook_exception
874             core.app.before_file_render
875             core.app.after_file_render
876             core.error.before
877             core.error.after
878             core.error.init
879             /;
880             }
881              
882             sub hook_aliases {
883 1902     1902 0 3915 my $self = shift;
884 1902   100     16350 $self->{'hook_aliases'} ||= {
885             before => 'core.app.before_request',
886             before_request => 'core.app.before_request',
887             after => 'core.app.after_request',
888             after_request => 'core.app.after_request',
889             init_error => 'core.error.init',
890             before_error => 'core.error.before',
891             after_error => 'core.error.after',
892             on_route_exception => 'core.app.route_exception',
893             on_hook_exception => 'core.app.hook_exception',
894              
895             before_file_render => 'core.app.before_file_render',
896             after_file_render => 'core.app.after_file_render',
897             before_handler_file_render => 'handler.file.before_render',
898             after_handler_file_render => 'handler.file.after_render',
899              
900              
901             # compatibility from Dancer1
902             before_error_render => 'core.error.before',
903             after_error_render => 'core.error.after',
904             before_error_init => 'core.error.init',
905              
906             # TODO: call $engine->hook_aliases as needed
907             # But.. currently there are use cases where hook_aliases
908             # are needed before the engines are intiialized :(
909             before_template_render => 'engine.template.before_render',
910             after_template_render => 'engine.template.after_render',
911             before_layout_render => 'engine.template.before_layout_render',
912             after_layout_render => 'engine.template.after_layout_render',
913             before_serializer => 'engine.serializer.before',
914             after_serializer => 'engine.serializer.after',
915             };
916             }
917              
918             sub defined_engines {
919 1645     1645 0 3198 my $self = shift;
920             return [
921 1645 100       34360 $self->template_engine,
922             $self->session_engine,
923             $self->logger_engine,
924             $self->has_serializer_engine
925             ? $self->serializer_engine
926             : (),
927             ];
928             }
929              
930             sub register_plugin {
931 0     0 1 0 my $self = shift;
932 0         0 my $plugin = shift;
933              
934 0         0 $self->log( core => "Registered $plugin");
935              
936 0         0 push @{ $self->plugins }, $plugin;
  0         0  
937             }
938              
939             # This method overrides the default one from Role::ConfigReader
940             sub settings {
941 971     971 0 2463 my $self = shift;
942 971         2068 +{ %{ Dancer2::runner()->config }, %{ $self->config } };
  971         3386  
  971         33220  
943             }
944              
945             sub cleanup {
946 712     712 0 1583 my $self = shift;
947 712         18612 $self->clear_request;
948 712         17832 $self->clear_response;
949 712         16504 $self->clear_session;
950 712         16710 $self->clear_destroyed_session;
951             # Clear engine attributes
952 712         3773 for my $engine ( @{ $self->defined_engines } ) {
  712         2256  
953 2217         87575 $engine->clear_session;
954 2217         46180 $engine->clear_request;
955             }
956             }
957              
958             sub _validate_engine {
959 29     29   83 my $self = shift;
960 29         65 my $name = shift;
961              
962 29 100       61 grep +( $_ eq $name ), @{ $self->supported_engines }
  29         150  
963             or croak "Engine '$name' is not supported.";
964             }
965              
966             sub engine {
967 27     27 0 1364 my $self = shift;
968 27         66 my $name = shift;
969              
970 27         164 $self->_validate_engine($name);
971              
972 26         120 my $attr_name = "${name}_engine";
973 26         754 return $self->$attr_name;
974             }
975              
976             sub template {
977 37     37 0 837 my $self = shift;
978              
979 37         769 my $template = $self->template_engine;
980 37         975 $template->set_settings( $self->config );
981              
982             # A session will not exist if there is no request (global keyword)
983             #
984             # A session may exist but the route code may not have instantiated
985             # the session object (sessions are lazy). If this is the case, do
986             # that now, so the templates have the session data for rendering.
987 37 100 100     2220 $self->has_request && $self->has_session && ! $template->has_session
      100        
988             and $self->setup_session;
989              
990             # return content
991 37 100       294 if ($self->has_with_return) {
992 33         140 my $old_with_return = $self->with_return;
993 33         59 my $local_response;
994             $self->set_with_return( sub {
995 3   33 3   39 $local_response ||= shift;
996 33         262 });
997             # Catch any exceptions that may happen during template processing
998 33         71 my $content = eval { $template->process( @_ ) };
  33         221  
999 33         37238 my $eval_result = $@;
1000 33         287 $self->set_with_return($old_with_return);
1001             # If there was a previous response set before the exception (or set as
1002             # part of the exception handling), then use that, otherwise throw the
1003             # exception as normal
1004 33 100       151 if ($local_response) {
    100          
1005 3         15 $self->with_return->($local_response);
1006             } elsif ($eval_result) {
1007 6         73 die $eval_result;
1008             }
1009 24         156 return $content;
1010             }
1011 4         28 return $template->process( @_ );
1012             }
1013              
1014             sub hook_candidates {
1015 45     45 0 94 my $self = shift;
1016              
1017 45         88 my @engines = @{ $self->defined_engines };
  45         473  
1018              
1019 45         3494 my @route_handlers;
1020 45         95 for my $handler ( @{ $self->route_handlers } ) {
  45         1027  
1021 45         428 my $handler_code = $handler->{handler};
1022 45 50 33     529 blessed $handler_code and $handler_code->can('supported_hooks')
1023             and push @route_handlers, $handler_code;
1024             }
1025              
1026             # TODO : get the list of all plugins registered
1027 45         82 my @plugins = @{ $self->plugins };
  45         1040  
1028              
1029 45         420 ( @route_handlers, @engines, @plugins );
1030             }
1031              
1032             sub all_hook_aliases {
1033 345     345 0 793 my $self = shift;
1034              
1035 345         1501 my $aliases = $self->hook_aliases;
1036 345         886 for my $plugin ( grep { $_->can('hook_aliases') } @{ $self->plugins } ) {
  13         194  
  345         8797  
1037 13         26 $aliases = { %{$aliases}, %{ $plugin->hook_aliases } };
  13         94  
  13         44  
1038             }
1039              
1040 345         3720 return $aliases;
1041             }
1042              
1043             sub log {
1044 2835     2835 0 40678 my $self = shift;
1045 2835         4824 my $level = shift;
1046              
1047 2835 50       58973 my $logger = $self->logger_engine
1048             or croak "No logger defined";
1049              
1050 2835         34717 $logger->$level(@_);
1051             }
1052              
1053             sub send_as {
1054 11     11 0 32 my $self = shift;
1055 11         40 my ( $type, $data, $options ) = @_;
1056 11   100     66 $options ||= {};
1057              
1058 11 100       315 $type or croak "Can not send_as using an undefined type";
1059              
1060 10 100 100     70 if ( lc($type) eq 'html' || lc($type) eq 'plain' ) {
1061 5 50       19 if ( $type ne lc $type ) {
1062 0         0 local $Carp::CarpLevel = 2;
1063 0         0 carp sprintf( "Please use %s as the type for 'send_as', not %s", lc($type), $type );
1064             }
1065              
1066 5   33     160 $options->{charset} //= $self->config->{charset};
1067 5         59 my $content = $data;
1068 5 50 33     39 if ( defined $options->{charset} && length $options->{charset} ) {
1069 5         64 $content = Encode::encode( $options->{charset}, $data );
1070             }
1071 5   33     552 $options->{content_type} ||= join '/', 'text', lc $type;
1072             # Explicit return needed here, as if we are currently rendering a
1073             # template then with_return will not longjump
1074 5         32 return $self->send_file( \$content, %$options );
1075             }
1076              
1077             # Try and load the serializer class
1078 5         14 my $serializer_class = "Dancer2::Serializer::$type";
1079             eval {
1080             $EVAL_SHIM->(sub {
1081 5     5   33 require_module( $serializer_class );
1082 5         70 });
1083 3         113 1;
1084 5 100       10 } or do {
1085 2   50     742 my $err = $@ || "Zombie Error";
1086 2         571 croak "Unable to load serializer class for $type: $err";
1087             };
1088              
1089             # load any serializer engine config
1090 3   50     107 my $engine_options =
1091             $self->_get_config_for_engine( serializer => $type, $self->config ) || {};
1092 3   66     82 $engine_options->{strict_utf8} //= $self->config->{strict_utf8};
1093              
1094 3         25 Scalar::Util::weaken( my $weak_self = $self );
1095             my $serializer = $self->_factory->create(
1096             serializer => $type,
1097             config => $engine_options,
1098             postponed_hooks => $self->postponed_hooks,
1099 0     0   0 log_cb => sub { $weak_self->log(@_) },
1100 3         140 );
1101 3         128 my $content = $serializer->serialize( $data );
1102 3   66     27 $options->{content_type} ||= $serializer->content_type;
1103 3         20 $self->send_file( \$content, %$options );
1104             }
1105              
1106             sub send_error {
1107 8     8 0 18 my $self = shift;
1108 8         29 my ( $message, $status ) = @_;
1109              
1110             my $err = Dancer2::Core::Error->new(
1111             message => $message,
1112             app => $self,
1113             charset => $self->config->{charset},
1114 8 100       188 ( status => $status )x!! $status,
1115              
1116             $self->has_serializer_engine
1117             ? ( serializer => $self->serializer_engine )
1118             : (),
1119             )->throw;
1120              
1121             # Immediately return to dispatch if with_return coderef exists
1122 8 50       201 $self->has_with_return && $self->with_return->($err);
1123 0         0 return $err;
1124             }
1125              
1126             sub send_file {
1127 20     20 0 42 my $self = shift;
1128 20         49 my $thing = shift;
1129 20         79 my %options = @_;
1130              
1131 20         52 my ($content_type, $charset, $file_path);
1132              
1133             # are we're given a filehandle? (based on what Plack::Middleware::Lint accepts)
1134             my $is_filehandle = Plack::Util::is_real_fh($thing)
1135 20   33     124 || ( is_globref($thing) && *{$thing}{IO} && *{$thing}{IO}->can('getline') )
1136             || ( Scalar::Util::blessed($thing) && $thing->can('getline') );
1137 20         1648 my ($fh) = ($thing)x!! $is_filehandle;
1138              
1139             # if we're given an IO::Scalar object, DTRT (take the scalar ref from it)
1140 20 50 33     77 if (Scalar::Util::blessed($thing) && $thing->isa('IO::Scalar')) {
1141 0         0 $thing = $thing->sref;
1142             }
1143              
1144             # if we're given a SCALAR reference, build a filehandle to it
1145 20 100       122 if ( is_scalarref($thing) ) {
1146             ## no critic qw(InputOutput::RequireCheckedOpen)
1147 9         155 open $fh, "<", $thing;
1148             }
1149              
1150             # If we haven't got a filehandle, create one to the requested content
1151 20 100       87 if (! $fh) {
1152 10         24 my $path = $thing;
1153             # remove prefix from given path (if not a filehandle)
1154 10         333 my $prefix = $self->prefix;
1155 10 50 33     120 if ( $prefix && $prefix ne '/' ) {
1156 0         0 $path =~ s/^\Q$prefix\E//;
1157             }
1158             # static file dir - either system root or public_dir
1159             my $dir = $options{system_path}
1160 10 100       209 ? Path::Tiny->rootdir
1161             : $self->_public_dir_path;
1162              
1163             my $err_response = sub {
1164 1     1   360 my $status = shift;
1165 1         37 $self->response->status($status);
1166 1         90 $self->response->header( 'Content-Type', 'text/plain' );
1167 1         193 $self->response->content( Dancer2::Core::HTTP->status_message($status) );
1168 1         337 $self->with_return->( $self->response );
1169 10         297 };
1170              
1171             # resolve relative paths (with '../') as much as possible
1172             # On Windows, absolute paths (e.g., C:/foo) must not be joined with
1173             # the rootdir since Path::Tiny would produce an invalid /C:/foo path.
1174 10         45 my $pt_path = Path::Tiny::path($path);
1175 10 100       518 $file_path = ( $pt_path->is_absolute
1176             ? $pt_path
1177             : Path::Tiny::path( $dir, $path )
1178             )->realpath;
1179              
1180             # We need to check whether they are trying to access
1181             # a directory outside their scope
1182 10 100       3543 $err_response->(403) if !$dir->realpath->subsumes($file_path);
1183              
1184             # other error checks
1185 9 50       2727 $err_response->(403) if !$file_path->exists;
1186 9 50       286 $err_response->(404) if !$file_path->is_file;
1187 9 50       172 $err_response->(403) if !-r $file_path;
1188              
1189             # Read file content as bytes
1190 9         206 $fh = $file_path->openr_raw();
1191 9   50     1445 $content_type = $self->mime_type->for_file($file_path) || 'text/plain';
1192 9 100       200 if ( $content_type =~ m!^text/! ) {
1193 6         218 $charset = $self->config->{charset};
1194             }
1195              
1196             # cleanup for other functions not assuming on Path::Tiny
1197 9         139 $file_path = $file_path->stringify;
1198             }
1199              
1200             # Now we are sure we can render the file...
1201 19         820 $self->execute_hook( 'core.app.before_file_render', $file_path );
1202              
1203             # response content type and charset
1204 19 100       272 ( exists $options{'content_type'} ) and $content_type = $options{'content_type'};
1205 19 100       68 ( exists $options{'charset'} ) and $charset = $options{'charset'};
1206 19 100 100     126 $content_type .= "; charset=$charset" if $content_type and $charset;
1207 19 100       484 ( defined $content_type )
1208             and $self->response->header('Content-Type' => $content_type );
1209              
1210             # content disposition
1211             ( exists $options{filename} )
1212             and $self->response->header( 'Content-Disposition' =>
1213 19 100 100     3119 ($options{content_disposition} || "attachment") . "; filename=\"$options{filename}\"" );
1214              
1215             # use a delayed response unless server does not support streaming
1216 19 100       315 my $use_streaming = exists $options{streaming} ? $options{streaming} : 1;
1217 19         40 my $response;
1218 19         138 my $env = $self->request->env;
1219 19 100 66     176 if ( $env->{'psgi.streaming'} && $use_streaming ) {
1220             my $cb = sub {
1221 18     18   42 my $responder = $Dancer2::Core::Route::RESPONDER;
1222 18         37 my $res = $Dancer2::Core::Route::RESPONSE;
1223 18         594 return $responder->(
1224             [ $res->status, $res->headers_to_array, $fh ]
1225             );
1226 18         118 };
1227              
1228 18         55 Scalar::Util::weaken( my $weak_self = $self );
1229              
1230             $response = Dancer2::Core::Response::Delayed->new(
1231 0     0   0 error_cb => sub { $weak_self->logger_engine->log( warning => @_ ) },
1232 18         608 cb => $cb,
1233             request => $Dancer2::Core::Route::REQUEST,
1234             response => $Dancer2::Core::Route::RESPONSE,
1235             );
1236             }
1237             else {
1238 1         25 $response = $self->response;
1239             # direct assignment to hash element, avoids around modifier
1240             # trying to serialise this this content.
1241              
1242             # optimized slurp
1243             {
1244             ## no critic qw(Variables::RequireInitializationForLocalVars)
1245 1         8 local $/;
  1         5  
1246 1         50 $response->{'content'} = <$fh>;
1247             }
1248              
1249 1         20 $response->is_encoded(1); # bytes are already encoded
1250             }
1251              
1252 19         21126 $self->execute_hook( 'core.app.after_file_render', $response );
1253 19         329 $self->with_return->( $response );
1254             }
1255              
1256             sub BUILD {
1257             my $self = shift;
1258             $self->init_route_handlers();
1259             $self->_init_hooks();
1260             }
1261              
1262             sub finish {
1263 222     222 0 559 my $self = shift;
1264              
1265             # normalize some values that require calculations
1266             defined $self->config->{'static_handler'}
1267 222 100       4307 or $self->config->{'static_handler'} = -d $self->config->{'public_dir'};
1268              
1269 222         15527 $self->register_route_handlers;
1270 222         2924 $self->compile_hooks;
1271              
1272 222 50 66     2177 @{$self->plugins}
  222         4869  
1273             && $self->plugins->[0]->can('_add_postponed_plugin_hooks')
1274             && $self->plugins->[0]->_add_postponed_plugin_hooks(
1275             $self->postponed_hooks
1276             );
1277              
1278 222         3236 foreach my $prep_cb ( @{ $self->prep_apps } ) {
  222         1396  
1279 4         24 $prep_cb->($self);
1280             }
1281             }
1282              
1283             sub init_route_handlers {
1284 259     259 0 663 my $self = shift;
1285              
1286 259         6238 my $handlers_config = $self->config->{route_handlers};
1287 256         8589 for my $handler_data ( @{$handlers_config} ) {
  256         1024  
1288 255         606 my ($handler_name, $config) = @{$handler_data};
  255         1086  
1289 255 50       1245 $config = {} if !is_ref($config);
1290              
1291 255         7470 my $handler = $self->_factory->create(
1292             Handler => $handler_name,
1293             app => $self,
1294             %$config,
1295             postponed_hooks => $self->postponed_hooks,
1296             );
1297              
1298 255         299115 push @{ $self->route_handlers }, {
  255         7429  
1299             name => $handler_name,
1300             handler => $handler,
1301             };
1302             }
1303             }
1304              
1305             sub register_route_handlers {
1306 222     222 0 548 my $self = shift;
1307 222         599 for my $handler ( @{$self->route_handlers} ) {
  222         5422  
1308 220         2275 my $handler_code = $handler->{handler};
1309 220         1601 $handler_code->register($self);
1310             }
1311             }
1312              
1313             sub compile_hooks {
1314 225     225 0 687 my ($self) = @_;
1315              
1316 225         1064 for my $position ( $self->supported_hooks ) {
1317 2025         13590 my $compiled_hooks = [];
1318 2025         2972 for my $hook ( @{ $self->hooks->{$position} } ) {
  2025         35058  
1319 276         2540 Scalar::Util::weaken( my $app = $self );
1320             my $compiled = set_subname subname($hook) => sub {
1321             # don't run the filter if halt has been used
1322 731 100 66 731   3650 $Dancer2::Core::Route::RESPONSE &&
1323             $Dancer2::Core::Route::RESPONSE->is_halted
1324             and return;
1325              
1326 730         2896 eval { $EVAL_SHIM->($hook,@_); 1; }
  715         4832  
1327 730 100       6202 or do {
1328 9   50     46 my $err = $@ || "Zombie Error";
1329 9         28 my $is_hook_exception = $position eq 'core.app.hook_exception';
1330             # Don't execute the hook_exception hook if the exception
1331             # has been generated from a hook exception handler itself,
1332             # thus preventing potentially recursive code.
1333 9 100       276 $app->execute_hook( 'core.app.hook_exception', $app, $err, $position )
1334             unless $is_hook_exception;
1335 8         206 my $is_halted = $app->response->is_halted; # Capture before cleanup
1336             # We can't cleanup if we're in the hook for a hook
1337             # exception, as this would clear the custom response that
1338             # may have been set by the hook. However, there is no need
1339             # to do so, as the upper hook that called this hook
1340             # exception will perform the cleanup instead anyway
1341 8 100       415 $app->cleanup
1342             unless $is_hook_exception;
1343             # Allow the hook function to halt the response, thus
1344             # retaining any response it may have set. Otherwise the
1345             # croak from this function will overwrite any content that
1346             # may have been set by the hook
1347 8 100       110 return if $is_halted;
1348             # Default behavior if nothing else defined
1349 5         35 $app->log('error', "Exception caught in '$position' filter: $err");
1350 5         1114 croak "Exception caught in '$position' filter: $err";
1351             };
1352 276         5593 };
1353              
1354 276         695 push @{$compiled_hooks}, $compiled;
  276         923  
1355             }
1356 2025         16084 $self->replace_hook( $position, $compiled_hooks );
1357             }
1358             }
1359              
1360             sub lexical_prefix {
1361 5     5 1 2723 my $self = shift;
1362 5         13 my $prefix = shift;
1363 5         11 my $cb = shift;
1364              
1365 5 100       24 $prefix eq '/' and undef $prefix;
1366              
1367             # save the app prefix
1368 5         132 my $app_prefix = $self->prefix;
1369              
1370             # alter the prefix for the callback
1371 5 100       58 my $new_prefix =
    100          
1372             ( defined $app_prefix ? $app_prefix : '' )
1373             . ( defined $prefix ? $prefix : '' );
1374              
1375             # if the new prefix is empty, it's a meaningless prefix, just ignore it
1376 5 100       130 length $new_prefix and $self->prefix($new_prefix);
1377              
1378 5         167 my $err;
1379 5         27 my $ok= eval { $EVAL_SHIM->($cb); 1 }
  4         23  
1380 5 100 50     13 or do { $err = $@ || "Zombie Error"; };
  1         45  
1381              
1382             # restore app prefix
1383 5         125 $self->prefix($app_prefix);
1384              
1385 5 100       685 $ok or croak "Unable to run the callback for prefix '$prefix': $err";
1386             }
1387              
1388             sub add_route {
1389 686     686 1 2500 my $self = shift;
1390 686         3196 my %route_attrs = @_;
1391              
1392             my $route = Dancer2::Core::Route->new(
1393             type_library => $self->config->{type_library},
1394 686         18492 prefix => $self->prefix,
1395             %route_attrs,
1396             );
1397              
1398 686         180945 my $method = $route->method;
1399 686         1617 push @{ $self->routes->{$method} }, $route;
  686         17538  
1400              
1401 686 100 100     12552 if ( $method ne 'head' && $route->has_name() ) {
1402 11         36 my $name = $route->name;
1403 11 100       255 $self->route_names->{$name}
1404             and die "Route with this name ($name) already exists";
1405              
1406 10         216 $self->route_names->{$name} = $route;
1407             }
1408              
1409 685         6894 return $route;
1410             }
1411              
1412             sub route_exists {
1413 0     0 1 0 my $self = shift;
1414 0         0 my $route = shift;
1415              
1416 0         0 my $routes = $self->routes->{ $route->method };
1417              
1418 0         0 foreach my $existing_route (@$routes) {
1419 0 0       0 $existing_route->spec_route eq $route->spec_route
1420             and return 1;
1421             }
1422              
1423 0         0 return 0;
1424             }
1425              
1426             sub routes_regexps_for {
1427 1     1 1 778 my $self = shift;
1428 1         2 my $method = shift;
1429              
1430 1         2 return [ map $_->regexp, @{ $self->routes->{$method} } ];
  1         22  
1431             }
1432              
1433             sub cookie {
1434 589     589 0 1223 my $self = shift;
1435              
1436 589 100       4653 @_ == 1 and return $self->request->cookies->{ $_[0] };
1437              
1438             # writer
1439 3         25 my ( $name, $value, %options ) = @_;
1440 3         112 my $c =
1441             Dancer2::Core::Cookie->new( name => $name, value => $value, %options );
1442 3         235 $self->response->push_header( 'Set-Cookie' => $c->to_header );
1443             }
1444              
1445             sub redirect {
1446 26     26 1 43 my $self = shift;
1447 26         51 my $destination = shift;
1448 26         45 my $status = shift;
1449              
1450 26 100       119 if ($destination =~ m{^/(?!/)}) {
1451             # If the app is mounted to something other than "/", we must
1452             # preserve its path.
1453 12         114 my $script_name = $self->request->script_name;
1454 12         103 $script_name =~ s{/$}{}; # Remove trailing slash (if present).
1455 12         55 $destination = $script_name . $destination;
1456             }
1457              
1458 26         522 $self->response->redirect( $destination, $status );
1459              
1460             # Short circuit any remaining before hook / route code
1461             # ('pass' and after hooks are still processed)
1462 26 50       3955 $self->has_with_return
1463             and $self->with_return->($self->response);
1464             }
1465              
1466             sub halt {
1467 7     7 1 16 my $self = shift;
1468 7         144 $self->response->halt( @_ );
1469              
1470             # Short citcuit any remaining hook/route code
1471 7 50       334 $self->has_with_return
1472             and $self->with_return->($self->response);
1473             }
1474              
1475             sub pass {
1476 2     2 1 6 my $self = shift;
1477 2         53 $self->response->pass;
1478              
1479             # Short citcuit any remaining hook/route code
1480 2 50       174 $self->has_with_return
1481             and $self->with_return->($self->response);
1482             }
1483              
1484             sub forward {
1485 44     44 1 88 my $self = shift;
1486 44         87 my $url = shift;
1487 44         76 my $params = shift;
1488 44         75 my $options = shift;
1489              
1490 44         154 my $new_request = $self->make_forward_to( $url, $params, $options );
1491              
1492 44 50       334 $self->has_with_return
1493             and $self->with_return->($new_request);
1494              
1495             # nothing else will run after this
1496             }
1497              
1498             # Create a new request which is a clone of the current one, apart
1499             # from the path location, which points instead to the new location
1500             sub make_forward_to {
1501 53     53 0 253 my $self = shift;
1502 53         103 my $url = shift;
1503 53         91 my $params = shift;
1504 53         92 my $options = shift;
1505              
1506 53         174 my $overrides = { PATH_INFO => $url };
1507             exists $options->{method} and
1508 53 100       205 $overrides->{REQUEST_METHOD} = $options->{method};
1509              
1510             # "clone" the existing request
1511 53         358 my $new_request = $self->request->_shallow_clone( $params, $overrides );
1512              
1513             # If a session object was created during processing of the original request
1514             # i.e. a session object exists but no cookie existed
1515             # add a cookie so the dispatcher can assign the session to the appropriate app
1516 53         1653 my $engine = $self->session_engine;
1517 53 100 66     2048 $engine && $self->_has_session or return $new_request;
1518 11         32 my $name = $engine->cookie_name;
1519 11 100       34 exists $new_request->cookies->{$name} and return $new_request;
1520 10         253 $new_request->cookies->{$name} =
1521             Dancer2::Core::Cookie->new( name => $name, value => $self->session->id );
1522              
1523 10         43 return $new_request;
1524             }
1525              
1526 1     1 1 2568 sub app { shift }
1527              
1528             # DISPATCHER
1529             sub to_app {
1530 220     220 0 1030 my $self = shift;
1531              
1532             # build engines
1533             {
1534 220         515 for ( qw ) {
  220         1259  
1535 660         34376 my $attr = "${_}_engine";
1536 660         16496 $self->$attr;
1537             }
1538              
1539             # the serializer engine does not have a default
1540             # and is the only engine that can actually not have a value
1541 220 100       23487 if ( $self->config->{'serializer'} ) {
1542 23         759 $self->serializer_engine;
1543             }
1544             }
1545              
1546 220         3563 $self->finish;
1547              
1548             my $psgi = sub {
1549 631     631   1981702 my $env = shift;
1550              
1551             # pre-request sanity check
1552 631         2503 my $method = uc $env->{'REQUEST_METHOD'};
1553 631 100       3727 $Dancer2::Core::Types::supported_http_methods{$method}
1554             or return [
1555             405,
1556             [ 'Content-Type' => 'text/plain' ],
1557             [ "Method Not Allowed\n\n$method is not supported." ]
1558             ];
1559              
1560 629         1335 my $response;
1561             eval {
1562 629         5931 $EVAL_SHIM->(sub{ $response = $self->dispatch($env)->to_psgi });
  629         2906  
1563 628         9522 1;
1564 629 100       1444 } or do {
1565 1   50     6 my $err = $@ || "Zombie Error";
1566             return [
1567 1         18 500,
1568             [ 'Content-Type' => 'text/plain' ],
1569             [ "Internal Server Error\n\n$err" ],
1570             ];
1571             };
1572              
1573 628         10663 return $response;
1574 220         2337 };
1575              
1576             # Only add static content handler if required
1577 220 100       4588 if ( $self->config->{'static_handler'} ) {
1578             # Use App::File to "serve" the static content
1579             my $static_app = Plack::App::File->new(
1580             root => $self->_public_dir_path->stringify,
1581 8     8   3171 content_type => sub { $self->mime_type->for_file( $_[0] ) },
1582 119         3281 )->to_app;
1583             # Conditionally use the static handler wrapped with ConditionalGET
1584             # when the file exists. Otherwise the request passes into our app.
1585             $psgi = Plack::Middleware::Conditional->wrap(
1586             $psgi,
1587             condition => sub {
1588 435     435   2644435 my $env = shift;
1589             $self->_public_dir_path->child(
1590             defined $env->{'PATH_INFO'} && length $env->{'PATH_INFO'}
1591 435 100 66     15470 ? ($env->{'PATH_INFO'})
1592             : (),
1593             )->is_file;
1594             },
1595 119     119   9522 builder => sub { Plack::Middleware::ConditionalGET->wrap( $static_app ) },
1596 119         12758 );
1597             }
1598              
1599             # Wrap with common middleware
1600 220 100       10976 if ( ! $self->config->{'no_default_middleware'} ) {
1601             # FixMissingBodyInRedirect
1602 216         4362 $psgi = Plack::Middleware::FixMissingBodyInRedirect->wrap( $psgi );
1603             # Apply Head. After static so a HEAD request on static content DWIM.
1604 216         11053 $psgi = Plack::Middleware::Head->wrap( $psgi );
1605             }
1606              
1607 220         8879 return $psgi;
1608             }
1609              
1610             sub dispatch {
1611 629     629 0 1304 my $self = shift;
1612 629         1214 my $env = shift;
1613              
1614 629         3773 my $runner = Dancer2::runner();
1615 629         1369 my $request;
1616 629         1263 my $request_built_successfully = eval {
1617             $EVAL_SHIM->(sub {
1618 629   100 629   6368 $request = $runner->{'internal_request'} || $self->build_request($env);
1619 629         4276 });
1620 625         3636 1;
1621             };
1622             # Catch bad content causing deserialization to fail when building the request
1623 629 100       2410 if ( ! $request_built_successfully ) {
1624 4         10 my $err = $@;
1625 4         12 Scalar::Util::weaken(my $app = $self);
1626             return Dancer2::Core::Error->new(
1627             app => $app,
1628             message => $err,
1629             status => 400, # 400 Bad request (dont send again), rather than 500
1630             charset => $self->config->{charset},
1631 4         91 )->throw;
1632             }
1633              
1634 625         21588 my $cname = $self->session_engine->cookie_name;
1635 625         9313 my $defined_engines = $self->defined_engines;
1636              
1637             DISPATCH:
1638 625         38509 while (1) {
1639 652         4743 my $http_method = lc $request->method;
1640 652         8079 my $path_info = $request->path_info;
1641              
1642             # Add request to app and engines
1643 652         6399 $self->set_request($request, $defined_engines);
1644              
1645 652         78409 $self->log( core => "looking for $http_method $path_info" );
1646              
1647             ROUTE:
1648 652         1402 foreach my $route ( @{ $self->routes->{$http_method} } ) {
  652         14215  
1649             #warn "testing route " . $route->regexp . "\n";
1650             # TODO store in route cache
1651              
1652             # go to the next route if no match
1653 1366 100       13124 my $match = $route->match($request)
1654             or next ROUTE;
1655              
1656 588         24031 $request->_set_route_params($match);
1657 588         2689 $request->_set_route_parameters($match);
1658 588         25822 $request->_set_route($route);
1659              
1660             # Add session to app *if* we have a session and the request
1661             # has the appropriate cookie header for _this_ app.
1662 588 100       2900 if ( my $sess = $runner->{'internal_sessions'}{$cname} ) {
1663 6         224 $self->set_session($sess);
1664             }
1665              
1666             # calling the actual route
1667 588         1361 my $response;
1668              
1669             # this is very evil, but allows breaking out of multiple stack
1670             # frames without throwing an exception. Avoiding exceptions means
1671             # a naive eval won't swallow our flow control mechanisms, and
1672             # avoids __DIE__ handlers. It also prevents some cleanup routines
1673             # from working, since they are expecting control to return to them
1674             # after an eval.
1675             DANCER2_CORE_APP_ROUTE_RETURN: {
1676 588 50       983 if (!$self->has_with_return) {
  588         3395  
1677             $self->set_with_return(sub {
1678 107     107   509 $response = shift;
1679 160     160   1910 no warnings 'exiting';
  160         477  
  160         295975  
1680 107         1040 last DANCER2_CORE_APP_ROUTE_RETURN;
1681 588         4852 });
1682             }
1683 588         2420 $response = $self->_dispatch_route($route);
1684             };
1685              
1686             # ensure we clear the with_return handler
1687 588         15341 $self->clear_with_return;
1688              
1689             # handle forward requests
1690 588 100       6275 if ( ref $response eq 'Dancer2::Core::Request' ) {
1691             # this is actually a request, not response
1692             # however, we need to clean up the request & response
1693 44         930 $self->clear_request;
1694 44         919 $self->clear_response;
1695              
1696             # this is in case we're asked for an old-style dispatching
1697 44 100       591 if ( $runner->{'internal_dispatch'} ) {
1698             # Get the session object from the app before we clean up
1699             # the request context, so we can propagate this to the
1700             # next dispatch cycle (if required).
1701             $self->_has_session
1702 17 100       246 and $runner->{'internal_sessions'}{$cname} =
1703             $self->session;
1704              
1705 17         111 $runner->{'internal_forward'} = 1;
1706 17         44 $runner->{'internal_request'} = $response;
1707 17         67 return $self->response_not_found($request);
1708             }
1709              
1710 27         53 $request = $response;
1711 27         81 next DISPATCH;
1712             }
1713              
1714             # from here we assume the response is a Dancer2::Core::Response
1715              
1716             # halted response, don't process further
1717 544 100       10374 if ( $response->is_halted ) {
1718 34         396 $self->cleanup;
1719 34         280 delete $runner->{'internal_request'};
1720 34         638 return $response;
1721             }
1722              
1723             # pass the baton if the response says so...
1724 510 100       12651 if ( $response->has_passed ) {
1725             ## A previous route might have used splat, failed
1726             ## this needs to be cleaned from the request.
1727             exists $request->{_params}{splat}
1728 9 100       100 and delete $request->{_params}{splat};
1729              
1730 9         236 $response->has_passed(0); # clear for the next round
1731              
1732             # clear the content because if you pass it,
1733             # the next route is in charge of catching it
1734 9         475 $response->clear_content;
1735 9         80 next ROUTE;
1736             }
1737              
1738             # it's just a regular response
1739 501         15156 $self->execute_hook( 'core.app.after_request', $response );
1740 500         2548 $self->cleanup;
1741 500         3638 delete $runner->{'internal_request'};
1742              
1743 500         7799 return $response;
1744             }
1745              
1746             # we don't actually want to continue the loop
1747 73         261 last;
1748             }
1749              
1750             # No response! ensure Core::Dispatcher recognizes this failure
1751             # so it can try the next Core::App
1752             # and set the created request so we don't create it again
1753             # (this is important so we don't ignore the previous body)
1754 73 100       345 if ( $runner->{'internal_dispatch'} ) {
1755 51         136 $runner->{'internal_404'} = 1;
1756 51         127 $runner->{'internal_request'} = $request;
1757             }
1758              
1759             # Render 404 response, cleanup, and return the response.
1760 73         385 my $response = $self->response_not_found($request);
1761 73         314 $self->cleanup;
1762 73         1001 return $response;
1763             }
1764              
1765             sub build_request {
1766 580     580 0 1650 my ( $self, $env ) = @_;
1767 580         1752 Scalar::Util::weaken( my $weak_self = $self );
1768              
1769             # If we have an app, send the serialization engine
1770             my $request = Dancer2::Core::Request->new(
1771             env => $env,
1772             is_behind_proxy => $self->settings->{'behind_proxy'} || 0,
1773 0     0   0 uri_for_route => sub { shift; $weak_self->uri_for_route(@_) },
  0         0  
1774             strict_utf8 => $self->config->{strict_utf8},
1775              
1776 580 100 100     2638 $self->has_serializer_engine
1777             ? ( serializer => $self->serializer_engine )
1778             : (),
1779             );
1780              
1781 576         5605 return $request;
1782             }
1783              
1784             # Call any before hooks then the matched route.
1785             sub _dispatch_route {
1786 588     588   1475 my ( $self, $route ) = @_;
1787              
1788 588         1194 local $@;
1789             eval {
1790             $EVAL_SHIM->(sub {
1791 588     588   20146 $self->execute_hook( 'core.app.before_request', $self );
1792 588         3362 });
1793 579         9826 1;
1794 588 100       1312 } or do {
1795 4   50     113 my $err = $@ || "Zombie Error";
1796 4         28 return $self->response_internal_error($err);
1797             };
1798 579         11736 my $response = $self->response;
1799              
1800 579 100       15219 if ( $response->is_halted ) {
1801 2         26 return $self->_prep_response( $response );
1802             }
1803              
1804             eval {
1805 577     577   4097 $EVAL_SHIM->(sub{ $response = $route->execute($self) });
  577         3638  
1806 462         3148 1;
1807 577 100       5018 } or do {
1808 14   50     394 my $err = $@ || "Zombie Error";
1809 14         112 return $self->response_internal_error($err);
1810             };
1811              
1812 462         1581 return $response;
1813             }
1814              
1815             sub _prep_response {
1816 485     485   1468 my ( $self, $response, $content ) = @_;
1817              
1818             # The response object has no back references to the content or app
1819             # Update the default_content_type of the response if any value set in
1820             # config so it can be applied when the response is encoded/returned.
1821 485         11929 my $config = $self->config;
1822 485 50 33     7219 if ( exists $config->{content_type}
1823             and my $ct = $config->{content_type} ) {
1824 485         10570 $response->default_content_type($ct);
1825             }
1826             exists $config->{charset}
1827 485 50       29288 and $response->charset( $config->{charset} );
1828              
1829             # if we were passed any content, set it in the response
1830 485 100       28427 defined $content && $response->content($content);
1831 485         60918 return $response;
1832             }
1833              
1834             sub response_internal_error {
1835 18     18 0 56 my ( $self, $error ) = @_;
1836              
1837 18         640 $self->execute_hook( 'core.app.route_exception', $self, $error );
1838 17         314 $self->log( error => "Route exception: $error" );
1839              
1840 17         270 local $Dancer2::Core::Route::REQUEST = $self->request;
1841 17         352 local $Dancer2::Core::Route::RESPONSE = $self->response;
1842              
1843             return Dancer2::Core::Error->new(
1844             app => $self,
1845             status => 500,
1846             exception => $error,
1847             charset => $self->config->{charset},
1848 17         470 )->throw;
1849             }
1850              
1851             sub response_not_found {
1852 99     99 0 291 my ( $self, $request ) = @_;
1853              
1854 99         367 $self->set_request($request);
1855              
1856 99         10584 local $Dancer2::Core::Route::REQUEST = $self->request;
1857 99         2156 local $Dancer2::Core::Route::RESPONSE = $self->response;
1858              
1859             my $response = Dancer2::Core::Error->new(
1860             app => $self,
1861             status => 404,
1862             message => $request->path,
1863             charset => $self->config->{charset},
1864 99         15178 )->throw;
1865              
1866 99         1629 $self->cleanup;
1867              
1868 99         1869 return $response;
1869             }
1870              
1871             sub uri_for_route {
1872 18     18 0 42 my ( $self, $route_name, $route_params, $query_params, $dont_escape ) = @_;
1873 18 100       237 my $route = $self->route_names->{$route_name}
1874             or die "Cannot find route named '$route_name'";
1875              
1876 16         117 my $string = $route->spec_route;
1877 16 50       30 is_regexpref($string)
1878             and die "uri_for_route() does not support regexp route paths";
1879              
1880             # Convert splat only to the general purpose structure
1881 16 100       26 if ( is_arrayref($route_params) ) {
1882 4         10 $route_params = { 'splat' => $route_params };
1883             }
1884              
1885             # The regexes are taken and altered from:
1886             # Dancer2::Core::Route::_build_regexp_from_string.
1887              
1888             # Replace :foo with arg (route parameters)
1889             # Not a fan of all this regex play to handle typed parameter -- SX
1890 16         66 my @params = $string =~ m{:([^/.\?]+)}xmsg;
1891              
1892 16         26 foreach my $param (@params) {
1893 14         50 $param =~ s{^([^\[]+).*}{$1}xms;
1894 14 100       47 my $value = $route_params->{$param}
1895             or die "Route $route_name uses the parameter '${param}', which was not provided";
1896              
1897 12         212 $string =~ s!\Q:$param\E(\[[^\]]+\])?!$value!xmsg;
1898             }
1899              
1900             # TODO: Can we cut this down by replacing on the spot?
1901             # I think that will be tricky because we first need all **, then *
1902              
1903 14         37 $string =~ s!\Q**\E!(?#megasplat)!g;
1904 14         30 $string =~ s!\*!(?#splat)!g;
1905              
1906             # TODO: Can we cut this down?
1907 14         67 my @token_or_splat =
1908             $string =~ /\(\?#((?:mega)?splat)\)/g;
1909              
1910 14         20 my $splat_params = $route_params->{'splat'};
1911 14 100 66     38 if ($splat_params && @token_or_splat) {
1912 6 100       8 $#{$splat_params} == $#token_or_splat
  6         45  
1913             or die 'Mismatch in amount of splat args and splat elements';
1914              
1915 4         8 for ( my $i = 0; $i < @{$splat_params}; $i++ ) {
  14         22  
1916 10 100       18 if ( is_arrayref($splat_params->[$i]) ){
1917 4         5 my $megasplat = join '/', @{ $splat_params->[$i] };
  4         9  
1918 4         9 $string =~ s{\Q(?#megasplat)\E}{$megasplat};
1919             } else {
1920 6         44 $string =~ s{\Q(?#splat)\E}{$splat_params->[$i]};
1921             }
1922             }
1923             }
1924              
1925 12         43 return $self->request->uri_for( $string, $query_params, $dont_escape );
1926             }
1927              
1928             1;
1929              
1930             __END__