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   1101990 use strict;
  23         50  
  23         1068  
6              
7             our $VERSION = '1.01'; # TRIAL VERSION
8             our $AUTHORITY = 'cpan:NIGELM'; # AUTHORITY
9              
10              
11 23     23   7100 use Moose;
  23         8433296  
  23         141  
12 23     23   150714 use MooseX::Attribute::Chained;
  23         239094  
  23         1041  
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   11294 use HTML::FormFu;
  23         11535624  
  23         1086  
21 23         1701 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   182 );
  23         42  
28 23         1380 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   138 );
  23         43  
35 23     23   8652 use HTML::FormFu::QueryType::CGI;
  23         1674924  
  23         1108  
36              
37 23     23   185 use Carp qw( croak );
  23         40  
  23         1316  
38 23     23   126 use Clone ();
  23         39  
  23         374  
39 23     23   10109 use Crypt::CBC;
  23         85507  
  23         738  
40 23     23   160 use List::MoreUtils qw( uniq );
  23         44  
  23         325  
41 23     23   10962 use Scalar::Util qw( blessed refaddr );
  23         45  
  23         1143  
42 23     23   126 use Storable qw( nfreeze thaw );
  23         42  
  23         1671  
43              
44             use overload (
45             'eq' => '_string_equals',
46             '==' => '_object_equals',
47 12     12   20814 '""' => sub { return shift->render },
48 0     0   0 bool => sub {1},
49 23         272 fallback => 1
50 23     23   125 );
  23         45  
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 954 my ( $self, $query ) = @_;
134              
135 33   66     234 $query ||= $self->query;
136              
137             # save it for further calls to process()
138 33 100       92 if ($query) {
139 28         766 $self->query($query);
140             }
141              
142 33         887 my $hidden_name = $self->multiform_hidden_name;
143              
144 33 100       106 if ( !defined $hidden_name ) {
145 9         250 $hidden_name = $self->default_multiform_hidden_name;
146             }
147              
148 33         57 my $input;
149              
150 33 50 66     273 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         162 $input = $self->get_nested_hash_value( $query, $hidden_name );
159             }
160              
161 33         723 my $data = $self->_process_get_data($input);
162 33         67 my $current_form_num;
163             my @forms;
164              
165 33         64 eval { @forms = @{ $self->forms } };
  33         60  
  33         840  
