File Coverage

blib/lib/Dancer2/Plugin/Deferred.pm
Criterion Covered Total %
statement 56 59 94.9
branch 8 12 66.6
condition 8 10 80.0
subroutine 15 15 100.0
pod 3 4 75.0
total 90 100 90.0


line stmt bran cond sub pod time code
1 2     2   2095306 use 5.008001;
  2         22  
2 2     2   12 use strict;
  2         4  
  2         43  
3 2     2   10 use warnings;
  2         4  
  2         182  
4              
5             package Dancer2::Plugin::Deferred;
6             our $AUTHORITY = 'cpan:YANICK';
7             $Dancer2::Plugin::Deferred::VERSION = '0.008000';
8             # ABSTRACT: Defer messages or data across redirections
9             # VERSION
10              
11 2     2   16 use Dancer2::Core::Types qw/Str/;
  2         6  
  2         17  
12 2     2   2344 use URI;
  2         4  
  2         57  
13 2     2   947 use URI::QueryParam;
  2         1595  
  2         72  
14              
15 2     2   1078 use Dancer2::Plugin 0.200000;
  2         28445  
  2         25  
16              
17             has var_key => (
18             is => 'ro',
19             isa => Str,
20             from_config => sub { 'dpdid' },
21             );
22              
23             has var_keep_key => (
24             is => 'ro',
25             isa => Str,
26             from_config => sub { 'dpd_keep' },
27             );
28              
29             has params_key => (
30             is => 'ro',
31             isa => Str,
32             from_config => sub { 'dpdid' },
33             );
34              
35             has session_key_prefix => (
36             is => 'ro',
37             isa => Str,
38             from_config => sub { 'dpd_' },
39             );
40              
41             has template_key => (
42             is => 'ro',
43             isa => Str,
44             from_config => sub { 'deferred' },
45             );
46              
47             plugin_keywords 'deferred', 'all_deferred', 'deferred_param';
48              
49             sub deferred {
50 3     3 1 362 my ( $plugin, $key, $value ) = @_;
51 3         33 my $app = $plugin->app;
52              
53 3         15 my $id = $plugin->_get_id;
54              
55             # message data is flat "dpd_$id" to avoid race condition with
56             # another session
57 3   50     136 my $data = $app->session->read( $plugin->session_key_prefix . $id ) || {};
58            
59             # set value or destructively retrieve it
60 3 50       3326 if ( defined $value ) {
61 3         15 $data->{$key} = $value;
62             }
63             else {
64             $value =
65             $app->request->var( $plugin->var_keep_key )
66             ? $data->{$key}
67 0 0       0 : delete $data->{$key};
68             }
69              
70             # store remaining data or clear it if no deferred messages are left
71 3 50       13 if ( keys %$data ) {
72 3         54 $app->session->write( $plugin->session_key_prefix . $id => $data );
73 3         364 $app->request->var( $plugin->var_key => $id );
74             }
75             else {
76 0         0 $app->session->delete( $plugin->session_key_prefix . $id );
77 0         0 $app->request->var( $plugin->var_key => undef );
78             }
79 3         52 return $value;
80             };
81              
82             sub all_deferred {
83 9     9 1 20 my $plugin = shift;
84 9         28 my $app = $plugin->app;
85              
86 9         30 my $id = $plugin->_get_id;
87 9   100     404 my $data = $plugin->app->session->read( $plugin->session_key_prefix . $id ) || {};
88              
89 9 100       714 unless ( $app->request->var( $plugin->var_keep_key ) ) {
90 8         296 $app->session->delete( $plugin->session_key_prefix . $id );
91 8         879 $app->request->var( $plugin->var_key, undef );
92             }
93 9         278 return $data;
94             }
95              
96             sub deferred_param {
97 3     3 1 17 my $plugin = shift;
98              
99 3         70 $plugin->app->request->var( $plugin->var_keep_key => 1 );
100              
101 3         106 return ( $plugin->params_key => $plugin->app->request->var( $plugin->var_key ) );
102             }
103              
104             # not crypto strong, but will be stored in session, which should be
105             sub _get_id {
106 12     12   27 my $plugin = shift;
107              
108 12   66     248 return $plugin->app->request->var( $plugin->var_key )
109             || sprintf( "%08d", int( rand(100_000_000) ) );
110             }
111              
112             sub BUILD {
113 2     2 0 9240 my $plugin = shift;
114              
115             $plugin->app->add_hook(
116             Dancer2::Core::Hook->new(
117             name => 'before_template',
118             code => sub {
119 9     9   14295 my $data = shift;
120 9         39 $data->{$plugin->template_key} = $plugin->all_deferred;
121             }
122             )
123 2         79 );
124              
125             $plugin->app->add_hook(
126             Dancer2::Core::Hook->new(
127             name => 'before',
128             code => sub {
129 11     11   246532 my $id = $plugin->app->request->params->{ $plugin->params_key };
130 11 100       462 $plugin->app->request->var( $plugin->var_key => $id )
131             if $id;
132             }
133             )
134 2         348949 );
135              
136             $plugin->app->add_hook(
137             Dancer2::Core::Hook->new(
138             name => 'after',
139             code => sub {
140 11     11   43248 my $response = shift;
141 11 100 100     247 if ( $plugin->app->request->var( $plugin->var_key )
142             && $response->status =~ /^3/ )
143             {
144 2         139 my $u = URI->new( $response->header("Location") );
145 2         257 $u->query_param( $plugin->deferred_param );
146 2         521 $response->header( "Location" => $u );
147             }
148             }
149             )
150 2         1048 );
151             };
152              
153             1;
154              
155              
156             # vim: ts=4 sts=4 sw=4 et:
157              
158             __END__
159              
160             =pod
161              
162             =encoding UTF-8
163              
164             =head1 NAME
165              
166             Dancer2::Plugin::Deferred - Defer messages or data across redirections
167              
168             =head1 VERSION
169              
170             version 0.008000
171              
172             =head1 SYNOPSIS
173              
174             use Dancer2::Plugin::Deferred;
175              
176             get '/defer' => sub {
177             deferred error => "Klaatu barada nikto";
178             redirect '/later';
179             };
180              
181             get '/later' => sub {
182             template 'later';
183             };
184              
185             # in template 'later.tt'
186             <% IF deferred.error %>
187             <div class="error"><% deferred.error %></div>
188             <% END %>
189              
190             =head1 DESCRIPTION
191              
192             This L<Dancer2> plugin provides a method for deferring a one-time message across
193             a redirect. It is similar to "flash" messages, but without the race conditions
194             that can result from multiple tabs in a browser or from AJAX requests. It is
195             similar in design to L<Catalyst::Plugin::StatusMessage>, but adapted for Dancer2.
196              
197             It works by creating a unique message ID within the session that holds deferred
198             data. The message ID is automatically added as a query parameter to redirection
199             requests. It's sort of like a session within a session, but tied to a request
200             rather than global to the browser. (It will even chain across multiple
201             redirects.)
202              
203             When a template is rendered, a pre-template hook retrieves the data and
204             deletes it from the session. Alternatively, the data can be retrieved manually
205             (which will also automatically delete the data.)
206              
207             Alternatively, the message ID parameters can be retrieved and used to
208             construct a hyperlink for a message to be retrieved later. In this case,
209             the message is preserved past the template hook. (The template should be
210             sure not to render the message if not desired.)
211              
212             =for Pod::Coverage method_names_here
213              
214             =head1 USAGE
215              
216             =head2 deferred
217              
218             deferred $key => $value;
219             $value = deferred $key; # also deletes $key
220              
221             This function works just like C<var> or C<session>, except that it lasts only
222             for the current request and across any redirects. Data is deleted if accessed.
223             If a key is set to an undefined value, the key is deleted from the deferred
224             data hash.
225              
226             =head2 all_deferred
227              
228             template 'index', { deferred => all_deferred };
229              
230             This function returns all the deferred data as a hash reference and deletes
231             the stored data. This is called automatically in the C<before_template_render>
232             hook, but is available if someone wants to have manual control.
233              
234             =head2 deferred_param
235              
236             template 'index' => { link => uri_for( '/other', { deferred_param } ) };
237              
238             This function returns the parameter key and value used to propagate the
239             message to another request. Using this function toggles the C<var_keep_key>
240             variable to true to ensure the message remains to be retrieved by the link.
241              
242             =head1 CONFIGURATION
243              
244             =for :list * C<var_key: dpdid> -- this is the key in the C<var> hash containing the message ID
245             * C<var_keep_key: dpd_keep> -- if this key in C<var> is true, retrieving values will not be destructive
246             * C<params_key: dpdid> -- this is the key in the C<params> hash containing the message ID
247             * C<session_key_prefix: dpd_> -- the message ID is appended to this prefix and used to store deferred data in the session
248             * C<template_key: deferred> -- this is the key to deferred data passed to the template
249              
250             =head1 SEE ALSO
251              
252             =for :list * L<Dancer2>
253             * L<Dancer::Plugin::FlashMessage>
254             * L<Dancer::Plugin::FlashNote>
255             * L<Catalyst::Plugin::StatusMessage>
256              
257             =head1 ACKNOWLEDGMENTS
258              
259             Thank you to mst for explaining why L<Catalyst::Plugin::StatusMessages> does
260             what it does and putting up with my dumb ideas along the way.
261              
262             =head1 AUTHORS
263              
264             =over 4
265              
266             =item *
267              
268             David Golden <dagolden@cpan.org>
269              
270             =item *
271              
272             Yanick Champoux <yanick@cpan.org>
273              
274             =item *
275              
276             Deluxaran <deluxaran@cpan.org>
277              
278             =back
279              
280             =head1 COPYRIGHT AND LICENSE
281              
282             This software is Copyright (c) 2020, 2018, 2016 by David Golden.
283              
284             This is free software, licensed under:
285              
286             The Apache License, Version 2.0, January 2004
287              
288             =cut