File Coverage

blib/lib/HTML/FormFu/MultiForm.pm
Criterion Covered Total %
statement 200 243 82.3
branch 42 70 60.0
condition 16 30 53.3
subroutine 22 26 84.6
pod 0 3 0.0
total 280 372 75.2


line stmt bran cond sub pod time code
1             package HTML::FormFu::MultiForm;
2              
3             # ABSTRACT: Handle multi-page/stage forms with FormFu
4              
5 23     23   2101114 use strict;
  23         76  
  23         2117  
6              
7             our $VERSION = '1.03'; # VERSION
8             our $AUTHORITY = 'cpan:NIGELM'; # AUTHORITY
9              
10              
11 23     23   14424 use Moose;
  23         13104770  
  23         198  
12 23     23   226786 use MooseX::Attribute::Chained;
  23         363527  
  23         1670  
13              
14             with
15             'HTML::FormFu::Role::FormAndElementMethods' => { -excludes => 'model_config' },
16             'HTML::FormFu::Role::FormBlockAndFieldMethods',
17             'HTML::FormFu::Role::NestedHashUtils',
18             'HTML::FormFu::Role::Populate';
19              
20 23     23   19958 use HTML::FormFu;
  23         17901813  
  23         1287  
21 23         2296 use HTML::FormFu::Attribute qw(
22             mk_attrs
23             mk_attr_accessors
24             mk_inherited_accessors
25             mk_inherited_merging_accessors
26             mk_output_accessors
27 23     23   241 );
  23         62  
28 23         1954 use HTML::FormFu::ObjectUtil qw(
29             form
30             clone stash
31             parent
32             load_config_file load_config_filestem
33             _string_equals _object_equals
34 23     23   178 );
  23         57  
35 23     23   15573 use HTML::FormFu::QueryType::CGI;
  23         2601243  
  23         1451  
36              
37 23     23   234 use Carp qw( croak );
  23         56  
  23         1796  
38 23     23   159 use Clone ();
  23         54  
  23         432  
39 23     23   16275 use Crypt::CBC;
  23         106477  
  23         1153  
40 23     23   283 use List::MoreUtils qw( uniq );
  23         67  
  23         431  
41 23     23   13391 use Scalar::Util qw( blessed refaddr );
  23         63  
  23         1693  
42 23     23   155 use Storable qw( nfreeze thaw );
  23         55  
  23         2150  
43              
44             use overload (
45             'eq' => '_string_equals',
46             '==' => '_object_equals',
47 12     12   39840 '""' => sub { return shift->render },
48 0     0   0 bool => sub {1},
49 23         343 fallback => 1
50 23     23   163 );
  23         57  