166 33 50       107 croak "forms() must be an arrayref" if $@;
167              
168 33 100       102 if ( defined $data ) {
169 12         29 $current_form_num = $data->{current_form};
170              
171 12         54 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     71 if ( ( $current_form_num == scalar @forms )
177             && $current_form->submitted_and_valid ) {
178 4         4061 $self->complete(1);
179             }
180              
181 12         321 $self->_data($data);
182             }
183             else {
184              
185             # default to first form
186              
187 21         82 $self->_load_current_form(1);
188             }
189              
190 33         196 return;
191             }
192              
193             sub _process_get_data {
194 33     33   89 my ( $self, $input ) = @_;
195              
196 33 100 66     162 return if !defined $input || !length $input;
197              
198 12         29 my $crypt = Crypt::CBC->new( %{ $self->crypt_args } );
  12         380  
199              
200 12         1298 my $data;
201              
202 12         23 eval { $data = $crypt->decrypt_hex($input) };
  12         43  
203              
204 12 50       4760 if ( defined $data ) {
205 12         63 $data = thaw($data);
206              
207 12         723 $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         56  
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         71 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   90 my ( $self, $current_form_num, $data ) = @_;
242              
243 33         859 my $current_form = HTML::FormFu->new;
244              
245 33         25398 my $current_data = Clone::clone( $self->forms->[ $current_form_num - 1 ] );
246              
247             # merge constructor args
248 33         128 for my $key (@SHARED_WITH_FORMFU) {
249 1518         3159066 my $value = $self->$key;
250              
251 1518 100       8062 if ( defined $value ) {
252 132         473 $current_form->$key($value);
253             }
254             }
255              
256             # copy attrs
257 33         127 my $attrs = $self->attrs;
258              
259 33         268 for my $key ( keys %$attrs ) {
260 33         178 $current_form->$key( $attrs->{$key} );
261             }
262              
263             # copy stash
264 33         508 my $stash = $self->stash;
265              
266 33         450 while ( my ( $key, $value ) = each %$stash ) {
267 0         0 $current_form->stash->{$key} = $value;
268             }
269              
270             # persist_stash
271 33 100       123 if ( defined $data ) {
272 12         29 for my $key ( @{ $self->persist_stash } ) {
  12         434  
273 0         0 $current_form->stash->{$key} = $data->{persist_stash}{$key};
274             }
275             }
276              
277             # build form
278 33         205 $current_form->populate($current_data);
279              
280             # add hidden field
281 33 100 100     16303529 if ( ( !defined $self->multiform_hidden_name ) && $current_form_num > 1 ) {
282 3         87 my $field = $current_form->element(
283             { type => 'Hidden',
284             name => $self->default_multiform_hidden_name,
285             }
286             );
287              
288 3         5856 $field->constraint( { type => 'Required', } );
289             }
290              
291 33         2325 $current_form->query( $self->query );
292 33         444 $current_form->process;
293              
294             # combine params
295 33 100 100     286151 if ( defined $data && $self->combine_params ) {
296              
297 9         45 my $params = $current_form->params;
298              
299 9         5177 for my $name ( @{ $data->{valid_names} } ) {
  9         35  
300              
301 25 100       1619 next if $self->nested_hash_key_exists( $params, $name );
302              
303 16         404 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         315 _reparent_upload( $value, $current_form );
309              
310 16         52 $current_form->add_valid( $name, $value );
311             }
312             }
313              
314 33         2049 $self->current_form_number($current_form_num);
315 33         895 $self->current_form($current_form);
316              
317 33         282 return $current_form;
318             }
319              
320             sub _reparent_upload {
321 16     16   39 my ( $value, $form ) = @_;
322              
323 16 50 33     101 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         31 return;
333             }
334              
335             sub render {
336 12     12 0 29 my $self = shift;
337              
338 12         394 my $form = $self->current_form;
339              
340 12 50       55 croak "process() must be called before render()"
341             if !defined $form;
342              
343 12 50       331 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       52 if ( $form->submitted_and_valid ) {
351              
352             # return the next form
353 8         7428 return $self->next_form->render(@_);
354             }
355              
356             # return the current form
357 4         123 return $form->render(@_);
358             }
359              
360             sub next_form {
361 25     25 0 51947 my ($self) = @_;
362              
363 25         776 my $form = $self->current_form;
364              
365 25 50       102 croak "process() must be called before next_form()"
366             if !defined $form;
367              
368 25         670 my $current_form_num = $self->current_form_number;
369              
370             # is there a next form defined?
371 25 50       53 return if $current_form_num >= scalar @{ $self->forms };
  25         676  
372              
373 25         595 my $form_data = Clone::clone( $self->forms->[$current_form_num] );
374              
375 25         661 my $next_form = HTML::FormFu->new;
376              
377             # merge constructor args
378 25         16800 for my $key (@SHARED_WITH_FORMFU) {
379 1150         46632 my $value = $self->$key;
380              
381 1150 100       5932 if ( defined $value ) {
382 100         340 $next_form->$key($value);
383             }
384             }
385              
386             # copy attrs
387 25         96 my $attrs = $self->attrs;
388              
389 25         230 while ( my ( $key, $value ) = each %$attrs ) {
390 25         116 $next_form->$key($value);
391             }
392              
393             # copy stash
394 25         1037 my $current_form = $self->current_form;
395 25         96 my $current_stash = $current_form->stash;
396              
397 25         252 while ( my ( $key, $value ) = each %$current_stash ) {
398 0         0 $next_form->stash->{$key} = $value;
399             }
400              
401             # persist_stash
402 25         54 for my $key ( @{ $self->persist_stash } ) {
  25         689  
403 0         0 $next_form->stash->{$key} = $current_form->stash->{$key};
404             }
405              
406             # build the form
407 25         108 $next_form->populate($form_data);
408              
409             # add hidden field
410 25 100       2178236 if ( !defined $self->multiform_hidden_name ) {
411 7         198 my $field = $next_form->element(
412             { type => 'Hidden',
413             name => $self->default_multiform_hidden_name,
414             }
415             );
416              
417 7         889967 $field->constraint( { type => 'Required', } );
418             }
419              
420 25         60596 $next_form->process;
421              
422             # encrypt params in hidden field
423 25         32605 $self->_save_hidden_data( $current_form_num, $next_form, $form );
424              
425 25         205 return $next_form;
426             }
427              
428             sub _save_hidden_data {
429 25     25   103 my ( $self, $current_form_num, $next_form, $form ) = @_;
430              
431 25         400 my @valid_names = $form->valid;
432 25         2086 my $hidden_name = $self->multiform_hidden_name;
433              
434 25 100       101 if ( !defined $hidden_name ) {
435 7         213 $hidden_name = $self->default_multiform_hidden_name;
436             }
437              
438             # don't include the hidden-field's name in valid_names
439 25         74 @valid_names = grep { $_ ne $hidden_name } @valid_names;
  70         166  
440              
441 25         55 my %params;
442 25 50       50 my @file_fields = @{ $self->_file_fields || [] };
  25         690  
443              
444 25         71 for my $name (@valid_names) {
445 62         187 my $value = $form->param_value($name);
446              
447 62         7478 $self->set_nested_hash_value( \%params, $name, $value );
448              
449             # populate @file_field
450 62 50       1216 if ( ref $value ne 'ARRAY' ) {
451 62         143 $value = [$value];
452             }
453              
454 62         127 for my $value (@$value) {
455 62 50 33     262 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         153 @file_fields = sort uniq @file_fields;
463              
464 25         60 my $crypt = Crypt::CBC->new( %{ $self->crypt_args } );
  25         718  
465              
466 25         19860 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         64 for my $key ( @{ $self->persist_stash } ) {
  25         780  
476 0         0 $data->{persist_stash}{$key} = $form->stash->{$key};
477             }
478              
479             # save file_fields
480 25         688 $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         674 my $current_form = $self->current_form;
486              
487 25         516 my $input = $current_form->input;
488 25         656 my $query = $current_form->query;
489 25         627 my $processed_params = $current_form->_processed_params;
490 25         258 my $parent = $current_form->parent;
491 25         203 my $stash = $current_form->stash;
492              
493 25         644 $current_form->input( {} );
494 25         747 $current_form->query( {} );
495 25         712 $current_form->_processed_params( {} );
496 25         346 $current_form->parent( {} );
497              
498             # empty the stash
499 25         295 %{ $current_form->stash } = ();
  25         74  
500              
501             # save a map of upload refaddrs to their parent
502 25         163 my %upload_parent;
503              
504 25         68 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         71 local $Storable::canonical = 1;
514 25         124 $data = nfreeze($data);
515              
516             # restore form
517 25         2606 $current_form->input($input);
518 25         760 $current_form->query($query);
519 25         671 $current_form->_processed_params($processed_params);
520 25         336 $current_form->parent($parent);
521              
522 25         224 %{ $current_form->stash } = %$stash;
  25         69  
523              
524 25         181 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         121 $data = $crypt->encrypt_hex($data);
534              
535 25         24986 my $hidden_field = $next_form->get_field( { nested_name => $hidden_name, } );
536              
537 25         23238 $hidden_field->default($data);
538              
539 25         620 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.01
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