File Coverage

blib/lib/CGI/Application/Plugin/FormState.pm
Criterion Covered Total %
statement 114 116 98.2
branch 33 36 91.6
condition 10 15 66.6
subroutine 22 22 100.0
pod 7 9 77.7
total 186 198 93.9


line stmt bran cond sub pod time code
1              
2             package CGI::Application::Plugin::FormState;
3              
4 8     8   366177 use warnings;
  8         19  
  8         262  
5 8     8   41 use strict;
  8         14  
  8         353  
6              
7 8     8   41 use CGI::Application;
  8         17  
  8         138  
8 8     8   6879 use CGI::Session::ID::md5;
  8         2547  
  8         189  
9 8     8   884 use CGI::Application::Plugin::Session;
  8         7152  
  8         54  
10 8     8   567 use vars qw(@ISA @EXPORT);
  8         17  
  8         365  
11              
12              
13 8     8   37 use Carp;
  8         11  
  8         504  
14 8     8   37 use Scalar::Util qw(weaken isweak);
  8         13  
  8         916  
15              
16 8     8   40 use Exporter;
  8         19  
  8         9844  
17             @ISA = qw(Exporter);
18             @EXPORT = qw(form_state);
19              
20             our $CGIAPP_Namespace = '__CAP_FORM_STATE';
21             my $Default_Expires = '2d';
22             my $Default_Storage_Name = 'cap_form_state';
23              
24             sub import {
25 15     15   5985 my $caller = scalar(caller);
26 15 50       223 if ($caller->can('add_callback')) {
27 15         70 $caller->add_callback('load_tmpl', \&_add_form_state_id_to_tmpl);
28             }
29             else {
30 0         0 croak "CAP::FormState: Calling package ($caller) is not a CGI::Application module so cannot install load_tmpl hooks. If you are using \@ISA instead of 'use base', make sure it is in a BEGIN { } block, and make sure these statements appear before the plugin is loaded";
31             }
32 15         779 goto &Exporter::import;
33             }
34              
35             =head1 NAME
36              
37             CGI::Application::Plugin::FormState - Store Form State without Hidden Fields
38              
39             =head1 VERSION
40              
41             Version 0.12
42              
43             =cut
44              
45             our $VERSION = '0.12';
46              
47             =head1 SYNOPSIS
48              
49             FormState is just a temporary stash that you can use for storing and
50             retrieving private parameters in your multi-page form.
51              
52             use CGI::Application::Plugin::FormState;
53              
54             my $form = <
55            
56            
57            
58             ...
59            
60             EOF
61              
62             sub form_display_runmode {
63             my $self = shift;
64              
65             # Store some parameters
66             $self->form_state->param('name' => 'Road Runner');
67             $self->form_state->param('occupation' => 'Having Fun');
68              
69             my $t = $self->load_tmpl(scalarref => \$form);
70             return $t->output;
71              
72             }
73              
74             sub form_process_runmode {
75             my $self = shift;
76              
77             # Retrieve some parameters
78             print $self->form_state->param('name'); # 'Road Runner'
79             print $self->form_state->param('occupation'); # 'Having Fun'
80             }
81              
82              
83             =head1 EXAMPLE
84              
85             This is a more complete example, using L.
86              
87             use CGI::Application::Plugin::Session;
88             use CGI::Application::Plugin::FormState;
89             use CGI::Application::Plugin::ValidateRM;
90              
91             my $form = <
92            
93            
94            
95             ...
96            
97             EOF
98              
99             sub my_form_display {
100             my $self = shift;
101             my $errs = shift;
102             my $t = $self->load_tmpl(scalarref => \$form);
103              
104             # Stash some data into it
105             $self->form_state->param('name' => 'Wile E. Coyote');
106             $self->form_state->param('occupation' => 'Mining Engineer');
107              
108             # Normal ValidateRM error handling
109             $t->param($errs) if $errs;
110             return $t->output;
111             }
112              
113             sub my_form_process {
114             my $self;
115              
116             # Normal ValidateRM validation
117             my ($results, $err_page) = $self->check_rm('my_form_display','_my_form_profile');
118             return $err_page if $err_page;
119              
120             # The data from the submitted form
121             my $params = $self->dfv_results;
122              
123             $params->{'name'} = $self->form_state->param('name'); # 'Wile E. Coyote'
124             $params->{'occupation'} = $self->form_state->param('occupation'); # 'Mining Engineer'
125              
126              
127             # Now do something interesting with $params
128             # ...
129              
130              
131             my $t = $self->load_tmpl('success.html');
132             return $t->output;
133             }
134              
135             # Standard ValiateRM profile
136             sub _my_form_profile {
137             return {
138             required => 'email',
139             msgs => {
140             any_errors => 'some_errors',
141             prefix => 'err_',
142             },
143             };
144             }
145              
146              
147             =head1 DESCRIPTION
148              
149             C provides a temporary storage area
150             within the user's session for storing form-related data.
151              
152             The main use of this is for multi-page forms. Instead of using hidden
153             fields to store data related to the form, you store and retrieve values
154             from the form state.
155              
156             In the first instance of your app:
157              
158             $self->form_state->param('some_name' => 'some_value');
159             $self->form_state->param('some_other_name' => 'some_other_value');
160              
161             And later, in a different instance of your app:
162              
163             $val1 = $self->form_state->param('some_name');
164             $val2 = $self->form_state->param('some_other_name');
165              
166             To connect the first instance and the second, you put a single hidden
167             field in your template:
168              
169            
170              
171             You don't have to worry about creating the template param
172             C; it is added automatically to your template
173             parameters via the C hook.
174              
175             If you want to use a parameter other than C you can do
176             so via the C parameter to Cconfig>.
177              
178             If you're skeptical about whether all this abstraction is a good idea,
179             see L<"MOTIVATION">, below.
180              
181             =head1 PRESERVING FORM STATE ACROSS REDIRECTS
182              
183             You can include the form_state hash in a link:
184              
185             my $link = '/app.cgi?rm=list&cap_form_state=' . $self->form_state->id;
186              
187             If you use L, you can easily create redirect this way:
188              
189             $self->redirect('/app.cgi?rm=list&cap_form_state=' . $self->form_state->id);
190              
191             If you also use L
192             it is as simple as:
193              
194             $self->redirect($self->link('/app.cgi', 'rm' => 'list', 'cap_form_state' => $self->form_state->id));
195              
196             Or, in the case of a link to the currently running app:
197              
198             $self->redirect($self->self_link('rm' => 'list', 'cap_form_state' => $self->form_state->id));
199              
200              
201             =head1 IMPLEMENTATION
202              
203             When you call C<< $self->form_state >> for the first time, a top-level
204             key is created in the user's session. This key contains a random,
205             hard-to-guess element. It might look something like:
206              
207             form_state_cap_form_state_84eb13cfed01764d9c401219faa56d53
208              
209             All data you place in the form state with C is stored in the
210             user's session under this key.
211              
212             You pass the name of this key on to the next instance of your
213             application by means of a hidden field in your form:
214              
215            
216              
217             You manually put this hidden field in your template. The template
218             parameter C is automatically added to your template
219             parameters via the C hook. It contains the random,
220             hard-to-guess portion (e.g. C<84eb13cfed01764d9c401219faa56d53>). When
221             the template is filled, the hidden field will look something like this:
222              
223            
224              
225             Since all values are stored on the server in the user's session, the
226             user can't tamper with any of them.
227              
228             To keep old form_data from cluttering up the user's session, the system
229             uses L's C feature to expire old form state keys
230             after a reasonable amount of time has passed (2 days by default).
231              
232             You can manually delete a form state storage by calling:
233              
234             $self->form_state->delete;
235              
236             =cut
237              
238             sub _new {
239 9     9   22 my ($class, $webapp) = @_;
240              
241 9         68 my $self = {
242             '__CGIAPP_OBJ' => $webapp,
243             '__STORAGE_NAME' => undef,
244             '__STORAGE_HASH' => undef,
245             '__STORAGE_KEY' => undef,
246             '__EXPIRES' => undef,
247             '__CONFIGURED' => undef,
248             };
249              
250             # Force reference to CGI::Application object to be weak to avoid
251             # circular references
252 9         51 weaken($self->{'__CGIAPP_OBJ'});
253              
254 9         50 return bless $self, $class;
255             }
256              
257             sub form_state {
258 109     109 0 680493 my ($self) = @_;
259              
260             # It's possible that in the future we will allow named configs, e.g.
261             # $self->form_state('foo')->param('bar);
262              
263 109 100       386 if (not exists $self->{$CGIAPP_Namespace}->{'__DEFAULT_CONFIG'}) {
264 9         91 $self->{$CGIAPP_Namespace}->{'__DEFAULT_CONFIG'} = __PACKAGE__->_new($self);
265             }
266 109         502 return $self->{$CGIAPP_Namespace}->{'__DEFAULT_CONFIG'};
267             }
268              
269             =head1 METHODS
270              
271             =over 4
272              
273             =item config(%options)
274              
275             Sets defaults for the plugin.
276              
277             B
278              
279             $self->form_state->config('name' => 'storage_names', 'expires' => '3d')
280              
281             The following options are allowed:
282              
283             =over 4
284              
285             =item name
286              
287             Sets the name of the default form state storage. This name is used for
288             the key in the user's session, for the name of hidden form field, and
289             the template parameter used to fill the hidden form field. So if you
290             set the C to C:
291              
292             $self->form_state_config('name' => 'foo');
293              
294             then the hidden field in your template should look like this:
295              
296            
297              
298             and the key in the user's session would look something like this:
299              
300             form_state_foo_84eb13cfed01764d9c401219faa56d53
301              
302             =item expires
303              
304             Indicates when form state storage keys should expire and disappear from
305             the user's session. Uses the same format as L's
306             C. Defaults to 2 days (C<'2d'>). To cancel expiration and make
307             the form state last as long as the user's session does, use:
308              
309             $self->form_state_config('expires' => 0);
310              
311             =back
312              
313             =cut
314              
315             my %Installed_Callback;
316              
317             sub config {
318 13     13 1 30 my $self = shift;
319 13         36 my %args = @_;
320 13 100       65 my $storage_name = delete $args{'name'} if exists $args{'name'};
321 13 100       58 my $expires = delete $args{'expires'} if exists $args{'expires'};
322              
323 13 100       44 if (keys %args) {
324 2         476 croak "CAP::FormState: unknown configuration keys: " . (join ', ', keys %args);
325             }
326              
327 11   66     130 $self->{'__STORAGE_NAME'} ||= $storage_name || $Default_Storage_Name;
      66        
328 11   66     118 $self->{'__EXPIRES'} ||= $expires || $Default_Expires;
      66        
329              
330 11         39 $self->{'__CONFIGURED'} = 1;
331              
332 11         76 $self->_initialize;
333             }
334              
335              
336             # Deprecated method. Wrapper around 'config'
337             sub init {
338 3     3 0 6 my $self = shift;
339 3         8 my $storage_name = shift;
340 3         11 my %args = @_;
341 3         14 $self->config('name' => $storage_name, %args);
342             }
343              
344             sub _initialize {
345 11     11   20 my $self = shift;
346              
347 11         56 my $webapp = $self->{__CGIAPP_OBJ};
348 11         41 my $expires = $self->{__EXPIRES};
349              
350 11         21 my $storage_name = $self->{__STORAGE_NAME};
351              
352 11   66     47 my $storage_hash = $webapp->query->param($storage_name)
353             || $webapp->query->url_param($storage_name);
354 11         3558 my $storage_key;
355              
356             my $already_exists;
357              
358 11 100       39 if ($storage_hash) {
359             # restore existing session
360 2         7 $storage_key = 'form_state_' . $storage_name . '_' . $storage_hash;
361 2 50       11 if (ref $webapp->session->param($storage_key) eq 'HASH') {
362 2         46 $already_exists = 1;
363             }
364             }
365              
366 11 100       37 unless ($already_exists) {
367             # create new session
368              
369 9         49 $storage_hash = CGI::Session::ID::md5->generate_id;
370              
371 9         243 $storage_key = 'form_state_' . $storage_name . '_' . $storage_hash;
372 9         40 $webapp->session->param($storage_key => {});
373 9         318 $webapp->query->param($storage_name => $storage_hash);
374              
375             }
376              
377             # reset expiry date on key
378 11         598 $webapp->session->expire($storage_key, $expires);
379              
380 11         502 $self->{'__STORAGE_NAME'} = $storage_name;
381 11         22 $self->{'__STORAGE_HASH'} = $storage_hash;
382 11         21 $self->{'__STORAGE_KEY'} = $storage_key;
383 11         22 $self->{'__EXPIRES'} = $expires;
384              
385 11 100       37 return 1 if $already_exists;
386 9         32 return;
387             }
388              
389             =item param
390              
391             Read and set values in the form state storage. It acts like the
392             C method typically does in modules such as L,
393             L, L, C etc.
394              
395             # set a value
396             $self->form_state->param('some_name' => 'some_value');
397              
398             # retrieve a value
399             my $val = $self->form_state->param('some_name');
400              
401             # set multiple values
402             $self->form_state->param(
403             'some_name' => 'some_value',
404             'some_other_name' => 'some_other_value',
405             );
406              
407             # retrive the names of all the keys
408             my @keys = $self->form_state->param;
409              
410             =cut
411              
412             sub param {
413 65     65 1 86 my $self = shift;
414              
415 65 100       161 if (!$self->{'__CONFIGURED'}) {
416 2         14 $self->config;
417             }
418 65         86 my $storage_key = $self->{'__STORAGE_KEY'};
419              
420 65         80 my $webapp = $self->{__CGIAPP_OBJ};
421 65         186 my $store = $webapp->session->param($storage_key);
422              
423 65 100       1293 if (@_) {
424 45         52 my $param;
425 45 100       135 if (ref $_[0] eq 'HASH') {
    100          
426 4         9 $param = shift;
427             }
428             elsif (@_ == 1) {
429 27         140 return $store->{$_[0]};
430             }
431             else {
432 14         80 $param = { @_ };
433             }
434 18         87 $store->{$_} = $param->{$_} for keys %$param;
435              
436             # A param has been changed, so we touch the session
437 18         56 $webapp->session->param($storage_key => $store);
438             }
439             else {
440 20         103 return keys %$store;
441             }
442             }
443              
444             =item clear_params
445              
446             Clear all of the values in the form state storage:
447              
448             $self->form_state->param('name' => 'Road Runner');
449             $self->form_state->clear_params;
450             print $self->form_state->param('name'); # undef
451              
452              
453             =cut
454              
455             sub clear_params {
456 4     4 1 13 my $self = shift;
457              
458 4 50       19 if (!$self->{'__CONFIGURED'}) {
459 0         0 $self->config;
460             }
461 4         8 my $storage_key = $self->{'__STORAGE_KEY'};
462              
463 4         9 my $webapp = $self->{__CGIAPP_OBJ};
464 4         16 $webapp->session->param($storage_key => {});
465             }
466              
467             =item delete
468              
469             Deletes the form_state storage from the user's session.
470              
471             =cut
472              
473             sub delete {
474 2     2 1 4 my $self = shift;
475              
476             # No need to call $self->config and create the session key since we're
477             # just going to delete it.
478             #
479             # However, it's very important not to call session->clear without a key;
480             # if we do, we'll delete all params in the user's session!
481              
482 2 100       13 if ($self->{'__STORAGE_KEY'}) {
483 1         7 $self->{__CGIAPP_OBJ}->session->clear($self->{'__STORAGE_KEY'});
484             }
485             }
486              
487              
488             =item id
489              
490             Returns the current value of the storage param - the "hard to guess"
491             portion of the session key.
492              
493             my $id = $self->form_state->id;
494              
495             =cut
496              
497             sub id {
498 14     14 1 25 my $self = shift;
499              
500 14 100       63 if (!$self->{'__CONFIGURED'}) {
501 1         5 $self->config;
502             }
503 14         47 $self->{'__STORAGE_HASH'};
504             }
505              
506             =item name
507              
508             Returns the current name being used for storage.
509             Defaults to C.
510              
511             my $name = $self->form_state->name;
512              
513             =cut
514              
515             sub name {
516 9     9 1 17 my $self = shift;
517              
518 9 100       40 if (!$self->{'__CONFIGURED'}) {
519 1         4 $self->config;
520             }
521 9         36 $self->{'__STORAGE_NAME'};
522             }
523              
524             =item session_key
525              
526             Returns the full key used for storage in the user's session.
527              
528             my $key = $self->form_state->session_key;
529              
530             # Get the full form state hash
531             my $data = $self->session->param($key);
532              
533             The following can be used to debug the form_state data:
534              
535             use Data::Dumper;
536             print STDERR Dumper $self->session->param($self->form_state->session_key);
537              
538             =back
539              
540             =cut
541              
542             sub session_key {
543 5     5 1 12 my $self = shift;
544              
545 5 100       24 if (!$self->{'__CONFIGURED'}) {
546 1         4 $self->config;
547             }
548 5         32 $self->{'__STORAGE_KEY'};
549             }
550              
551             # add the storage id to the template params
552             sub _add_form_state_id_to_tmpl {
553 4     4   222 my ($self, $ht_params, $tmpl_params, $tmpl_file) = @_;
554              
555 4         14 my $storage_hash = $self->form_state->id;
556 4         14 my $storage_name = $self->form_state->name;
557              
558 4         19 $tmpl_params->{$storage_name} = $storage_hash;
559             }
560              
561             =head1 MOTIVATION
562              
563             =head2 Why not just use hidden fields?
564              
565             Hidden fields are not secure. The end user could save a local copy of
566             your form, change the hidden fields and tamper with your app's form
567             state.
568              
569             =head2 Why not just use the user's session?
570              
571             With C the data is associated with
572             a particular instance of a form, not with the user. If the user gives
573             up halfway through your multi-page form, you don't want their session to
574             be cluttered up with the incomplete form state data.
575              
576             If a user opens up your application in two browser windows (both sharing
577             the same user session), each window should have it's own independent
578             form state.
579              
580             For instance, in an email application the user might have one window
581             open for the inbox and another open for the outbox. If you store the
582             value of C<"current_mailbox"> in the user's session, then one of these
583             windows will go to the wrong mailbox.
584              
585             Finally, the user's session probably sticks around longer than the form
586             state should.
587              
588             =head1 AUTHOR
589              
590             Michael Graham, C<< >>
591              
592             =head1 BUGS
593              
594             Please report any bugs or feature requests to
595             C, or through the web interface at
596             L. I will be notified, and then you'll automatically
597             be notified of progress on your bug as I make changes.
598              
599             =head1 ACKNOWLEDGEMENTS
600              
601             Thanks to Richard Dice and Cees Hek for helping me sort out the issues
602             with this approach.
603              
604             The informative error message text used for when this module is loaded
605             before your app actually C<@ISA> C object was stolen from
606             Cees's L module.
607              
608             =head1 COPYRIGHT & LICENSE
609              
610             Copyright 2005 Michael Graham, All Rights Reserved.
611              
612             This program is free software; you can redistribute it and/or modify it
613             under the same terms as Perl itself.
614              
615             =cut
616              
617             1;