51              
52             __PACKAGE__->mk_attr_accessors(qw( id action enctype method ));
53              
54             # accessors shared with HTML::FormFu
55             our @ACCESSORS = qw(
56             default_args
57             model_config auto_fieldset
58             );
59              
60             for my $name (@ACCESSORS) {
61             has $name => ( is => 'rw', traits => ['Chained'] );
62             }
63              
64             for my $name (@HTML::FormFu::MULTIFORM_SHARED) {
65             has $name => ( is => 'rw', traits => ['Chained'] );
66             }
67              
68             has forms => ( is => 'rw', traits => ['Chained'] );
69             has query => ( is => 'rw', traits => ['Chained'] );
70             has current_form_number => ( is => 'rw', traits => ['Chained'] );
71             has current_form => ( is => 'rw', traits => ['Chained'] );
72             has multiform_hidden_name => ( is => 'rw', traits => ['Chained'] );
73             has default_multiform_hidden_name => ( is => 'rw', traits => ['Chained'] );
74             has combine_params => ( is => 'rw', traits => ['Chained'] );
75             has complete => ( is => 'rw', traits => ['Chained'] );
76              
77             has crypt_args => (
78             is => 'rw',
79             isa => 'HashRef',
80             default => sub { +{} },
81             );
82              
83             has _data => ( is => 'rw' );
84              
85             __PACKAGE__->mk_output_accessors(qw( form_error_message ));
86              
87             our @SHARED_WITH_FORMFU = (
88             @ACCESSORS,
89             @HTML::FormFu::MULTIFORM_SHARED,
90             @HTML::FormFu::Role::FormAndElementMethods::MULTIFORM_SHARED,
91             @HTML::FormFu::Role::FormBlockAndFieldMethods::MULTIFORM_SHARED,
92             );
93              
94             *loc = \&localize;
95              
96             for my $name (
97             qw(
98             persist_stash
99             _file_fields
100             )
101             ) {
102             has $name => (
103             is => 'rw',
104             default => sub { [] },
105             lazy => 1,
106             isa => 'ArrayRef',
107             );
108             }
109              
110             has languages => (
111             is => 'rw',
112             default => sub { ['en'] },
113             lazy => 1,
114             isa => 'ArrayRef',
115             );
116              
117             sub BUILD {
118             my ( $self, $args ) = @_;
119              
120             my %defaults = (
121             tt_args => {},
122             model_config => {},
123             combine_params => 1,
124             default_multiform_hidden_name => '_multiform',
125             );
126              
127             $self->populate( \%defaults );
128              
129             return $self;
130             }
131              
132             sub process {
133 33     33 0 1478 my ( $self, $query ) = @_;
134              
135 33   66     328 $query ||= $self->query;
136              
137             # save it for further calls to process()
138 33 100       140 if ($query) {
139 28         1124 $self->query($query);
140             }
141              
142 33         1183 my $hidden_name = $self->multiform_hidden_name;
143              
144 33 100       153 if ( !defined $hidden_name ) {
145 9         288 $hidden_name = $self->default_multiform_hidden_name;
146             }
147              
148 33         90 my $input;
149              
150 33 50 66     670 if ( defined $query && blessed($query) ) {
    100          
151 0         0 $input = $query->param($hidden_name);
152             }
153             elsif ( defined $query ) {
154              
155             # it's not an object, just a hashref.
156             # and HTML::FormFu::FakeQuery doesn't work with a MultiForm object
157              
158 28         236 $input = $self->get_nested_hash_value( $query, $hidden_name );
159             }
160              
161 33         973 my $data = $self->_process_get_data($input);
162 33         99 my $current_form_num;
163             my @forms;
164              
165 33         93 eval { @forms = @{ $self->forms } };
  33         85  
  33         1160  
166 33 50       153 croak "forms() must be an arrayref" if $@;
167              
168 33 100       143 if ( defined $data ) {
169 12         36 $current_form_num = $data->{current_form};
170              
171 12         65 my $current_form = $self->_load_current_form( $current_form_num, $data );
172              
173             # are we on the last form?
174             # are we complete?
175              
176 12 100 66     110 if ( ( $current_form_num == scalar @forms )
177             && $current_form->submitted_and_valid ) {
178 4         5812 $self->complete(1);
179             }
180              
181 12         423 $self->_data($data);
182             }
183             else {
184              
185             # default to first form
186              
187 21         121 $self->_load_current_form(1);
188             }
189              
190 33         229 return;
191             }
192              
193             sub _process_get_data {
194 33     33   123 my ( $self, $input ) = @_;
195              
196 33 100 66     230 return if !defined $input || !length $input;
197              
198 12         36 my $crypt = Crypt::CBC->new( %{ $self->crypt_args } );
  12         405  
199              
200 12         2022 my $data;
201              
202 12         39 eval { $data = $crypt->decrypt_hex($input) };
  12         68  
203              
204 12 50       6362 if ( defined $data ) {
205 12         92 $data = thaw($data);
206              
207 12         1043 $self->_file_fields( $data->{file_fields} );
208              
209             # rebless all file uploads as basic CGI objects
210 12         26 for my $name ( @{ $data->{file_fields} } ) {
  12         51  
211 0         0 my $value = $self->get_nested_hash_value( $data->{params}, $name );
212              
213 0         0 _rebless_upload($value);
214             }
215             }
216             else {
217              
218             # TODO: should handle errors better
219 0         0 $data = undef;
220             }
221              
222 12         91 return $data;
223             }
224              
225             sub _rebless_upload {
226 0     0   0 my ($value) = @_;
227              
228 0 0       0 if ( ref $value eq 'ARRAY' ) {
    0          
229 0         0 for my $value (@$value) {
230 0         0 _rebless_upload($value);
231             }
232             }
233             elsif ( blessed($value) ) {
234 0         0 bless $value, 'HTML::FormFu::QueryType::CGI';
235             }
236              
237 0         0 return;
238             }
239              
240             sub _load_current_form {
241 33     33   128 my ( $self, $current_form_num, $data ) = @_;
242              
243 33         1204 my $current_form = HTML::FormFu->new;
244              
245 33         36115 my $current_data = Clone::clone( $self->forms->[ $current_form_num - 1 ] );
246              
247             # merge constructor args
248 33         167 for my $key (@SHARED_WITH_FORMFU) {
249 1518         5057114 my $value = $self->$key;
250              
251 1518 100       11070 if ( defined $value ) {
252 132         691 $current_form->$key($value);
253             }
254             }
255              
256             # copy attrs
257 33         183 my $attrs = $self->attrs;
258              
259 33         354 for my $key ( keys %$attrs ) {
260 33         236 $current_form->$key( $attrs->{$key} );
261             }
262              
263             # copy stash
264 33         736 my $stash = $self->stash;
265              
266 33         609 while ( my ( $key, $value ) = each %$stash ) {
267 0         0 $current_form->stash->{$key} = $value;
268             }
269              
270             # persist_stash
271 33 100       188 if ( defined $data ) {
272 12         44 for my $key ( @{ $self->persist_stash } ) {
  12         683  
273 0         0 $current_form->stash->{$key} = $data->{persist_stash}{$key};
274             }
275             }
276              
277             # build form
278 33         312 $current_form->populate($current_data);
279              
280             # add hidden field
281 33 100 100     24730029 if ( ( !defined $self->multiform_hidden_name ) && $current_form_num > 1 ) {
282 3         93 my $field = $current_form->element(
283             { type => 'Hidden',
284             name => $self->default_multiform_hidden_name,
285             }
286             );
287              
288 3         5603 $field->constraint( { type => 'Required', } );
289             }
290              
291 33         2875 $current_form->query( $self->query );
292 33         609 $current_form->process;
293              
294             # combine params
295 33 100 100     390713 if ( defined $data && $self->combine_params ) {
296              
297 9         67 my $params = $current_form->params;
298              
299 9         7849 for my $name ( @{ $data->{valid_names} } ) {
  9         47  
300              
301 25 100       2319 next if $self->nested_hash_key_exists( $params, $name );
302              
303 16         553 my $value = $self->get_nested_hash_value( $data->{params}, $name );
304              
305             # need to set upload object's parent manually
306             # for now, parent points to the form
307             # when formfu fixes this, this code will need updated
308 16         471 _reparent_upload( $value, $current_form );
309              
310 16         78 $current_form->add_valid( $name, $value );
311             }
312             }
313              
314 33         2963 $self->current_form_number($current_form_num);
315 33         1261 $self->current_form($current_form);
316              
317 33         367 return $current_form;
318             }
319              
320             sub _reparent_upload {
321 16     16   57 my ( $value, $form ) = @_;
322              
323 16 50 33     149 if ( ref $value eq 'ARRAY' ) {
    50          
324 0         0 for my $value (@$value) {
325 0         0 _reparent_upload( $value, $form );
326             }
327             }
328             elsif ( blessed($value) && $value->isa('HTML::FormFu::Upload') ) {
329 0         0 $value->parent($form);
330             }
331              
332 16         45 return;
333             }
334              
335             sub render {
336 12     12 0 45 my $self = shift;
337              
338 12         538 my $form = $self->current_form;
339              
340 12 50       72 croak "process() must be called before render()"
341             if !defined $form;
342              
343 12 50       469 if ( $self->complete ) {
344              
345             # why would you render if it's complete?
346             # anyway, just show the last form
347 0         0 return $form->render(@_);
348             }
349              
350 12 100       87 if ( $form->submitted_and_valid ) {
351              
352             # return the next form
353 8         12368 return $self->next_form->render(@_);
354             }
355              
356             # return the current form
357 4         201 return $form->render(@_);
358             }
359              
360             sub next_form {
361 25     25 0 77858 my ($self) = @_;
362              
363 25         1454 my $form = $self->current_form;
364              
365 25 50       131 croak "process() must be called before next_form()"
366             if !defined $form;
367              
368 25         938 my $current_form_num = $self->current_form_number;
369              
370             # is there a next form defined?
371 25 50       72 return if $current_form_num >= scalar @{ $self->forms };
  25         863  
372              
373 25         802 my $form_data = Clone::clone( $self->forms->[$current_form_num] );
374              
375 25         939 my $next_form = HTML::FormFu->new;
376              
377             # merge constructor args
378 25         23016 for my $key (@SHARED_WITH_FORMFU) {
379 1150         61247 my $value = $self->$key;
380              
381 1150 100       7765 if ( defined $value ) {
382 100         461 $next_form->$key($value);
383             }
384             }
385              
386             # copy attrs
387 25         127 my $attrs = $self->attrs;
388              
389 25         318 while ( my ( $key, $value ) = each %$attrs ) {
390 25         152 $next_form->$key($value);
391             }
392              
393             # copy stash
394 25         1340 my $current_form = $self->current_form;
395 25         158 my $current_stash = $current_form->stash;
396              
397 25         355 while ( my ( $key, $value ) = each %$current_stash ) {
398 0         0 $next_form->stash->{$key} = $value;
399             }
400              
401             # persist_stash
402 25         74 for my $key ( @{ $self->persist_stash } ) {
  25         905  
403 0         0 $next_form->stash->{$key} = $current_form->stash->{$key};
404             }
405              
406             # build the form
407 25         168 $next_form->populate($form_data);
408              
409             # add hidden field
410 25 100       3225921 if ( !defined $self->multiform_hidden_name ) {
411 7         283 my $field = $next_form->element(
412             { type => 'Hidden',
413             name => $self->default_multiform_hidden_name,
414             }
415             );
416              
417 7         1160404 $field->constraint( { type => 'Required', } );
418             }
419              
420 25         64336 $next_form->process;
421              
422             # encrypt params in hidden field
423 25         47168 $self->_save_hidden_data( $current_form_num, $next_form, $form );
424              
425 25         338 return $next_form;
426             }
427              
428             sub _save_hidden_data {
429 25     25   115 my ( $self, $current_form_num, $next_form, $form ) = @_;
430              
431 25         153 my @valid_names = $form->valid;
432 25         2952 my $hidden_name = $self->multiform_hidden_name;
433              
434 25 100       131 if ( !defined $hidden_name ) {
435 7         225 $hidden_name = $self->default_multiform_hidden_name;
436             }
437              
438             # don't include the hidden-field's name in valid_names
439 25         95 @valid_names = grep { $_ ne $hidden_name } @valid_names;
  70         257  
440              
441 25         73 my %params;
442 25 50       66 my @file_fields = @{ $self->_file_fields || [] };
  25         1099  
443              
444 25         99 for my $name (@valid_names) {
445 62         287 my $value = $form->param_value($name);
446              
447 62         10572 $self->set_nested_hash_value( \%params, $name, $value );
448              
449             # populate @file_field
450 62 50       1767 if ( ref $value ne 'ARRAY' ) {
451 62         205 $value = [$value];
452             }
453              
454 62         181 for my $value (@$value) {
455 62 50 33     378 if ( blessed($value) && $value->isa('HTML::FormFu::Upload') ) {
456 0         0 push @file_fields, $name;
457 0         0 last;
458             }
459             }
460             }
461              
462 25         184 @file_fields = sort uniq @file_fields;
463              
464 25         68 my $crypt = Crypt::CBC->new( %{ $self->crypt_args } );
  25         954  
465              
466 25         26887 my $data = {
467             current_form => $current_form_num + 1,
468             valid_names => \@valid_names,
469             params => \%params,
470             persist_stash => {},
471             file_fields => \@file_fields,
472             };
473              
474             # persist_stash
475 25         84 for my $key ( @{ $self->persist_stash } ) {
  25         1036  
476 0         0 $data->{persist_stash}{$key} = $form->stash->{$key};
477             }
478              
479             # save file_fields
480 25         928 $self->_file_fields( \@file_fields );
481              
482             # to freeze, we need to remove anything that might have a
483             # file handle or code block
484             # make sure we restore them, after freezing
485 25         937 my $current_form = $self->current_form;
486              
487 25         771 my $input = $current_form->input;
488 25         967 my $query = $current_form->query;
489 25         1009 my $processed_params = $current_form->_processed_params;
490 25         405 my $parent = $current_form->parent;
491 25         424 my $stash = $current_form->stash;
492              
493 25         995 $current_form->input( {} );
494 25         1007 $current_form->query( {} );
495 25         897 $current_form->_processed_params( {} );
496 25         483 $current_form->parent( {} );
497              
498             # empty the stash
499 25         367 %{ $current_form->stash } = ();
  25         102  
500              
501             # save a map of upload refaddrs to their parent
502 25         220 my %upload_parent;
503              
504 25         82 for my $name (@file_fields) {
505 0 0       0 next if !$self->nested_hash_key_exists( \%params, $name );
506              
507 0         0 my $value = $self->get_nested_hash_value( \%params, $name );
508              
509 0         0 _save_upload_parent( \%upload_parent, $value );
510             }
511              
512             # freeze
513 25         97 local $Storable::canonical = 1;
514 25         169 $data = nfreeze($data);
515              
516             # restore form
517 25         3295 $current_form->input($input);
518 25         1011 $current_form->query($query);
519 25         930 $current_form->_processed_params($processed_params);
520 25         974 $current_form->parent($parent);
521              
522 25         331 %{ $current_form->stash } = %$stash;
  25         104  
523              
524 25         241 for my $name (@file_fields) {
525 0 0       0 next if !$self->nested_hash_key_exists( \%params, $name );
526              
527 0         0 my $value = $self->get_nested_hash_value( \%params, $name );
528              
529 0         0 _restore_upload_parent( \%upload_parent, $value );
530             }
531              
532             # store data in hidden field
533 25         159 $data = $crypt->encrypt_hex($data);
534              
535 25         40028 my $hidden_field = $next_form->get_field( { nested_name => $hidden_name, } );
536              
537 25         33934 $hidden_field->default($data);
538              
539 25         1089 return;
540             }
541              
542             sub _save_upload_parent {
543 0     0     my ( $upload_parent, $value ) = @_;
544              
545 0 0 0       if ( ref $value eq 'ARRAY' ) {
    0          
546 0           for my $value (@$value) {
547 0           _save_upload_parent( $upload_parent, $value );
548             }
549             }
550             elsif ( blessed($value) && $value->isa('HTML::FormFu::Upload') ) {
551 0           my $refaddr = refaddr($value);
552              
553 0           $upload_parent->{$refaddr} = $value->parent;
554              
555 0           $value->parent(undef);
556             }
557              
558 0           return;
559             }
560              
561             sub _restore_upload_parent {
562 0     0     my ( $upload_parent, $value ) = @_;
563              
564 0 0 0       if ( ref $value eq 'ARRAY' ) {
    0          
565 0           for my $value (@$value) {
566 0           _restore_upload_parent( $upload_parent, $value );
567             }
568             }
569             elsif ( blessed($value) && $value->isa('HTML::FormFu::Upload') ) {
570 0           my $refaddr = refaddr($value);
571              
572 0           $value->parent( $upload_parent->{$refaddr} );
573             }
574              
575 0           return;
576             }
577              
578             __PACKAGE__->meta->make_immutable;
579              
580             1;
581              
582             __END__
583              
584             =pod
585              
586             =encoding UTF-8
587              
588             =head1 NAME
589              
590             HTML::FormFu::MultiForm - Handle multi-page/stage forms with FormFu
591              
592             =head1 VERSION
593              
594             version 1.03
595              
596             =head1 DESCRIPTION
597              
598             For now, see test files in L<Catalyst::Controller::HTML::FormFu> for examples.
599              
600             =head1 AUTHORS
601              
602             =over 4
603              
604             =item *
605              
606             Carl Franks <cpan@fireartist.com>
607              
608             =item *
609              
610             Nigel Metheringham <nigelm@cpan.org>
611              
612             =item *
613              
614             Dean Hamstead <dean@bytefoundry.com.au>
615              
616             =back
617              
618             =head1 COPYRIGHT AND LICENSE
619              
620             This software is copyright (c) 2013-2017 by Carl Franks / Nigel Metheringham / Dean Hamstead.
621              
622             This is free software; you can redistribute it and/or modify it under
623             the same terms as the Perl 5 programming language system itself.
624              
625             =for :stopwords cpan testmatrix url annocpan anno bugtracker rt cpants kwalitee diff irc mailto metadata placeholders metacpan
626              
627             =head1 SUPPORT
628              
629             =head2 Perldoc
630              
631             You can find documentation for this module with the perldoc command.
632              
633             perldoc HTML::FormFu::MultiForm
634              
635             =head2 Websites
636              
637             The following websites have more information about this module, and may be of help to you. As always,
638             in addition to those websites please use your favorite search engine to discover more resources.
639              
640             =over 4
641              
642             =item *
643              
644             MetaCPAN
645              
646             A modern, open-source CPAN search engine, useful to view POD in HTML format.
647              
648             L<http://metacpan.org/release/HTML-FormFu-MultiForm>
649              
650             =item *
651              
652             Search CPAN
653              
654             The default CPAN search engine, useful to view POD in HTML format.
655              
656             L<http://search.cpan.org/dist/HTML-FormFu-MultiForm>
657              
658             =item *
659              
660             RT: CPAN's Bug Tracker
661              
662             The RT ( Request Tracker ) website is the default bug/issue tracking system for CPAN.
663              
664             L<https://rt.cpan.org/Public/Dist/Display.html?Name=HTML-FormFu-MultiForm>
665              
666             =item *
667              
668             AnnoCPAN
669              
670             The AnnoCPAN is a website that allows community annotations of Perl module documentation.
671              
672             L<http://annocpan.org/dist/HTML-FormFu-MultiForm>
673              
674             =item *
675              
676             CPAN Ratings
677              
678             The CPAN Ratings is a website that allows community ratings and reviews of Perl modules.
679              
680             L<http://cpanratings.perl.org/d/HTML-FormFu-MultiForm>
681              
682             =item *
683              
684             CPAN Forum
685              
686             The CPAN Forum is a web forum for discussing Perl modules.
687              
688             L<http://cpanforum.com/dist/HTML-FormFu-MultiForm>
689              
690             =item *
691              
692             CPANTS
693              
694             The CPANTS is a website that analyzes the Kwalitee ( code metrics ) of a distribution.
695              
696             L<http://cpants.cpanauthors.org/dist/HTML-FormFu-MultiForm>
697              
698             =item *
699              
700             CPAN Testers
701              
702             The CPAN Testers is a network of smokers who run automated tests on uploaded CPAN distributions.
703              
704             L<http://www.cpantesters.org/distro/H/HTML-FormFu-MultiForm>
705              
706             =item *
707              
708             CPAN Testers Matrix
709              
710             The CPAN Testers Matrix is a website that provides a visual overview of the test results for a distribution on various Perls/platforms.
711              
712             L<http://matrix.cpantesters.org/?dist=HTML-FormFu-MultiForm>
713              
714             =item *
715              
716             CPAN Testers Dependencies
717              
718             The CPAN Testers Dependencies is a website that shows a chart of the test results of all dependencies for a distribution.
719              
720             L<http://deps.cpantesters.org/?module=HTML::FormFu::MultiForm>
721              
722             =back
723              
724             =head2 Bugs / Feature Requests
725              
726             Please report any bugs or feature requests by email to C<bug-html-formfu-multiform at rt.cpan.org>, or through
727             the web interface at L<https://rt.cpan.org/Public/Bug/Report.html?Queue=HTML-FormFu-MultiForm>. You will be automatically notified of any
728             progress on the request by the system.
729              
730             =head2 Source Code
731              
732             The code is open to the world, and available for you to hack on. Please feel free to browse it and play
733             with it, or whatever. If you want to contribute patches, please send me a diff or prod me to pull
734             from your repository :)
735              
736             L<https://github.com/FormFu/HTML-FormFu-MultiForm>
737              
738             git clone https://github.com/FormFu/HTML-FormFu-MultiForm.git
739              
740             =head1 CONTRIBUTOR
741              
742             =for stopwords fireartist
743              
744             fireartist <fireartist@gmail.com>
745              
746             =cut