File Coverage

blib/lib/CGI/Ex/App.pm
Criterion Covered Total %
statement 577 592 97.4
branch 288 344 83.7
condition 241 325 74.1
subroutine 163 165 98.7
pod 103 135 76.3
total 1372 1561 87.8


line stmt bran cond sub pod time code
1             package CGI::Ex::App;
2              
3             ###---------------------###
4             # Copyright - Paul Seamons
5             # Distributed under the Perl Artistic License without warranty
6              
7 1     1   707 use 5.006; #our
  1         3  
8 1     1   4 use strict;
  1         1  
  1         23  
9             BEGIN {
10 1     1   412 eval { use Time::HiRes qw(time) };
  1     1   1102  
  1         3  
  1         2  
  0         0  
11 1     1   170 eval { use Scalar::Util };
  1         2  
  1         26  
  1         7765  
  0         0  
12             }
13             our $VERSION = '2.53'; # VERSION
14              
15 21     21 0 248 sub croak { die sprintf "%s at %3\$s line %4\$s\n", $_[0], caller 1 }
16              
17             sub new {
18 196   66 196 1 32225 my $class = shift || croak "Missing class name";
19 194 100       640 my $self = bless ref($_[0]) ? shift() : (@_ % 2) ? {} : {@_}, $class;
    100          
20 194         506 $self->init;
21 194         535 $self->init_from_conf;
22 192         775 return $self;
23             }
24              
25       69 1   sub init {}
26             sub init_from_conf {
27 194     194 1 229 my $self = shift;
28 194 100 100     359 @$self{keys %$_} = values %$_ if $self->load_conf and $_ = $self->conf;
29             }
30              
31             sub import { # only ever called with explicit use CGI::Ex::App qw() - not with use base
32 3     3   1328 my $class = shift;
33 3 50       6 return if not @_ = grep { /^:?App($|__)/ } @_;
  3         23  
34 3         424 require CGI::Ex::App::Constants;
35 3         7 unshift @_, 'CGI::Ex::App::Constants';
36 3         7949 goto &CGI::Ex::App::Constants::import;
37             }
38              
39             ###---------------------###
40              
41             sub navigate {
42 78     78 1 2674 my ($self, $args) = @_;
43 78 100       172 $self = $self->new($args) if ! ref $self;
44              
45 78         190 $self->{'_time'} = time;
46 78         105 eval {
47 78 100 100     285 return $self if ! $self->{'_no_pre_navigate'} && $self->pre_navigate;
48 77 100       101 local $self->{'_morph_lineage_start_index'} = $#{$self->{'_morph_lineage'} || []};
  77         275  
49 77         211 $self->nav_loop;
50             };
51 78         178 my $err = $@;
52 78 100 66     194 if ($err && (ref($err) || $err ne "Long Jump\n")) { # catch any errors
      100        
53 8 50       30 die $err if ! $self->can('handle_error');
54 8 100       14 if (! eval { $self->handle_error($err); 1 }) {
  8         18  
  6         15  
55 2         17 die "$err\nAdditionally, the following happened while calling handle_error: $@";
56             }
57             }
58 76 0 66     158 $self->handle_error($@) if ! $self->{'_no_post_navigate'} && ! eval { $self->post_navigate; 1 } && $@ && $@ ne "Long Jump\n";
      33        
      0        
59 76         188 $self->destroy;
60 76         249 return $self;
61             }
62              
63             sub nav_loop {
64 153     153 1 184 my $self = shift;
65 153   100     361 local $self->{'_recurse'} = $self->{'_recurse'} || 0;
66 153 100       326 if ($self->{'_recurse'}++ >= $self->recurse_limit) {
67 2         4 my $err = "recurse_limit (".$self->recurse_limit.") reached";
68 2 50 50     10 croak(($self->{'jumps'} || 0) <= 1 ? $err : "$err number of jumps (".$self->{'jumps'}.")");
69             }
70              
71 151         304 my $path = $self->path;
72 149 100       313 return if $self->pre_loop($path);
73              
74 148   100     461 foreach ($self->{'path_i'} ||= 0; $self->{'path_i'} <= $#$path; $self->{'path_i'}++) {
75 127         215 my $step = $path->[$self->{'path_i'}];
76 127 100       577 if ($step !~ /^([^\W0-9]\w*)$/) {
77 1         4 $self->stash->{'forbidden_step'} = $step;
78 1         4 $self->goto_step($self->forbidden_step);
79             }
80 126         291 $step = $1; # untaint
81              
82 126 100       276 if (! $self->is_authed) {
83 125         243 my $req = $self->run_hook('require_auth', $step, 1);
84 125 100 66     310 return if (ref($req) ? $req->{$step} : $req) && ! $self->run_hook('get_valid_auth', $step);
    50          
85             }
86              
87 119         250 $self->run_hook('morph', $step); # let steps be in external modules
88 117         221 $self->parse_path_info('path_info_map', $self->run_hook('path_info_map', $step));
89 115 100       218 if ($self->run_hook('run_step', $step)) {
90 68         146 $self->run_hook('unmorph', $step);
91 68         171 return;
92             }
93              
94 36         115 $self->run_hook('refine_path', $step, $self->{'path_i'} >= $#$path);
95 36         62 $self->run_hook('unmorph', $step);
96             }
97              
98 57 100       147 return if $self->post_loop($path);
99 56         124 $self->insert_path($self->default_step); # run the default step as a last resort
100 56         232 $self->nav_loop; # go recursive
101 22         59 return;
102             }
103              
104             sub path {
105 329     329 1 478 my $self = shift;
106 329   66     653 return $self->{'path'} ||= do {
107 80         90 my @path;
108 80         193 $self->parse_path_info('path_info_map_base', $self->path_info_map_base); # add initial items to the form hash from path_info
109 76         314 my $step = $self->form->{$self->step_key}; # make sure the step is valid
110 76 100       172 if (defined $step) {
111 41         77 $step =~ s|^/+||; $step =~ s|/|__|g;
  41         69  
112 41 100 100     485 if ($step =~ /^_/) { # can't begin with _
    100 100        
      100        
113 1         5 $self->stash->{'forbidden_step'} = $step;
114 1         4 push @path, $self->forbidden_step;
115             } elsif ($self->valid_steps # must be in valid_steps if defined
116             && ! $self->valid_steps->{$step}
117             && $step ne $self->default_step
118             && $step ne $self->js_step) {
119 1         4 $self->stash->{'forbidden_step'} = $step;
120 1         4 push @path, $self->forbidden_step;
121             } else {
122 39         69 push @path, $step;
123             }
124             }
125 76         227 \@path;
126             };
127             }
128              
129             sub parse_path_info {
130 197     197 0 484 my ($self, $type, $maps, $info, $form) = @_;
131 197 100       365 return if !$maps;
132 88   100     245 $info ||= $self->path_info || return;
      66        
133 18 100       66 croak "Usage: sub $type { [] }" if ! UNIVERSAL::isa($maps, 'ARRAY');
134 15         26 foreach my $map (@$maps) {
135 12 100       27 croak "Usage: sub $type { [[qr{/path_info/(\\w+)}, 'keyname']] }" if ! UNIVERSAL::isa($map, 'ARRAY');
136 9 100       55 my @match = $info =~ $map->[0] or next;
137 6   33     22 $form ||= $self->form;
138 6 100       38 if (UNIVERSAL::isa($map->[1], 'CODE')) {
139 1         5 $map->[1]->($form, @match);
140             } else {
141 5         10 $form->{$map->[$_]} = $match[$_ - 1] foreach grep {! defined $form->{$map->[$_]}} 1 .. $#$map;
  5         24  
142             }
143 6         18 last;
144             }
145             }
146              
147             sub run_hook {
148 2545     2545 1 4573 my ($self, $hook, $step, @args) = @_;
149 2545 50       4417 my ($code, $found) = (ref $hook eq 'CODE') ? ($_[1], $hook = 'coderef') : ($self->find_hook($hook, $step));
150 2545 100       4147 croak "Could not find a method named ${step}_${hook} or ${hook}" if ! $code;
151              
152 2544 100       5295 return scalar $self->$code($step, @args) if !$self->{'no_history'};
153              
154 31         28 push @{ $self->history }, my $hist = {step => $step, meth => $hook, found => $found, time => time, level => $self->{'_level'}, elapsed => 0};
  31         47  
155 31   100     83 local $self->{'_level'} = 1 + ($self->{'_level'} || 0);
156 31         62 $hist->{'elapsed'} = time - $hist->{'time'};
157 31         57 return $hist->{'response'} = $self->$code($step, @args);
158             }
159              
160             sub find_hook {
161 2545     2545 1 3087 my ($self, $hook, $step) = @_;
162 2545 100       3287 croak "Missing hook name" if ! $hook;
163 2544 100 100     14699 if ($step and my $code = $self->can("${step}_${hook}")) {
    100          
164 311         793 return ($code, "${step}_${hook}");
165             } elsif ($code = $self->can($hook)) {
166 2232         5152 return ($code, $hook);
167             }
168 1         3 return;
169             }
170              
171             sub run_hook_as {
172 3     3 1 2785 my ($self, $hook, $step, $pkg, @args) = @_;
173 3 50       6 croak "Missing hook" if ! $hook;
174 3 50       7 croak "Missing step" if ! $step;
175 3 50       6 croak "Missing package" if ! $pkg;
176 3         9 $self->morph($step, 2, $pkg);
177 3         7 my $resp = $self->run_hook($hook, $step, @args);
178 3         7 $self->unmorph;
179 3         5 return $resp;
180             }
181              
182             sub run_step {
183 111     111 1 157 my ($self, $step) = @_;
184 111 100       169 return 1 if $self->run_hook('pre_step', $step); # if true exit the nav_loop
185 109 100       177 return 0 if $self->run_hook('skip', $step); # if true skip this step
186              
187             # check for complete valid information for this step
188 101 100 100     170 if ( ! $self->run_hook('prepare', $step)
      100        
189             || ! $self->run_hook('info_complete', $step)
190             || ! $self->run_hook('finalize', $step)) {
191              
192 73         274 $self->run_hook('prepared_print', $step); # show the page requesting the information
193 72         831 $self->run_hook('post_print', $step); # a hook after the printing process
194              
195 72         201 return 1;
196             }
197              
198 28 100       46 return 1 if $self->run_hook('post_step', $step); # if true exit the nav_loop
199 27         57 return 0; # let the nav_loop continue searching the path
200             }
201              
202             sub prepared_print {
203 73     73 1 83 my $self = shift;
204 73         92 my $step = shift;
205 73   100     110 my $hash_form = $self->run_hook('hash_form', $step) || {};
206 73   100     316 my $hash_base = $self->run_hook('hash_base', $step) || {};
207 73   100     117 my $hash_comm = $self->run_hook('hash_common', $step) || {};
208 73   100     125 my $hash_swap = $self->run_hook('hash_swap', $step) || {};
209 72   100     139 my $hash_fill = $self->run_hook('hash_fill', $step) || {};
210 72   100     140 my $hash_errs = $self->run_hook('hash_errors', $step) || {};
211 72         213 $hash_errs->{$_} = $self->format_error($hash_errs->{$_}) foreach keys %$hash_errs;
212 72 100       130 $hash_errs->{'has_errors'} = 1 if scalar keys %$hash_errs;
213              
214 72         593 my $swap = {%$hash_form, %$hash_base, %$hash_comm, %$hash_swap, %$hash_errs};
215 72         364 my $fill = {%$hash_form, %$hash_base, %$hash_comm, %$hash_fill};
216 72         165 $self->run_hook('print', $step, $swap, $fill);
217             }
218              
219             sub print {
220 72     72 1 104 my ($self, $step, $swap, $fill) = @_;
221 72         104 my $file = $self->run_hook('file_print', $step); # get a filename relative to template_path
222 72         223 my $out = $self->run_hook('swap_template', $step, $file, $swap);
223 72         204 $self->run_hook('fill_template', $step, \$out, $fill);
224 72         189 $self->run_hook('print_out', $step, \$out);
225             }
226              
227             sub handle_error {
228 8     8 1 14 my ($self, $err) = @_;
229 8 50       14 die $err if $self->{'_handling_error'};
230 8         22 local @$self{'_handling_error', '_recurse' } = (1, 0); # allow for this next step - even if we hit a recurse error
231 8         20 $self->stash->{'error_step'} = $self->current_step;
232 6         10 $self->stash->{'error'} = $err;
233 6         7 eval {
234 6         17 my $step = $self->error_step;
235 6         22 $self->morph($step); # let steps be in external modules
236 6 50       22 $self->run_hook('run_step', $step) && $self->unmorph($step);
237             };
238 6 50 33     23 die $@ if $@ && $@ ne "Long Jump\n";
239             }
240              
241             ###---------------------###
242             # read only accessors
243              
244 117     117 1 369 sub allow_morph { $_[0]->{'allow_morph'} }
245 2     2 1 9 sub auth_args { $_[0]->{'auth_args'} }
246 11 50   11 1 26 sub auth_obj { shift->{'auth_obj'} || do { require CGI::Ex::Auth; CGI::Ex::Auth->new(@_) } }
  11         960  
  11         36  
247 5 100   5 0 25 sub charset { $_[0]->{'charset'} || '' }
248 5     5 1 16 sub conf_args { $_[0]->{'conf_args'} }
249 2 100   2 0 11 sub conf_die_on_fail { $_[0]->{'conf_die_on_fail'} || ! defined $_[0]->{'conf_die_on_fail'} }
250 3 100   3 1 13 sub conf_path { $_[0]->{'conf_path'} || $_[0]->base_dir_abs }
251 4     4 1 9 sub conf_validation { $_[0]->{'conf_validation'} }
252 60 100   60 1 274 sub default_step { $_[0]->{'default_step'} || 'main' }
253 8 100   8 1 33 sub error_step { $_[0]->{'error_step'} || '__error' }
254 71     71 1 216 sub fill_args { $_[0]->{'fill_args'} }
255 5 100   5 1 33 sub forbidden_step { $_[0]->{'forbidden_step'} || '__forbidden' }
256 79 50   79 1 248 sub form_name { $_[0]->{'form_name'} || 'theform' }
257 519   100 519 1 1887 sub history { $_[0]->{'history'} ||= [] }
258 18 100   18 0 76 sub js_step { $_[0]->{'js_step'} || 'js' }
259 9 100   9 0 46 sub login_step { $_[0]->{'login_step'} || '__login' }
260 5 100   5 0 33 sub mimetype { $_[0]->{'mimetype'} || 'text/html' }
261 173 100 100 173 0 823 sub path_info { $_[0]->{'path_info'} || $ENV{'PATH_INFO'} || '' }
262 80 100   80 1 382 sub path_info_map_base { $_[0]->{'path_info_map_base'} ||[[qr{/(\w+)}, $_[0]->step_key]] }
263 155 100   155 1 447 sub recurse_limit { $_[0]->{'recurse_limit'} || 15 }
264 116 100 100 116 0 613 sub script_name { $_[0]->{'script_name'} || $ENV{'SCRIPT_NAME'} || $0 }
265 24   100 24 1 70 sub stash { $_[0]->{'stash'} ||= {} }
266 224 100   224 1 1077 sub step_key { $_[0]->{'step_key'} || 'step' }
267 74     74 1 218 sub template_args { $_[0]->{'template_args'} }
268 73 100   73 1 173 sub template_obj { shift->{'template_obj'} || do { require Template::Alloy; Template::Alloy->new(@_) } }
  72         862  
  72         21518  
269 78 100   78 1 295 sub template_path { $_[0]->{'template_path'} || $_[0]->base_dir_abs }
270 16     16 0 47 sub val_args { $_[0]->{'val_args'} }
271 10 100   10 0 64 sub val_path { $_[0]->{'val_path'} || $_[0]->template_path }
272              
273             sub conf_obj {
274 4     4 1 7 my $self = shift;
275 4   66     12 return $self->{'conf_obj'} || do {
276             my $args = $self->conf_args || {};
277             $args->{'paths'} ||= $self->conf_path;
278             $args->{'directive'} ||= 'MERGE';
279             require CGI::Ex::Conf;
280             CGI::Ex::Conf->new($args);
281             };
282             }
283              
284             sub val_obj {
285 15     15 0 19 my $self = shift;
286 15   66     41 return $self->{'val_obj'} || do {
287             my $args = $self->val_args || {};
288             $args->{'cgix'} ||= $self->cgix;
289             require CGI::Ex::Validate;
290             CGI::Ex::Validate->new($args);
291             };
292             }
293              
294             ###---------------------###
295             # read/write accessors
296              
297 143 100   143 1 281 sub auth_data { (@_ == 2) ? $_[0]->{'auth_data'} = pop : $_[0]->{'auth_data'} }
298 82 100 100 82 1 856 sub base_dir_abs { (@_ == 2) ? $_[0]->{'base_dir_abs'} = pop : $_[0]->{'base_dir_abs'} || ['.'] }
299 19 100 100 19 1 78 sub base_dir_rel { (@_ == 2) ? $_[0]->{'base_dir_rel'} = pop : $_[0]->{'base_dir_rel'} || '' }
300 14 100 66 14 0 78 sub cgix { (@_ == 2) ? $_[0]->{'cgix'} = pop : $_[0]->{'cgix'} ||= do { require CGI::Ex; CGI::Ex->new } }
  1         706  
  1         6  
301 3 100 33 3 1 27 sub cookies { (@_ == 2) ? $_[0]->{'cookies'} = pop : $_[0]->{'cookies'} ||= $_[0]->cgix->get_cookies }
302 6 100 100 6 1 56 sub ext_conf { (@_ == 2) ? $_[0]->{'ext_conf'} = pop : $_[0]->{'ext_conf'} || 'pl' }
303 7 100 100 7 1 45 sub ext_print { (@_ == 2) ? $_[0]->{'ext_print'} = pop : $_[0]->{'ext_print'} || 'html' }
304 11 100 100 11 1 54 sub ext_val { (@_ == 2) ? $_[0]->{'ext_val'} = pop : $_[0]->{'ext_val'} || 'val' }
305 11 100 66 11 1 53 sub form { (@_ == 2) ? $_[0]->{'form'} = pop : $_[0]->{'form'} ||= $_[0]->cgix->get_form }
306 195 100   195 1 697 sub load_conf { (@_ == 2) ? $_[0]->{'load_conf'} = pop : $_[0]->{'load_conf'} }
307              
308             sub conf {
309 8     8 1 13 my $self = shift;
310 8 100       19 $self->{'conf'} = pop if @_ == 1;
311 8   66     39 return $self->{'conf'} ||= do {
312 4         8 my $conf = $self->conf_file;
313 4 100 33     31 $conf = $self->conf_obj->read($conf, {no_warn_on_fail => 1}) || ($self->conf_die_on_fail ? croak $@ : {})
314             if ! ref $conf;
315 3         11 my $hash = $self->conf_validation;
316 3 50 100     21 if ($hash && scalar keys %$hash) {
317 2         8 my $err_obj = $self->val_obj->validate($conf, $hash);
318 2 100       18 croak "$err_obj" if $err_obj;
319             }
320 2         22 $conf;
321             }
322             }
323              
324             sub conf_file {
325 10     10 1 16 my $self = shift;
326 10 100       22 $self->{'conf_file'} = pop if @_ == 1;
327 10   66     24 return $self->{'conf_file'} ||= do {
328 4   66     9 my $module = $self->name_module || croak 'Missing name_module during conf_file call';
329 3         22 $module .'.'. $self->ext_conf;
330             };
331             }
332              
333             ###---------------------###
334             # general methods
335              
336 2     2 0 15 sub add_to_base { my $self = shift; $self->add_to_hash($self->hash_base, @_) }
  2         4  
337 2     2 0 9 sub add_to_common { my $self = shift; $self->add_to_hash($self->hash_common, @_) }
  2         5  
338 3     3 0 16 sub add_to_errors { shift->add_errors(@_) }
339 2     2 0 11 sub add_to_fill { my $self = shift; $self->add_to_hash($self->hash_fill, @_) }
  2         6  
340 2     2 0 15 sub add_to_form { my $self = shift; $self->add_to_hash($self->hash_form, @_) }
  2         5  
341 1     1 0 21 sub add_to_path { shift->append_path(@_) } # legacy
342 2     2 0 14 sub add_to_swap { my $self = shift; $self->add_to_hash($self->hash_swap, @_) }
  2         5  
343 7     7 1 31 sub append_path { my $self = shift; push @{ $self->path }, @_ }
  7         64  
  7         15  
344 3     3 1 5 sub cleanup_user { my ($self, $user) = @_; $user }
  3         28  
345 10   100 10 1 42 sub current_step { $_[0]->step_by_path_index($_[0]->{'path_i'} || 0) }
346       76 1   sub destroy {}
347 2     2 1 12 sub first_step { $_[0]->step_by_path_index(0) }
348       12 0   sub fixup_after_morph {}
349       10 0   sub fixup_before_unmorph {}
350 8     8 0 12 sub format_error { my ($self, $error) = @_; $error }
  8         17  
351 1     1 1 3 sub get_pass_by_user { croak "get_pass_by_user is a virtual method and needs to be overridden for authentication to work" }
352 1     1 0 3 sub has_errors { scalar keys %{ $_[0]->hash_errors } }
  1         3  
353 2     2 1 4 sub last_step { $_[0]->step_by_path_index($#{ $_[0]->path }) }
  2         5  
354       62 1   sub path_info_map {}
355 56     56 1 94 sub post_loop { 0 } # true value means to abort the nav_loop - don't recurse
356       74 1   sub post_navigate {}
357 148     148 1 256 sub pre_loop { 0 } # true value means to abort the nav_loop routine
358 73     73 1 196 sub pre_navigate { 0 } # true means to not enter nav_loop
359 3   100 3 1 29 sub previous_step { $_[0]->step_by_path_index(($_[0]->{'path_i'} || 0) - 1) }
360       36 1   sub valid_steps {}
361 3     3 1 8 sub verify_user { 1 }
362              
363             sub add_errors {
364 7     7 0 13 my $self = shift;
365 7         10 my $hash = $self->hash_errors;
366 7 100       16 my $args = ref($_[0]) ? shift : {@_};
367 7         15 foreach my $key (keys %$args) {
368 7 100       19 my $_key = ($key =~ /error$/) ? $key : "${key}_error";
369 7 100       12 if ($hash->{$_key}) {
370 1         3 $hash->{$_key} .= '
' . $args->{$key};
371             } else {
372 6         16 $hash->{$_key} = $args->{$key};
373             }
374             }
375 7         13 $hash->{'has_errors'} = 1;
376             }
377              
378             sub add_to_hash {
379 10     10 0 14 my $self = shift;
380 10         10 my $old = shift;
381 10 100       20 my $new = ref($_[0]) ? shift : {@_};
382 10         33 @$old{keys %$new} = values %$new;
383             }
384              
385             sub clear_app {
386 1     1 1 2 my $self = shift;
387 1         7 delete @$self{qw(cgix cookies form hash_common hash_errors hash_fill hash_swap history
388             _morph_lineage _morph_lineage_start_index path path_i stash val_obj)};
389 1         5 return $self;
390             }
391              
392             sub dump_history {
393 3     3 1 13 my ($self, $all) = @_;
394 3         7 my $hist = $self->history;
395 3         32 my $dump = [sprintf "Elapsed: %.5f", time - $self->{'_time'}];
396              
397 3         6 foreach my $row (@$hist) {
398 33 100 100     134 if (! ref($row) || ref($row) ne 'HASH' || ! exists $row->{'elapsed'}) {
      100        
399 9         10 push @$dump, $row;
400 9         12 next;
401             }
402             my $note = (' ' x ($row->{'level'} || 0))
403 24   50     117 . join(' - ', $row->{'step'}, $row->{'meth'}, $row->{'found'}, sprintf '%.5f', $row->{'elapsed'});
404 24         30 my $resp = $row->{'response'};
405 24 100       30 if ($all) {
406 16         40 $note = [$note, $resp];
407             } else {
408 8 100 100     61 $note .= ' - '
    100 100        
    100          
    100          
409             .(! defined $resp ? 'undef'
410             : ref($resp) eq 'ARRAY' && !@$resp ? '[]'
411             : ref($resp) eq 'HASH' && !scalar keys %$resp ? '{}'
412             : $resp =~ /^(.{30}|.{0,30}(?=\n))(?s:.)/ ? "$1..." : $resp);
413 8 50       16 $note .= ' - '.$row->{'info'} if defined $row->{'info'};
414             }
415 24         45 push @$dump, $note;
416             }
417              
418 3         19 return $dump;
419             }
420              
421             sub exit_nav_loop {
422 14     14 1 31 my $self = shift;
423 14 100       28 if (my $ref = $self->{'_morph_lineage'}) { # undo morphs
424 2         3 my $index = $self->{'_morph_lineage_start_index'}; # allow for early "morphers" to only get rolled back so far
425 2 100       5 $index = -1 if ! defined $index;
426 2         5 $self->unmorph while $#$ref != $index;
427             }
428 14         185 die "Long Jump\n";
429             }
430              
431             sub insert_path {
432 57     57 1 79 my $self = shift;
433 57         82 my $ref = $self->path;
434 57   100     122 my $i = $self->{'path_i'} || 0;
435 57 100       113 if ($i + 1 > $#$ref) { push @$ref, @_ }
  56         110  
436 1         4 else { splice(@$ref, $i + 1, 0, @_) } # insert a path at the current location
437             }
438              
439 9     9 1 101 sub jump { shift->goto_step(@_) }
440              
441             sub goto_step {
442 20     20 1 46 my $self = shift;
443 20 50       34 my $i = @_ == 1 ? shift : 1;
444 20         34 my $path = $self->path;
445 20   100     47 my $path_i = $self->{'path_i'} || 0;
446              
447 20 100       103 if ( $i eq 'FIRST' ) { $i = - $path_i - 1 }
  2 100       10  
    100          
    100          
    100          
    100          
448 1         3 elsif ($i eq 'LAST' ) { $i = $#$path - $path_i }
449 1         3 elsif ($i eq 'NEXT' ) { $i = 1 }
450 1         5 elsif ($i eq 'CURRENT' ) { $i = 0 }
451 1         2 elsif ($i eq 'PREVIOUS') { $i = -1 }
452             elsif ($i !~ /^-?\d+/) { # look for a step by that name in the current remaining path
453 11         12 my $found;
454 11         22 for (my $j = $path_i; $j < @$path; $j++) {
455 16 100       35 if ($path->[$j] eq $i) {
456 1         2 $i = $j - $path_i;
457 1         3 $found = 1;
458 1         2 last;
459             }
460             }
461 11 100       30 if (! $found) {
462 10         61 $self->replace_path($i);
463 10         18 $i = $#$path;
464             }
465             }
466 20 50       82 croak "Invalid jump index ($i)" if $i !~ /^-?\d+$/;
467              
468 20         23 my $cut_i = $path_i + $i; # manipulate the path to contain the new jump location
469 20 100       110 my @replace = ($cut_i > $#$path) ? $self->default_step
    100          
470             : ($cut_i < 0) ? @$path
471             : @$path[$cut_i .. $#$path];
472 20         61 $self->replace_path(@replace);
473              
474 20   100     54 $self->{'jumps'} = ($self->{'jumps'} || 0) + 1;
475 20         25 $self->{'path_i'}++; # move along now that the path is updated
476              
477 20   100     56 my $lin = $self->{'_morph_lineage'} || [];
478 20 100       39 $self->unmorph if @$lin;
479 20         70 $self->nav_loop; # recurse on the path
480 12         64 $self->exit_nav_loop;
481             }
482              
483             sub js_uri_path {
484 13     13 1 26 my $self = shift;
485 13         17 my $script = $self->script_name;
486 13         35 my $js_step = $self->js_step;
487 13 50 33     105 return ($self->can('path') == \&CGI::Ex::App::path
488             && $self->can('path_info_map_base') == \&CGI::Ex::App::path_info_map_base)
489             ? $script .'/'. $js_step # try to use a cache friendly URI (if path is our own)
490             : $script .'?'. $self->step_key .'='. $js_step .'&js='; # use one that works with more paths
491             }
492              
493              
494             sub morph {
495 133     133 1 517 my $self = shift;
496 133         313 my $ref = $self->history->[-1];
497 133 100 66     428 if (! $ref || ! $ref->{'meth'} || $ref->{'meth'} ne 'morph') {
      100        
498 115         124 push @{ $self->history }, ($ref = {meth => 'morph', found => 'morph', elapsed => 0, step => 'unknown', level => $self->{'_level'}});
  115         152  
499             }
500 133   100     253 my $step = shift || return;
501 132   100     315 my $allow = shift || $self->run_hook('allow_morph', $step) || return;
502 21         52 my $new = shift; # optionally allow passing in the package to morph to
503 21   100     55 my $lin = $self->{'_morph_lineage'} ||= [];
504 21         33 my $ok = 0;
505 21         28 my $cur = ref $self;
506              
507 21         28 push @$lin, $cur; # store so subsequent unmorph calls can do the right thing
508              
509             # hash - but no step - record for unbless
510 21 100 100     68 if (ref($allow) && ! ($allow = $allow->{$step})) {
    50 66        
    100          
511 1         2 $ref->{'info'} = "not allowed to morph to that step";
512              
513             } elsif (! ($new ||= $self->run_hook('morph_package', $step))) {
514 0         0 $ref->{'info'} = "Missing morph_package for step $step";
515              
516             } elsif ($cur eq $new) {
517 2         10 $ref->{'info'} = "already isa $new";
518 2         2 $ok = 1;
519              
520             ### if we are not already that package - bless us there
521             } else {
522 18         66 (my $file = "$new.pm") =~ s|::|/|g;
523 18 100 66     122 if (UNIVERSAL::can($new, 'fixup_after_morph') # check if the package space exists
    100 66        
    50          
524 6         742 || (eval { require $file } # check for a file that holds this package
525             && UNIVERSAL::can($new, 'fixup_after_morph'))) {
526 12         20 bless $self, $new; # become that package
527 12         22 $self->fixup_after_morph($step);
528 12         27 $ref->{'info'} = "changed $cur to $new";
529             } elsif ($@) {
530 5 100 66     37 if ($allow eq '1' && $@ =~ /^\s*(Can\'t locate \S+ in \@INC)/) { # let us know what happened
531 4         18 $ref->{'info'} = "failed from $cur to $new: $1";
532             } else {
533 1         6 $ref->{'info'} = "failed from $cur to $new: $@";
534 1         8 die "Trouble while morphing from $cur to $new: $@";
535             }
536             } elsif ($allow ne '1') {
537 1         4 $ref->{'info'} = "package $new doesn't support CGI::Ex::App API";
538 1         9 die "Found package $new, but $new does not support CGI::Ex::App API";
539             }
540 16         38 $ok = 1;
541             }
542              
543 19         40 return $ok;
544             }
545              
546             sub replace_path {
547 31     31 1 42 my $self = shift;
548 31         39 my $ref = $self->path;
549 31   100     83 my $i = $self->{'path_i'} || 0;
550 31 100       61 if ($i + 1 > $#$ref) { push @$ref, @_; }
  13         21  
551 18         60 else { splice(@$ref, $i + 1, $#$ref - $i, @_); } # replace remaining entries
552             }
553              
554             sub set_path {
555 3     3 1 601 my $self = shift;
556 3   100     13 my $path = $self->{'path'} ||= [];
557 3 100       9 croak "Cannot call set_path after the navigation loop has begun" if $self->{'path_i'};
558 2         7 splice @$path, 0, $#$path + 1, @_; # change entries in the ref (which updates other copies of the ref)
559             }
560              
561             sub step_by_path_index {
562 45     45 0 58 my $self = shift;
563 45   100     77 my $i = shift || 0;
564 45         66 my $ref = $self->path;
565 43 100       101 return '' if $i < 0;
566 42         157 return $ref->[$i];
567             }
568              
569             sub unmorph {
570 116     116 1 136 my $self = shift;
571 116   100     198 my $step = shift || '_no_step';
572 116   50     227 my $ref = $self->history->[-1] || {};
573 116 100 33     435 if (! $ref || ! $ref->{'meth'} || $ref->{'meth'} ne 'unmorph') {
      66        
574 114         130 push @{ $self->history }, ($ref = {meth => 'unmorph', found => 'unmorph', elapsed => 0, step => $step, level => $self->{'_level'}});
  114         153  
575             }
576 116   100     333 my $lin = $self->{'_morph_lineage'} || return;
577 19         26 my $cur = ref $self;
578 19   33     33 my $prev = pop(@$lin) || croak "unmorph called more times than morph (current: $cur)";
579 19 100       41 delete $self->{'_morph_lineage'} if ! @$lin;
580              
581 19 100       34 if ($cur ne $prev) {
582 10         41 $self->fixup_before_unmorph($step);
583 10         12 bless $self, $prev;
584 10         23 $ref->{'info'} = "changed from $cur to $prev";
585             } else {
586 9         17 $ref->{'info'} = "already isa $cur";
587             }
588              
589 19         37 return 1;
590             }
591              
592             ###---------------------###
593             # hooks
594              
595             sub file_print {
596 7     7 1 13 my ($self, $step) = @_;
597 7         17 my $base_dir = $self->base_dir_rel;
598 7         16 my $module = $self->run_hook('name_module', $step);
599 7   66     17 my $_step = $self->run_hook('name_step', $step) || croak "Missing name_step";
600 6         35 $_step =~ s|\B__+|/|g;
601 6 100       20 $_step .= '.'. $self->ext_print if $_step !~ /\.\w+$/;
602 6 100 66     13 foreach ($base_dir, $module) { $_ .= '/' if length($_) && ! m|/$| }
  12         47  
603 6         43 return $base_dir . $module . $_step;
604             }
605              
606             sub file_val {
607 10     10 1 20 my ($self, $step) = @_;
608              
609 10   100     17 my $abs = $self->val_path || [];
610 10 100       42 $abs = $abs->() if UNIVERSAL::isa($abs, 'CODE');
611 10 100       31 $abs = [$abs] if ! UNIVERSAL::isa($abs, 'ARRAY');
612 10 100       21 return {} if @$abs == 0;
613              
614 9         16 my $base_dir = $self->base_dir_rel;
615 9         17 my $module = $self->run_hook('name_module', $step);
616 9   66     23 my $_step = $self->run_hook('name_step', $step) || croak "Missing name_step";
617 8         37 $_step =~ s|\B__+|/|g;
618 8         18 $_step =~ s/\.\w+$//;
619 8         18 $_step .= '.'. $self->ext_val;
620              
621 8 100 100     17 foreach (@$abs, $base_dir, $module) { $_ .= '/' if length($_) && ! m|/$| }
  25         71  
622              
623 8 100       16 if (@$abs > 1) {
624 1         3 foreach my $_abs (@$abs) {
625 2         8 my $path = "$_abs/$base_dir/$module/$_step";
626 2 50       38 return $path if -e $path;
627             }
628             }
629 8         59 return $abs->[0] . $base_dir . $module . $_step;
630             }
631              
632             sub fill_template {
633 72     72 1 127 my ($self, $step, $outref, $fill) = @_;
634 72 100 66     269 return if ! $fill || ! scalar keys %$fill;
635 71   50     127 my $args = $self->run_hook('fill_args', $step) || {};
636 71         192 local @$args{'text', 'form'} = ($outref, $fill);
637 71         1113 require CGI::Ex::Fill;
638 71         185 CGI::Ex::Fill::fill($args);
639             }
640              
641 25     25 1 56 sub finalize { 1 } # false means show step
642              
643             sub hash_base {
644 78     78 1 132 my ($self, $step) = @_;
645 78   100     250 my $hash = $self->{'hash_base'} ||= {
646             script_name => $self->script_name,
647             path_info => $self->path_info,
648             };
649              
650 78         146 my $copy = $self; eval { require Scalar::Util; Scalar::Util::weaken($copy) };
  78         103  
  78         334  
  78         226  
651 78     1   295 $hash->{'js_validation'} = sub { $copy->run_hook('js_validation', $step, shift) };
  1         729  
652 78 0   0   206 $hash->{'generate_form'} = sub { $copy->run_hook('generate_form', $step, (ref($_[0]) ? (undef, shift) : shift)) };
  0         0  
653 78         166 $hash->{'form_name'} = $self->run_hook('form_name', $step);
654 78         163 $hash->{$self->step_key} = $step;
655 78         250 return $hash;
656             }
657              
658 61   100 61 1 276 sub hash_common { $_[0]->{'hash_common'} ||= {} }
659 82   100 82 1 277 sub hash_errors { $_[0]->{'hash_errors'} ||= {} }
660 72   100 72 1 262 sub hash_fill { $_[0]->{'hash_fill'} ||= {} }
661 76     76 1 170 sub hash_form { $_[0]->form }
662 72   100 72 1 249 sub hash_swap { $_[0]->{'hash_swap'} ||= {} }
663              
664             sub hash_validation {
665 2     2 1 6 my ($self, $step) = @_;
666 2   66     8 return $self->{'hash_validation'}->{$step} ||= do {
667 1         3 my $file = $self->run_hook('file_val', $step);
668 1 50       9 $file ? $self->val_obj->get_validation($file) : {}; # if the file is not found, errors will be in the webserver logs (all else dies)
669             };
670             }
671              
672             sub info_complete {
673 9     9 1 15 my ($self, $step) = @_;
674 9 100       14 return 0 if ! $self->run_hook('ready_validate', $step);
675 8 100       23 return $self->run_hook('validate', $step, $self->form) ? 1 : 0;
676             }
677              
678             sub js_validation {
679 6     6 1 12 my ($self, $step) = @_;
680 6   100     28 my $form_name = $_[2] || $self->run_hook('form_name', $step);
681 6   100     23 my $hash_val = $_[3] || $self->run_hook('hash_validation', $step);
682 6 100 100     41 return '' if ! $form_name || ! ref($hash_val) || ! scalar keys %$hash_val;
      100        
683 2         11 return $self->val_obj->generate_js($hash_val, $form_name, $self->js_uri_path);
684             }
685              
686             sub generate_form {
687 0     0 0 0 my ($self, $step) = @_;
688 0   0     0 my $form_name = $_[2] || $self->run_hook('form_name', $step);
689 0 0       0 my $args = ref($_[3]) eq 'HASH' ? $_[3] : {};
690 0         0 my $hash_val = $self->run_hook('hash_validation', $step);
691 0 0 0     0 return '' if ! $form_name || ! ref($hash_val) || ! scalar keys %$hash_val;
      0        
692 0         0 local $args->{'js_uri_path'} = $self->js_uri_path;
693 0         0 return $self->val_obj->generate_form($hash_val, $form_name, $args);
694             }
695              
696 20     20 0 30 sub morph_base { my $self = shift; ref($self) }
  20         31  
697             sub morph_package {
698 20     20 1 674 my ($self, $step) = @_;
699 20         40 my $cur = $self->morph_base; # default to using self as the base for morphed modules
700 20 50 66     63 my $new = ($cur ? $cur .'::' : '') . ($step || croak "Missing step");
701 19         43 $new =~ s/\B__+/::/g; # turn Foo::my_nested__step info Foo::my_nested::step
702 19         187 $new =~ s/(?:_+|\b)(\w)/\u$1/g; # turn Foo::my_step_name into Foo::MyStepName
703 19         93 return $new;
704             }
705              
706             sub name_module {
707 21     21 1 47 my ($self, $step) = @_;
708 21 100 100     61 return $self->{'name_module'} ||= ($self->script_name =~ m/ (\w+) (?:\.\w+)? $/x)
      66        
709             ? $1 : die "Could not determine module name from \"name_module\" lookup (".($step||'').")\n";
710             }
711              
712 10     10 1 21 sub name_step { my ($self, $step) = @_; $step }
  10         22  
713 28   100 28 1 77 sub next_step { $_[0]->step_by_path_index(($_[0]->{'path_i'} || 0) + 1) }
714 72     72 1 92 sub post_print { 0 }
715 27     27 1 46 sub post_step { 0 } # true indicates we handled step (exit loop)
716 109     109 1 218 sub pre_step { 0 } # true indicates we handled step (exit loop)
717 100     100 1 239 sub prepare { 1 } # false means show step
718              
719             sub print_out {
720 4     4 1 8 my ($self, $step, $out) = @_;
721 4         10 $self->cgix->print_content_type($self->run_hook('mimetype', $step), $self->run_hook('charset', $step));
722 4 100       420 print ref($out) eq 'SCALAR' ? $$out : $out;
723             }
724              
725             sub ready_validate {
726 12     12 1 24 my ($self, $step) = @_;
727 12 50 33     22 if ($self->run_hook('validate_when_data', $step)
728 0 0       0 and my @keys = keys %{ $self->run_hook('hash_validation', $step) || {} }) {
729 0         0 my $form = $self->form;
730 0 0       0 return (grep { exists $form->{$_} } @keys) ? 1 : 0;
  0         0  
731             }
732 12 100 66     72 return ($ENV{'REQUEST_METHOD'} && $ENV{'REQUEST_METHOD'} eq 'POST') ? 1 : 0;
733             }
734              
735             sub refine_path {
736 36     36 1 56 my ($self, $step, $is_at_end) = @_;
737 36 100       82 return 0 if ! $is_at_end; # if we are not at the end of the path, do not do anything
738 27   100     41 my $next_step = $self->run_hook('next_step', $step) || return 0;
739 1         9 $self->run_hook('set_ready_validate', $step, 0);
740 1         4 $self->append_path($next_step);
741 1         2 return 1;
742             }
743              
744             sub set_ready_validate {
745 5     5 1 33 my $self = shift;
746 5 100       13 my ($step, $is_ready) = (@_ == 2) ? @_ : (undef, shift); # hook and method
747 5 100       21 $ENV{'REQUEST_METHOD'} = ($is_ready) ? 'POST' : 'GET';
748 5         9 return $is_ready;
749             }
750              
751 101     101 1 176 sub skip { 0 } # success indicates to skip the step (and continue loop)
752              
753             sub swap_template {
754 72     72 1 409 my ($self, $step, $file, $swap) = @_;
755 72         171 my $t = $self->__template_obj($step);
756 72         1125 my $out = '';
757 72 50       203 $t->process($file, $swap, \$out) || die $t->error;
758 72         60059 return $out;
759             }
760              
761             sub __template_obj {
762 72     72   94 my ($self, $step) = @_;
763 72   50     106 my $args = $self->run_hook('template_args', $step) || {};
764 72   33     337 $args->{'INCLUDE_PATH'} ||= $args->{'include_path'} || $self->template_path;
      33        
765 72         179 return $self->template_obj($args);
766             }
767              
768             sub validate {
769 8     8 1 11 my ($self, $step, $form) = @_;
770 8         16 my $hash = $self->__hash_validation($step);
771 8 100 66     68 return 1 if ! ref($hash) || ! scalar keys %$hash;
772              
773 7         8 my @validated_fields;
774 7 100       7 if (my $err_obj = eval { $self->val_obj->validate($form, $hash, \@validated_fields) }) {
  7         22  
775 4         19 $self->add_errors($err_obj->as_hash({as_hash_join => "
\n", as_hash_suffix => '_error'}));
776 4         44 return 0;
777             }
778 3 50       10 die "Step $step: $@" if $@;
779              
780 3         6 foreach my $ref (@validated_fields) { # allow for the validation to give us some redirection
781 3 0       4 $self->append_path( ref $_ ? @$_ : $_) if $_ = $ref->{'append_path'};
    50          
782 3 0       7 $self->replace_path(ref $_ ? @$_ : $_) if $_ = $ref->{'replace_path'};
    50          
783 3 0       4 $self->insert_path( ref $_ ? @$_ : $_) if $_ = $ref->{'insert_path'};
    50          
784             }
785              
786 3         22 return 1;
787             }
788              
789 8     8   27 sub __hash_validation { shift->run_hook('hash_validation', @_) }
790              
791 12     12 1 26 sub validate_when_data { $_[0]->{'validate_when_data'} }
792              
793             ###---------------------###
794             # authentication
795              
796             sub navigate_authenticated {
797 3     3 1 7 my ($self, $args) = @_;
798 3 50       7 $self = $self->new($args) if ! ref $self;
799 3 100       20 croak "Cannot call navigate_authenticated method if default require_auth method is overwritten"
800             if $self->can('require_auth') != \&CGI::Ex::App::require_auth;
801 2         7 $self->require_auth(1);
802 2         6 return $self->navigate;
803             }
804              
805             sub require_auth {
806 116     116 1 145 my $self = shift;
807 116 50 66     217 $self->{'require_auth'} = shift if @_ == 1 && (! defined($_[0]) || ref($_[0]) || $_[0] =~ /^[01]$/);
      100        
808 116   100     323 return $self->{'require_auth'} || 0;
809             }
810              
811 138 100   138 1 280 sub is_authed { my $data = shift->auth_data; $data && ! $data->{'error'} }
  138         349  
812              
813 4     1 0 24 sub check_valid_auth { shift->_do_auth({login_print => sub {}, location_bounce => sub {}}) }
        4      
814              
815             sub get_valid_auth {
816 8     8 1 9 my $self = shift;
817             return $self->_do_auth({
818             login_print => sub { # use CGI::Ex::Auth - but use our formatting and printing
819 7     7   13 my ($auth, $template, $hash) = @_;
820 7         13 local $self->{'__login_file_print'} = $template;
821 7         11 local $self->{'__login_hash_common'} = $hash;
822 7         37 return $self->goto_step($self->login_step);
823             }
824 8         46 });
825             }
826              
827             sub _do_auth {
828 12     12   19 my ($self, $extra) = @_;
829 12 100       18 return $self->auth_data if $self->is_authed;
830 11 50       16 my $args = { %{ $self->auth_args || {} }, %{ $extra || {} } };
  11 50       27  
  11         123  
831 11   33     67 $args->{'script_name'} ||= $self->script_name;
832 11   33     38 $args->{'path_info'} ||= $self->path_info;
833 11   33     49 $args->{'cgix'} ||= $self->cgix;
834 11   33     87 $args->{'form'} ||= $self->form;
835 11   33     63 $args->{'cookies'} ||= $self->cookies;
836 11   33     67 $args->{'js_uri_path'} ||= $self->js_uri_path;
837 11   50 3   59 $args->{'get_pass_by_user'} ||= sub { my ($auth, $user) = @_; $self->get_pass_by_user($user, $auth) };
  3         5  
  3         7  
838 11   50 3   60 $args->{'verify_user'} ||= sub { my ($auth, $user) = @_; $self->verify_user( $user, $auth) };
  3         4  
  3         14  
839 11   50 3   67 $args->{'cleanup_user'} ||= sub { my ($auth, $user) = @_; $self->cleanup_user( $user, $auth) };
  3         6  
  3         11  
840              
841 11         34 my $obj = $self->auth_obj($args);
842 11         27 my $resp = $obj->get_valid_auth;
843 4         10 my $data = $obj->last_auth_data;
844 4 100       11 delete $data->{'real_pass'} if defined $data; # data may be defined but false
845 4         11 $self->auth_data($data); # failed authentication may still have auth_data
846 4 100 66     20 return ($resp && $data) ? $data : undef;
847             }
848              
849             ###---------------------###
850             # default steps
851              
852 1     1 0 3 sub js_require_auth { 0 }
853             sub js_run_step { # step that allows for printing javascript libraries that are stored in perls @INC.
854 3     3 0 5 my $self = shift;
855 3   100     6 my $path = $self->form->{'js'} || $self->path_info;
856 3 100       16 $self->cgix->print_js($path =~ m!^(?:/js/|/)?(\w+(?:/\w+)*\.js)$! ? $1 : '');
857 3         20 $self->{'_no_post_navigate'} = 1;
858 3         4 return 1;
859             }
860              
861 3     3   6 sub __forbidden_require_auth { 0 }
862 3 50   3   7 sub __forbidden_allow_morph { shift->allow_morph(@_) && 1 }
863 3     3   8 sub __forbidden_info_complete { 0 } # step that will be used the path method determines it is forbidden
864 3     3   7 sub __forbidden_hash_common { shift->stash }
865 3     3   5 sub __forbidden_file_print { \ "

Denied

You do not have access to the step \"[% forbidden_step.html %]\"" }
866              
867 4 50   4   7 sub __error_allow_morph { shift->allow_morph(@_) && 1 }
868 6     6   48 sub __error_info_complete { 0 } # step that is used by the default handle_error
869 6     6   12 sub __error_hash_common { shift->stash }
870 4     4   6 sub __error_file_print { \ "

A fatal error occurred

Step: \"[% error_step.html %]\"
[% TRY; CONFIG DUMP => {header => 0}; DUMP error; END %]" }
871              
872 7     7   14 sub __login_require_auth { 0 }
873 7 50   7   19 sub __login_allow_morph { shift->allow_morph(@_) && 1 }
874 7     7   19 sub __login_info_complete { 0 } # step used by default authentication
875 7 50   7   23 sub __login_hash_common { shift->{'__login_hash_common'} || {error => "hash_common not set during default __login"} }
876 7 50   7   18 sub __login_file_print { shift->{'__login_file_print'} || \ "file_print not set during default __login
[% login_error %]" }
877              
878             1; # Full documentation resides in CGI/Ex/App.pod