File Coverage

blib/lib/HTML/FormFu/MultiForm.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package HTML::FormFu::MultiForm;
2             {
3             $HTML::FormFu::MultiForm::VERSION = '1.00';
4             }
5 1     1   38005 use Moose;
  0            
  0            
6             use MooseX::Attribute::Chained;
7              
8             with
9             'HTML::FormFu::Role::FormAndElementMethods' =>
10             { -excludes => 'model_config' },
11             'HTML::FormFu::Role::FormBlockAndFieldMethods',
12             'HTML::FormFu::Role::NestedHashUtils',
13             'HTML::FormFu::Role::Populate';
14              
15             use HTML::FormFu;
16             use HTML::FormFu::Attribute qw(
17             mk_attrs
18             mk_attr_accessors
19             mk_inherited_accessors
20             mk_inherited_merging_accessors
21             mk_output_accessors
22             );
23             use HTML::FormFu::ObjectUtil qw(
24             form
25             clone stash
26             parent
27             load_config_file load_config_filestem
28             _string_equals _object_equals
29             );
30             use HTML::FormFu::QueryType::CGI;
31              
32             use Carp qw( croak );
33             use Clone ();
34             use Crypt::CBC;
35             use List::MoreUtils qw( uniq );
36             use Scalar::Util qw( blessed refaddr );
37             use Storable qw( nfreeze thaw );
38              
39             use overload (
40             'eq' => '_string_equals',
41             '==' => '_object_equals',
42             '""' => sub { return shift->render },
43             bool => sub {1},
44             fallback => 1
45             );
46              
47             __PACKAGE__->mk_attr_accessors(qw( id action enctype method ));
48              
49             # accessors shared with HTML::FormFu
50             our @ACCESSORS = qw(
51             default_args
52             model_config auto_fieldset
53             );
54              
55             for my $name (@ACCESSORS) {
56             has $name => ( is => 'rw', traits => ['Chained'] );
57             }
58              
59             for my $name (@HTML::FormFu::MULTIFORM_SHARED) {
60             has $name => ( is => 'rw', traits => ['Chained'] );
61             }
62              
63             has forms => ( is => 'rw', traits => ['Chained'] );
64             has query => ( is => 'rw', traits => ['Chained'] );
65             has current_form_number => ( is => 'rw', traits => ['Chained'] );
66             has current_form => ( is => 'rw', traits => ['Chained'] );
67             has multiform_hidden_name => ( is => 'rw', traits => ['Chained'] );
68             has default_multiform_hidden_name => ( is => 'rw', traits => ['Chained'] );
69             has combine_params => ( is => 'rw', traits => ['Chained'] );
70             has complete => ( is => 'rw', traits => ['Chained'] );
71              
72             has crypt_args => (
73             is => 'rw',
74             isa => 'HashRef',
75             default => sub { +{} },
76             );
77              
78             has _data => ( is => 'rw' );
79              
80             __PACKAGE__->mk_output_accessors(qw( form_error_message ));
81              
82             our @SHARED_WITH_FORMFU = (
83             @ACCESSORS,
84             @HTML::FormFu::MULTIFORM_SHARED,
85             @HTML::FormFu::Role::FormAndElementMethods::MULTIFORM_SHARED,
86             @HTML::FormFu::Role::FormBlockAndFieldMethods::MULTIFORM_SHARED,
87             );
88              
89             *loc = \&localize;
90              
91             for my $name ( qw(
92             persist_stash
93             _file_fields
94             ) )
95             {
96             has $name => (
97             is => 'rw',
98             default => sub { [] },
99             lazy => 1,
100             isa => 'ArrayRef',
101             );
102             }
103              
104             has languages => (
105             is => 'rw',
106             default => sub { ['en'] },
107             lazy => 1,
108             isa => 'ArrayRef',
109             );
110              
111             sub BUILD {
112             my ( $self, $args ) = @_;
113              
114             my %defaults = (
115             tt_args => {},
116             model_config => {},
117             combine_params => 1,
118             default_multiform_hidden_name => '_multiform',
119             );
120              
121             $self->populate( \%defaults );
122              
123             return $self;
124             }
125              
126             sub process {
127             my ( $self, $query ) = @_;
128              
129             $query ||= $self->query;
130              
131             # save it for further calls to process()
132             if ($query) {
133             $self->query($query);
134             }
135              
136             my $hidden_name = $self->multiform_hidden_name;
137              
138             if ( !defined $hidden_name ) {
139             $hidden_name = $self->default_multiform_hidden_name;
140             }
141              
142             my $input;
143              
144             if ( defined $query && blessed($query) ) {
145             $input = $query->param($hidden_name);
146             }
147             elsif ( defined $query ) {
148              
149             # it's not an object, just a hashref.
150             # and HTML::FormFu::FakeQuery doesn't work with a MultiForm object
151              
152             $input = $self->get_nested_hash_value( $query, $hidden_name );
153             }
154              
155             my $data = $self->_process_get_data($input);
156             my $current_form_num;
157             my @forms;
158              
159             eval { @forms = @{ $self->forms } };
160             croak "forms() must be an arrayref" if $@;
161              
162             if ( defined $data ) {
163             $current_form_num = $data->{current_form};
164              
165             my $current_form
166             = $self->_load_current_form( $current_form_num, $data );
167              
168             # are we on the last form?
169             # are we complete?
170              
171             if ( ( $current_form_num == scalar @forms )
172             && $current_form->submitted_and_valid )
173             {
174             $self->complete(1);
175             }
176              
177             $self->_data($data);
178             }
179             else {
180              
181             # default to first form
182              
183             $self->_load_current_form(1);
184             }
185              
186             return;
187             }
188              
189             sub _process_get_data {
190             my ( $self, $input ) = @_;
191              
192             return if !defined $input || !length $input;
193              
194             my $crypt = Crypt::CBC->new( %{ $self->crypt_args } );
195              
196             my $data;
197              
198             eval { $data = $crypt->decrypt_hex($input) };
199              
200             if ( defined $data ) {
201             $data = thaw($data);
202              
203             $self->_file_fields( $data->{file_fields} );
204              
205             # rebless all file uploads as basic CGI objects
206             for my $name ( @{ $data->{file_fields} } ) {
207             my $value = $self->get_nested_hash_value( $data->{params}, $name );
208              
209             _rebless_upload($value);
210             }
211             }
212             else {
213              
214             # TODO: should handle errors better
215             $data = undef;
216             }
217              
218             return $data;
219             }
220              
221             sub _rebless_upload {
222             my ($value) = @_;
223              
224             if ( ref $value eq 'ARRAY' ) {
225             for my $value (@$value) {
226             _rebless_upload($value);
227             }
228             }
229             elsif ( blessed($value) ) {
230             bless $value, 'HTML::FormFu::QueryType::CGI';
231             }
232              
233             return;
234             }
235              
236             sub _load_current_form {
237             my ( $self, $current_form_num, $data ) = @_;
238              
239             my $current_form = HTML::FormFu->new;
240              
241             my $current_data = Clone::clone( $self->forms->[ $current_form_num - 1 ] );
242              
243             # merge constructor args
244             for my $key ( @SHARED_WITH_FORMFU ) {
245             my $value = $self->$key;
246              
247             if ( defined $value ) {
248             $current_form->$key($value);
249             }
250             }
251              
252             # copy attrs
253             my $attrs = $self->attrs;
254              
255             for my $key ( keys %$attrs ) {
256             $current_form->$key( $attrs->{$key} );
257             }
258              
259             # copy stash
260             my $stash = $self->stash;
261              
262             while ( my ( $key, $value ) = each %$stash ) {
263             $current_form->stash->{$key} = $value;
264             }
265              
266             # persist_stash
267             if ( defined $data ) {
268             for my $key ( @{ $self->persist_stash } ) {
269             $current_form->stash->{$key} = $data->{persist_stash}{$key};
270             }
271             }
272              
273             # build form
274             $current_form->populate($current_data);
275              
276             # add hidden field
277             if ( ( !defined $self->multiform_hidden_name ) && $current_form_num > 1 ) {
278             my $field = $current_form->element( {
279             type => 'Hidden',
280             name => $self->default_multiform_hidden_name,
281             } );
282              
283             $field->constraint( { type => 'Required', } );
284             }
285              
286             $current_form->query( $self->query );
287             $current_form->process;
288              
289             # combine params
290             if ( defined $data && $self->combine_params ) {
291              
292             my $params = $current_form->params;
293              
294             for my $name ( @{ $data->{valid_names} } ) {
295              
296             next if $self->nested_hash_key_exists( $params, $name );
297              
298             my $value = $self->get_nested_hash_value( $data->{params}, $name );
299              
300             # need to set upload object's parent manually
301             # for now, parent points to the form
302             # when formfu fixes this, this code will need updated
303             _reparent_upload( $value, $current_form );
304              
305             $current_form->add_valid( $name, $value );
306             }
307             }
308              
309             $self->current_form_number($current_form_num);
310             $self->current_form($current_form);
311              
312             return $current_form;
313             }
314              
315             sub _reparent_upload {
316             my ( $value, $form ) = @_;
317              
318             if ( ref $value eq 'ARRAY' ) {
319             for my $value (@$value) {
320             _reparent_upload( $value, $form );
321             }
322             }
323             elsif ( blessed($value) && $value->isa('HTML::FormFu::Upload') ) {
324             $value->parent($form);
325             }
326              
327             return;
328             }
329              
330             sub render {
331             my $self = shift;
332              
333             my $form = $self->current_form;
334              
335             croak "process() must be called before render()"
336             if !defined $form;
337              
338             if ( $self->complete ) {
339              
340             # why would you render if it's complete?
341             # anyway, just show the last form
342             return $form->render(@_);
343             }
344              
345             if ( $form->submitted_and_valid ) {
346              
347             # return the next form
348             return $self->next_form->render(@_);
349             }
350              
351             # return the current form
352             return $form->render(@_);
353             }
354              
355             sub next_form {
356             my ($self) = @_;
357              
358             my $form = $self->current_form;
359              
360             croak "process() must be called before next_form()"
361             if !defined $form;
362              
363             my $current_form_num = $self->current_form_number;
364              
365             # is there a next form defined?
366             return if $current_form_num >= scalar @{ $self->forms };
367              
368             my $form_data = Clone::clone( $self->forms->[$current_form_num] );
369              
370             my $next_form = HTML::FormFu->new;
371              
372             # merge constructor args
373             for my $key ( @SHARED_WITH_FORMFU ) {
374             my $value = $self->$key;
375              
376             if ( defined $value ) {
377             $next_form->$key($value);
378             }
379             }
380              
381             # copy attrs
382             my $attrs = $self->attrs;
383              
384             while ( my ( $key, $value ) = each %$attrs ) {
385             $next_form->$key($value);
386             }
387              
388             # copy stash
389             my $current_form = $self->current_form;
390             my $current_stash = $current_form->stash;
391              
392             while ( my ( $key, $value ) = each %$current_stash ) {
393             $next_form->stash->{$key} = $value;
394             }
395              
396             # persist_stash
397             for my $key ( @{ $self->persist_stash } ) {
398             $next_form->stash->{$key} = $current_form->stash->{$key};
399             }
400              
401             # build the form
402             $next_form->populate($form_data);
403              
404             # add hidden field
405             if ( !defined $self->multiform_hidden_name ) {
406             my $field = $next_form->element( {
407             type => 'Hidden',
408             name => $self->default_multiform_hidden_name,
409             } );
410              
411             $field->constraint( { type => 'Required', } );
412             }
413              
414             $next_form->process;
415              
416             # encrypt params in hidden field
417             $self->_save_hidden_data( $current_form_num, $next_form, $form );
418              
419             return $next_form;
420             }
421              
422             sub _save_hidden_data {
423             my ( $self, $current_form_num, $next_form, $form ) = @_;
424              
425             my @valid_names = $form->valid;
426             my $hidden_name = $self->multiform_hidden_name;
427              
428             if ( !defined $hidden_name ) {
429             $hidden_name = $self->default_multiform_hidden_name;
430             }
431              
432             # don't include the hidden-field's name in valid_names
433             @valid_names = grep { $_ ne $hidden_name } @valid_names;
434              
435             my %params;
436             my @file_fields = @{ $self->_file_fields || [] };
437              
438             for my $name (@valid_names) {
439             my $value = $form->param_value($name);
440              
441             $self->set_nested_hash_value( \%params, $name, $value );
442              
443             # populate @file_field
444             if ( ref $value ne 'ARRAY' ) {
445             $value = [$value];
446             }
447              
448             for my $value (@$value) {
449             if ( blessed($value) && $value->isa('HTML::FormFu::Upload') ) {
450             push @file_fields, $name;
451             last;
452             }
453             }
454             }
455              
456             @file_fields = sort uniq @file_fields;
457              
458             my $crypt = Crypt::CBC->new( %{ $self->crypt_args } );
459              
460             my $data = {
461             current_form => $current_form_num + 1,
462             valid_names => \@valid_names,
463             params => \%params,
464             persist_stash => {},
465             file_fields => \@file_fields,
466             };
467              
468             # persist_stash
469             for my $key ( @{ $self->persist_stash } ) {
470             $data->{persist_stash}{$key} = $form->stash->{$key};
471             }
472              
473             # save file_fields
474             $self->_file_fields( \@file_fields );
475              
476             # to freeze, we need to remove anything that might have a
477             # file handle or code block
478             # make sure we restore them, after freezing
479             my $current_form = $self->current_form;
480              
481             my $input = $current_form->input;
482             my $query = $current_form->query;
483             my $processed_params = $current_form->_processed_params;
484             my $parent = $current_form->parent;
485             my $stash = $current_form->stash;
486              
487             $current_form->input( {} );
488             $current_form->query( {} );
489             $current_form->_processed_params( {} );
490             $current_form->parent( {} );
491              
492             # empty the stash
493             %{ $current_form->stash } = ();
494              
495             # save a map of upload refaddrs to their parent
496             my %upload_parent;
497              
498             for my $name (@file_fields) {
499             next if !$self->nested_hash_key_exists( \%params, $name );
500              
501             my $value = $self->get_nested_hash_value( \%params, $name );
502              
503             _save_upload_parent( \%upload_parent, $value );
504             }
505              
506             # freeze
507             local $Storable::canonical = 1;
508             $data = nfreeze($data);
509              
510             # restore form
511             $current_form->input($input);
512             $current_form->query($query);
513             $current_form->_processed_params($processed_params);
514             $current_form->parent($parent);
515              
516             %{ $current_form->stash } = %$stash;
517              
518             for my $name (@file_fields) {
519             next if !$self->nested_hash_key_exists( \%params, $name );
520              
521             my $value = $self->get_nested_hash_value( \%params, $name );
522              
523             _restore_upload_parent( \%upload_parent, $value );
524             }
525              
526             # store data in hidden field
527             $data = $crypt->encrypt_hex($data);
528              
529             my $hidden_field
530             = $next_form->get_field( { nested_name => $hidden_name, } );
531              
532             $hidden_field->default($data);
533              
534             return;
535             }
536              
537             sub _save_upload_parent {
538             my ( $upload_parent, $value ) = @_;
539              
540             if ( ref $value eq 'ARRAY' ) {
541             for my $value (@$value) {
542             _save_upload_parent( $upload_parent, $value );
543             }
544             }
545             elsif ( blessed($value) && $value->isa('HTML::FormFu::Upload') ) {
546             my $refaddr = refaddr($value);
547              
548             $upload_parent->{$refaddr} = $value->parent;
549              
550             $value->parent(undef);
551             }
552              
553             return;
554             }
555              
556             sub _restore_upload_parent {
557             my ( $upload_parent, $value ) = @_;
558              
559             if ( ref $value eq 'ARRAY' ) {
560             for my $value (@$value) {
561             _restore_upload_parent( $upload_parent, $value );
562             }
563             }
564             elsif ( blessed($value) && $value->isa('HTML::FormFu::Upload') ) {
565             my $refaddr = refaddr($value);
566              
567             $value->parent( $upload_parent->{$refaddr} );
568             }
569              
570             return;
571             }
572              
573             __PACKAGE__->meta->make_immutable;
574              
575             1;
576              
577             __END__
578              
579             =head1 NAME
580              
581             HTML::FormFu::MultiForm - Handle multi-page/stage forms
582              
583             =head1 DESCRIPTION
584              
585             For now, see test files in L<Catalyst::Controller::HTML::FormFu> for examples.
586              
587             =head1 AUTHOR
588              
589             Carl Franks, C<cfranks@cpan.org>
590              
591             =head1 LICENSE
592              
593             This library is free software, you can redistribute it and/or modify it under
594             the same terms as Perl itself.
595              
596             =cut