blib/lib/File/Tabular/Web.pm | |||
---|---|---|---|
Criterion | Covered | Total | % |
statement | 307 | 372 | 82.5 |
branch | 75 | 144 | 52.0 |
condition | 32 | 82 | 39.0 |
subroutine | 50 | 57 | 87.7 |
pod | 29 | 31 | 93.5 |
total | 493 | 686 | 71.8 |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | package File::Tabular::Web; # documentation at bottom of file | ||||||
2 | |||||||
3 | our $VERSION = "0.25"; | ||||||
4 | |||||||
5 | 2 | 2 | 73305 | use strict; | |||
2 | 9 | ||||||
2 | 52 | ||||||
6 | 2 | 2 | 8 | use warnings; | |||
2 | 3 | ||||||
2 | 36 | ||||||
7 | 2 | 2 | 6 | no warnings 'uninitialized'; | |||
2 | 2 | ||||||
2 | 44 | ||||||
8 | 2 | 2 | 684 | use locale; | |||
2 | 1025 | ||||||
2 | 7 | ||||||
9 | 2 | 2 | 782 | use Template; | |||
2 | 29670 | ||||||
2 | 53 | ||||||
10 | 2 | 2 | 710 | use POSIX 'strftime'; | |||
2 | 8958 | ||||||
2 | 9 | ||||||
11 | 2 | 2 | 2011 | use List::Util qw/min/; | |||
2 | 4 | ||||||
2 | 187 | ||||||
12 | 2 | 2 | 863 | use List::MoreUtils qw/uniq any all/; | |||
2 | 17773 | ||||||
2 | 9 | ||||||
13 | 2 | 2 | 2624 | use AppConfig qw/:argcount/; | |||
2 | 7886 | ||||||
2 | 157 | ||||||
14 | 2 | 2 | 836 | use File::Tabular 0.71; | |||
2 | 47546 | ||||||
2 | 55 | ||||||
15 | 2 | 2 | 15 | use Search::QueryParser; | |||
2 | 3 | ||||||
2 | 31 | ||||||
16 | 2 | 2 | 797 | use Try::Tiny; | |||
2 | 3008 | ||||||
2 | 100 | ||||||
17 | |||||||
18 | 2 | 2 | 13 | use parent 'Plack::Component'; | |||
2 | 5 | ||||||
2 | 13 | ||||||
19 | 2 | 2 | 7393 | use Plack::Request; | |||
2 | 80175 | ||||||
2 | 61 | ||||||
20 | 2 | 2 | 692 | use Plack::Response; | |||
2 | 2590 | ||||||
2 | 8080 | ||||||
21 | |||||||
22 | my %app_cache; | ||||||
23 | my %datafile_cache; # persistent data private to _cached_content | ||||||
24 | |||||||
25 | #====================================================================== | ||||||
26 | # MAIN ENTRY POINT | ||||||
27 | #====================================================================== | ||||||
28 | |||||||
29 | #---------------------------------------------------------------------- | ||||||
30 | sub call { # Plack request dispatcher (see L |
||||||
31 | #---------------------------------------------------------------------- | ||||||
32 | 9 | 9 | 1 | 29739 | my ($self, $env) = @_; | ||
33 | |||||||
34 | try { | ||||||
35 | # regular response | ||||||
36 | 9 | 9 | 283 | $self->_new($env); | |||
37 | 9 | 19 | $self->_dispatch_request; | ||||
38 | } | ||||||
39 | # in case of an exception | ||||||
40 | catch { | ||||||
41 | # try displaying through msg view.. | ||||||
42 | 0 | 0 | 0 | $self->{msg} = "ERROR : $_"; | |||
43 | 0 | 0 | $self->{view} = 'msg'; | ||||
44 | 0 | 0 | try {$self->display} | ||||
45 | catch { | ||||||
46 | # .. or else fallback with simple HTML page | ||||||
47 | 0 | 0 | my $res = Plack::Response->new(500); | ||||
48 | 0 | 0 | $res->body("$self->{msg}"); | ||||
49 | 0 | 0 | $res->content_type('text/html'); | ||||
50 | 0 | 0 | return $res->finalize; | ||||
51 | 0 | 0 | }; | ||||
52 | 9 | 59 | }; | ||||
53 | } | ||||||
54 | |||||||
55 | |||||||
56 | |||||||
57 | #---------------------------------------------------------------------- | ||||||
58 | sub handler : method { # for backwards compatibility : can be called | ||||||
59 | # as a modperl handler or from a CGI script. | ||||||
60 | # New apps should rather use the Plack interface. | ||||||
61 | #---------------------------------------------------------------------- | ||||||
62 | 0 | 0 | 1 | 0 | my ($class, $request) = @_; | ||
63 | |||||||
64 | 0 | 0 | my $self = $class->new; | ||||
65 | 0 | 0 | my $app = $self->to_app; | ||||
66 | |||||||
67 | 0 | 0 | 0 | 0 | if ($request && ref($request) =~ /^Apache2/) { | ||
68 | 0 | 0 | require Plack::Handler::Apache2; | ||||
69 | 0 | 0 | Plack::Handler::Apache2->call_app($request, $app); | ||||
70 | } | ||||||
71 | else { | ||||||
72 | 0 | 0 | require Plack::Handler::CGI; | ||||
73 | 0 | 0 | 0 | $ENV{QUERY_STRING} = $request if $request; | |||
74 | 0 | 0 | Plack::Handler::CGI->new->run($app); | ||||
75 | } | ||||||
76 | } | ||||||
77 | |||||||
78 | |||||||
79 | #====================================================================== | ||||||
80 | # METHODS FOR CREATING / INITIALIZING "APPLICATION" HASHREFS # | ||||||
81 | #====================================================================== | ||||||
82 | |||||||
83 | #---------------------------------------------------------------------- | ||||||
84 | sub _app_new { # creates a new application hashref (not an object) | ||||||
85 | #---------------------------------------------------------------------- | ||||||
86 | 1 | 1 | 3 | my ($self, $config_file) = @_; | |||
87 | 1 | 1 | my $app = {}; | ||||
88 | |||||||
89 | # application name and directory : defaults from the name of config file | ||||||
90 | 1 | 7 | @{$app}{qw(dir name)} = ($config_file =~ m[^(.+[/\\])(.+?)(?:\.[^.]*)$]); | ||||
1 | 3 | ||||||
91 | |||||||
92 | # read the config file | ||||||
93 | 1 | 3 | $app->{cfg} = $self->_app_read_config($config_file); | ||||
94 | |||||||
95 | 1 | 1 | my $tmp; # predeclare $tmp so that it can be used in "and" clauses | ||||
96 | |||||||
97 | # application directory | ||||||
98 | 1 | 50 | 9 | $tmp = $app->{cfg}->get('application_dir') and do { | |||
99 | 0 | 0 | $tmp =~ s{[^/\\]$}{/}; # add trailing "/" to dir if necessary | ||||
100 | 0 | 0 | $app->{dir} = $tmp; | ||||
101 | }; | ||||||
102 | |||||||
103 | # application name | ||||||
104 | 1 | 50 | 7 | $tmp = $app->{cfg}->get('application_name') and $app->{name} = $tmp; | |||
105 | |||||||
106 | # data file | ||||||
107 | 1 | 6 | $tmp = $app->{cfg}->get('application_data'); | ||||
108 | 1 | 33 | 8 | $app->{data_file} = $app->{dir} . ($tmp || "$app->{name}.txt"); | |||
109 | |||||||
110 | # application class | ||||||
111 | 1 | 3 | $app->{class} = ref $self; # initial value, may be overridden | ||||
112 | 1 | 50 | 3 | $tmp = $app->{cfg}->get('application_class') and do { | |||
113 | 0 | 0 | 0 | eval "require $tmp" or die $@; # dynamically load the requested code | |||
114 | 0 | 0 | 0 | $tmp->isa($app->{class}) or die "$tmp is not a $app->{class}"; | |||
115 | 0 | 0 | $app->{class} = $tmp; | ||||
116 | }; | ||||||
117 | |||||||
118 | 1 | 8 | return $app; | ||||
119 | } | ||||||
120 | |||||||
121 | #---------------------------------------------------------------------- | ||||||
122 | sub _app_read_config { # read configuration file through Appconfig | ||||||
123 | #---------------------------------------------------------------------- | ||||||
124 | 1 | 1 | 2 | my ($class, $config_file) = @_; | |||
125 | |||||||
126 | # error handler : die for all errors except "no such variable" | ||||||
127 | my $error_func = sub { | ||||||
128 | 187 | 187 | 4909 | my $fmt = shift; | |||
129 | 187 | 50 | 439 | die sprintf("AppConfig : $fmt\n", @_) | |||
130 | unless $fmt =~ /no such variable/; | ||||||
131 | 1 | 5 | }; | ||||
132 | |||||||
133 | # create AppConfig object (options documented in L |
||||||
134 | 1 | 11 | my $cfg = AppConfig->new({ | ||||
135 | CASE => 1, # case-sensitive | ||||||
136 | CREATE => 1, # accept dynamic creation of variables | ||||||
137 | ERROR => $error_func, # specific error handler | ||||||
138 | GLOBAL => {ARGCOUNT => ARGCOUNT_ONE},# default option for undefined vars | ||||||
139 | }); | ||||||
140 | |||||||
141 | # define specific options for some variables | ||||||
142 | # NOTE: fields_upload is not used here, but by F::T::Attachments | ||||||
143 | 1 | 138 | foreach my $hash_var (qw/fields_default fields_time fields_upload/) { | ||||
144 | 3 | 147 | $cfg->define($hash_var => {ARGCOUNT => ARGCOUNT_HASH}); | ||||
145 | } | ||||||
146 | 1 | 51 | $cfg->define(fieldSep => {DEFAULT => "|"}); | ||||
147 | |||||||
148 | # read the configuration file | ||||||
149 | 1 | 53 | $cfg->file($config_file); # or croak "AppConfig: open $config_file: $^E"; | ||||
150 | # BUG : AppConfig does not return any error code if ->file(..) fails !! | ||||||
151 | |||||||
152 | 1 | 4532 | return $cfg; | ||||
153 | } | ||||||
154 | |||||||
155 | |||||||
156 | |||||||
157 | #---------------------------------------------------------------------- | ||||||
158 | sub app_initialize { | ||||||
159 | #---------------------------------------------------------------------- | ||||||
160 | # NOTE: this method is called after instance creation and therefore | ||||||
161 | # takes into account the subclass which may have been given in the | ||||||
162 | # config file. | ||||||
163 | |||||||
164 | 1 | 1 | 1 | 3 | my ($self) = @_; | ||
165 | 1 | 2 | my $app = $self->{app}; | ||||
166 | 1 | 4 | my ($last_subdir) = ($app->{dir} =~ m[^.*[/\\](.+)[/\\]?$]); | ||||
167 | my $default = $self->{template_root} | ||||||
168 | 1 | 33 | 5 | || $self->app_tmpl_default_dir; | |||
169 | |||||||
170 | # directories to search for Templates | ||||||
171 | 5 | 63 | my @tmpl_dirs = grep {-d} ($app->{cfg}->get("template_dir"), | ||||
172 | $app->{dir}, | ||||||
173 | 1 | 4 | "$default/$last_subdir", | ||||
174 | $default, | ||||||
175 | "$default/default", | ||||||
176 | ); | ||||||
177 | |||||||
178 | # initialize template toolkit object | ||||||
179 | 1 | 50 | 9 | $app->{tmpl} = Template->new({ | |||
180 | INCLUDE_PATH => \@tmpl_dirs, | ||||||
181 | FILTERS => $self->app_tmpl_filters, | ||||||
182 | EVAL_PERL => 1, | ||||||
183 | }) | ||||||
184 | or die Template->error; | ||||||
185 | |||||||
186 | # special fields : time of last modif, author of last modif | ||||||
187 | 1 | 16116 | $app->{time_fields} = $app->{cfg}->get('fields_time'); | ||||
188 | 1 | 36 | $app->{user_field} = $app->{cfg}->get('fields_user'); | ||||
189 | } | ||||||
190 | |||||||
191 | |||||||
192 | #---------------------------------------------------------------------- | ||||||
193 | sub app_tmpl_default_dir { # default; override in subclasses | ||||||
194 | #---------------------------------------------------------------------- | ||||||
195 | 1 | 1 | 1 | 2 | my ($self) = @_; | ||
196 | |||||||
197 | 1 | 3 | return "$self->{app_root}/../lib/tmpl/ftw"; | ||||
198 | } | ||||||
199 | |||||||
200 | |||||||
201 | #---------------------------------------------------------------------- | ||||||
202 | sub app_tmpl_filters { # default; override in subclasses | ||||||
203 | #---------------------------------------------------------------------- | ||||||
204 | 1 | 1 | 1 | 2 | my ($self) = @_; | ||
205 | 1 | 3 | my $cfg = $self->{app}{cfg}; | ||||
206 | 1 | 4 | my $ini_marker = $cfg->get('preMatch'); | ||||
207 | 1 | 6 | my $end_marker = $cfg->get('postMatch'); | ||||
208 | |||||||
209 | # no highlight filters without pre/postMatch | ||||||
210 | 1 | 50 | 33 | 23 | $ini_marker && $end_marker or return {}; | ||
211 | |||||||
212 | 0 | 0 | 0 | my $HL_class = $cfg->get('highlightClass') || "HL"; | |||
213 | 0 | 0 | my $regex = qr/\Q$ini_marker\E(.*?)\Q$end_marker\E/s; | ||||
214 | |||||||
215 | my $filters = { | ||||||
216 | highlight => sub { | ||||||
217 | 0 | 0 | 0 | my $text = shift; | |||
218 | 0 | 0 | $text =~ s[$regex][$1]g; | ||||
219 | 0 | 0 | return $text; | ||||
220 | }, | ||||||
221 | unhighlight => sub { | ||||||
222 | 0 | 0 | 0 | my $text = shift; | |||
223 | 0 | 0 | $text =~ s[$regex][$1]g; | ||||
224 | 0 | 0 | return $text; | ||||
225 | } | ||||||
226 | 0 | 0 | }; | ||||
227 | 0 | 0 | return $filters; | ||||
228 | } | ||||||
229 | |||||||
230 | |||||||
231 | |||||||
232 | |||||||
233 | #---------------------------------------------------------------------- | ||||||
234 | sub app_phases_definitions { | ||||||
235 | #---------------------------------------------------------------------- | ||||||
236 | 9 | 9 | 0 | 9 | my $class = shift; | ||
237 | |||||||
238 | # PHASES DEFINITIONS TABLE : each single letter is expanded into | ||||||
239 | # optional methods for data preparation, data operation, and view. | ||||||
240 | # It is also possible to differentiate between GET and POST requests. | ||||||
241 | return ( | ||||||
242 | |||||||
243 | 9 | 109 | A => # prepare a new record for adding | ||||
244 | {GET => {pre => 'empty_record', view => 'modif'}, | ||||||
245 | POST => {pre => 'empty_record', op => 'update' } }, | ||||||
246 | |||||||
247 | D => # delete record | ||||||
248 | {pre => 'search_key', op => 'delete' }, | ||||||
249 | |||||||
250 | H => # display home page | ||||||
251 | { view => 'home' }, | ||||||
252 | |||||||
253 | L => # display "long" view of one single record | ||||||
254 | {pre => 'search_key', view => 'long' }, | ||||||
255 | |||||||
256 | M => # modif: GET displays the form, POST performs the update | ||||||
257 | {GET => {pre => 'search_key', view => 'modif'}, | ||||||
258 | POST => {pre => 'search_key', op => 'update' } }, | ||||||
259 | |||||||
260 | S => # search and display "short" view | ||||||
261 | {pre => 'search', op => 'sort_and_slice', view => 'short' }, | ||||||
262 | |||||||
263 | X => # display all records in "download view" (mnemonic: eXtract) | ||||||
264 | {pre => 'prepare_download', view => 'download'}, | ||||||
265 | |||||||
266 | ); | ||||||
267 | } | ||||||
268 | |||||||
269 | |||||||
270 | |||||||
271 | #====================================================================== | ||||||
272 | # METHODS FOR INSTANCE CREATION / INITIALIZATION # | ||||||
273 | #====================================================================== | ||||||
274 | |||||||
275 | |||||||
276 | |||||||
277 | #---------------------------------------------------------------------- | ||||||
278 | sub _new { # expands and re-blesses the File::Tabular::Web instance | ||||||
279 | #---------------------------------------------------------------------- | ||||||
280 | 9 | 9 | 16 | my ($self, $env) = @_; | |||
281 | |||||||
282 | 9 | 46 | my $req = Plack::Request->new($env); | ||||
283 | 9 | 50 | 82 | my $path_info = $req->path_info | |||
284 | or die __PACKAGE__ . ": no application (PATH_INFO is empty)"; | ||||||
285 | |||||||
286 | # add some fields within object | ||||||
287 | 9 | 166 | $self->{req} = $req; | ||||
288 | 9 | 50 | 164 | $self->{user} = $req->user || "Anonymous"; | |||
289 | 9 | 65 | $self->{url} = $req->base . $path_info; | ||||
290 | 9 | 1422 | $self->{method} = $req->method; | ||||
291 | 9 | 49 | $self->{msg} = ""; | ||||
292 | |||||||
293 | # are we running under mod_perl ? if so, have a handle to the Rec object. | ||||||
294 | 9 | 12 | my $mod_perl = do {my $input = $self->{req}->env->{'psgi.input'}; | ||||
9 | 17 | ||||||
295 | 9 | 50 | 78 | $input->isa('Apache2::RequestRec') && $input}; | |||
296 | |||||||
297 | # find the app root, by default equal to server document root | ||||||
298 | $self->{app_root} | ||||||
299 | ||= $mod_perl && $mod_perl->document_root | ||||||
300 | || $env->{CONTEXT_DOCUMENT_ROOT} # new in Apache2.4 | ||||||
301 | 9 | 33 | 32 | || $env->{DOCUMENT_ROOT}; # standard CGI protocol | |||
66 | |||||||
302 | |||||||
303 | # find application file | ||||||
304 | my $app_file = $mod_perl && $mod_perl->filename | ||||||
305 | || $env->{SCRIPT_FILENAME} | ||||||
306 | || $env->{PATH_TRANSLATED} | ||||||
307 | 9 | 33 | 85 | || $self->{app_root} . $req->script_name . $path_info; | |||
308 | |||||||
309 | # compare modification time with cache; load app if necessary | ||||||
310 | 9 | 50 | 204 | my $mtime = (stat $app_file)[9] | |||
311 | or die "couldn't stat app file for $path_info"; | ||||||
312 | 9 | 26 | my $cache_entry = $app_cache{$app_file}; | ||||
313 | 9 | 66 | 36 | my $app_initialized = $cache_entry && $cache_entry->{mtime} == $mtime; | |||
314 | 9 | 100 | 13 | if (not $app_initialized) { | |||
315 | 1 | 3 | $app_cache{$app_file} = {mtime => $mtime, | ||||
316 | content => $self->_app_new($app_file)}; | ||||||
317 | } | ||||||
318 | 9 | 23 | $self->{app} = $app_cache{$app_file}->{content}; | ||||
319 | 9 | 18 | $self->{cfg} = $self->{app}{cfg}; # shortcut | ||||
320 | |||||||
321 | # rebless the request obj into the application class, initialize and return | ||||||
322 | 9 | 31 | bless $self, $self->{app}{class}; | ||||
323 | |||||||
324 | # now that we have the proper class, initialize the app if needed | ||||||
325 | 9 | 100 | 21 | $self->app_initialize unless $app_initialized; | |||
326 | |||||||
327 | # initialize the request obj | ||||||
328 | 9 | 26 | $self->initialize; | ||||
329 | |||||||
330 | 9 | 15 | return $self; | ||||
331 | } | ||||||
332 | |||||||
333 | |||||||
334 | #---------------------------------------------------------------------- | ||||||
335 | sub initialize { # setup params from config and/or CGI params | ||||||
336 | #---------------------------------------------------------------------- | ||||||
337 | 9 | 9 | 1 | 14 | my $self = shift; | ||
338 | |||||||
339 | # default values | ||||||
340 | 9 | 50 | 20 | $self->{max} = $self->param('max') || 500; | |||
341 | 9 | 50 | 17 | $self->{count} = $self->param('count') || 50; | |||
342 | 9 | 33 | 210 | $self->{orderBy} = $self->param('orderBy') | |||
343 | || $self->param('sortBy'); # for backwards compatibility | ||||||
344 | |||||||
345 | 9 | 43 | return $self; | ||||
346 | } | ||||||
347 | |||||||
348 | |||||||
349 | #---------------------------------------------------------------------- | ||||||
350 | sub _setup_phases { # decide about next phases | ||||||
351 | #---------------------------------------------------------------------- | ||||||
352 | 9 | 9 | 10 | my $self = shift; | |||
353 | |||||||
354 | # get all phases definitions (expansions of single-letter param) | ||||||
355 | 9 | 21 | my %request_phases = $self->app_phases_definitions; | ||||
356 | |||||||
357 | # find out which single-letter was requested | ||||||
358 | 9 | 21 | my @letters = grep {defined $request_phases{$_}} uniq $self->param; | ||||
8 | 22 | ||||||
359 | |||||||
360 | # cannot ask for several operations at once | ||||||
361 | 9 | 50 | 25 | @letters <= 1 or die "conflict in request: " . join(" / ", @letters); | |||
362 | |||||||
363 | # by default : homepage | ||||||
364 | 9 | 100 | 20 | my $letter = $letters[0] || "H"; | |||
365 | |||||||
366 | # argument passed to operation | ||||||
367 | 9 | 17 | my $letter_arg = $self->param($letters[0]); | ||||
368 | |||||||
369 | # special case : with POST requests, we want to also consider the ?A or ?M=.. | ||||||
370 | # or ?D=.. from the query string | ||||||
371 | 9 | 50 | 66 | 23 | if (not @letters and $self->{method} eq 'POST') { | ||
372 | LETTER: | ||||||
373 | 0 | 0 | for my $try_letter (qw/A M D/) { | ||||
374 | 0 | 0 | $letter_arg = $self->{req}->query_parameters->get($try_letter); | ||||
375 | 0 | 0 | 0 | 0 | $letter = $try_letter and last LETTER if defined($letter_arg); | ||
376 | } | ||||||
377 | } | ||||||
378 | |||||||
379 | # setup info in $self according to the chosen letter | ||||||
380 | 9 | 17 | my $entry = $request_phases{$letter}; | ||||
381 | 9 | 66 | 30 | my $phases = $entry->{$self->{method}} || $entry; | |||
382 | 9 | 66 | 16 | $self->{view} = $self->param('V') || $phases->{view}; | |||
383 | 9 | 53 | $self->{pre} = $phases->{pre}; | ||||
384 | 9 | 15 | $self->{op} = $phases->{op}; | ||||
385 | |||||||
386 | 9 | 45 | return $letter_arg; | ||||
387 | } | ||||||
388 | |||||||
389 | |||||||
390 | #---------------------------------------------------------------------- | ||||||
391 | sub open_data { # open File::Tabular object on data file | ||||||
392 | #---------------------------------------------------------------------- | ||||||
393 | 9 | 9 | 1 | 12 | my $self = shift; | ||
394 | |||||||
395 | # parameters for opening the file | ||||||
396 | 9 | 16 | my $open_src = $self->{app}{data_file}; | ||||
397 | 9 | 50 | 145 | my $mtime = (stat $open_src)[9] or die "couldn't stat $open_src"; | |||
398 | |||||||
399 | # text version of modified time for templates | ||||||
400 | 9 | 50 | 60 | if (my $fmt = $self->{cfg}->get('application_mtime')) { | |||
401 | 0 | 0 | $self->{mtime} = strftime($fmt, localtime($mtime)); | ||||
402 | } | ||||||
403 | |||||||
404 | 9 | 100 | 58 | my $open_mode = ($self->{op} =~ /delete|update/) ? "+<" : "<"; | |||
405 | |||||||
406 | # application option : use in-memory cache only for read operations | ||||||
407 | 9 | 50 | 33 | 28 | if ($self->{cfg}->get('application_useFileCache') | ||
408 | && $open_mode eq '<') { | ||||||
409 | 0 | 0 | my $cache_entry = $datafile_cache{$open_src}; | ||||
410 | 0 | 0 | 0 | 0 | unless ($cache_entry && $cache_entry->{mtime} == $mtime) { | ||
411 | 0 | 0 | 0 | open my $fh, $open_src or die "open $open_src : $^E"; | |||
412 | 0 | 0 | local $/ = undef; | ||||
413 | 0 | 0 | my $content = <$fh>; # slurps the whole file into memory | ||||
414 | 0 | 0 | close $fh; | ||||
415 | 0 | 0 | $datafile_cache{$open_src} = {mtime => $mtime, | ||||
416 | content => \$content }; | ||||||
417 | } | ||||||
418 | 0 | 0 | $open_src = $cache_entry->{content}; # ref to in-memory content | ||||
419 | } | ||||||
420 | |||||||
421 | # set up options for creating File::Tabular object | ||||||
422 | 9 | 30 | my %options; | ||||
423 | 9 | 13 | foreach (qw/preMatch postMatch avoidMatchKey fieldSep recordSep/) { | ||||
424 | 45 | 335 | $options{$_} = $self->{cfg}->get($_); | ||||
425 | } | ||||||
426 | 9 | 42 | $options{autoNumField} = $self->{cfg}->get('fields_autoNum'); | ||||
427 | 9 | 207 | my $jFile = $self->{cfg}->get('journal'); | ||||
428 | 9 | 50 | 30 | $options{journal} = "$self->{app}{dir}$jFile" if $jFile; | |||
429 | |||||||
430 | # create File::Tabular object | ||||||
431 | 9 | 42 | $self->{data} = new File::Tabular($open_mode, $open_src, \%options); | ||||
432 | } | ||||||
433 | |||||||
434 | |||||||
435 | #====================================================================== | ||||||
436 | # PUBLIC METHODS CALLABLE FROM TEMPLATES # | ||||||
437 | #====================================================================== | ||||||
438 | |||||||
439 | |||||||
440 | #---------------------------------------------------------------------- | ||||||
441 | sub param { # Encapsulates access to the lower layer param() method, and | ||||||
442 | # merge with config information. | ||||||
443 | #---------------------------------------------------------------------- | ||||||
444 | 71 | 71 | 1 | 156 | my ($self, $param_name) = @_; # $param_name might be undef | ||
445 | |||||||
446 | # Like old CGI->param(), we only return body parameters on POST | ||||||
447 | # requests (ignoring query parameters). | ||||||
448 | my $params = $self->{method} eq 'POST' ? $self->{req}->body_parameters | ||||||
449 | 71 | 100 | 196 | : $self->{req}->parameters; | |||
450 | |||||||
451 | # if no arg, just return the list of param names | ||||||
452 | 71 | 100 | 1631 | return keys %$params if not defined $param_name; | |||
453 | |||||||
454 | # otherwise, first check in "fixed" section in config | ||||||
455 | 61 | 220 | my $val = $self->{cfg}->get("fixed_$param_name"); | ||||
456 | 61 | 100 | 462 | return $val if $val; | |||
457 | |||||||
458 | # then check in parameters to this request (flattened into a scalar) | ||||||
459 | 52 | 94 | my @vals = $params->get_all($param_name); | ||||
460 | 52 | 100 | 497 | if (@vals) { | |||
461 | 8 | 15 | $val = join(' ', @vals); # join multiple values | ||||
462 | 8 | 19 | $val =~ s/^\s+//; # remove initial spaces | ||||
463 | 8 | 12 | $val =~ s/\s+$//; # remove final spaces | ||||
464 | 8 | 19 | return $val; | ||||
465 | } | ||||||
466 | |||||||
467 | # finally check in "default" section in config | ||||||
468 | 44 | 141 | return $self->{cfg}->get("default_$param_name"); | ||||
469 | } | ||||||
470 | |||||||
471 | |||||||
472 | #---------------------------------------------------------------------- | ||||||
473 | sub can_do { # can be called from templates; $record is optional | ||||||
474 | #---------------------------------------------------------------------- | ||||||
475 | 13 | 13 | 1 | 19 | my ($self, $action, $record) = @_; | ||
476 | |||||||
477 | 13 | 61 | my $allow = $self->{cfg}->get("permissions_$action"); | ||||
478 | 13 | 133 | my $deny = $self->{cfg}->get("permissions_no_$action"); | ||||
479 | |||||||
480 | # some permissions are granted by default to everybody | ||||||
481 | 13 | 100 | 50 | 89 | $allow ||= "*" if $action =~ /^(read|search|download)$/; | ||
482 | |||||||
483 | 13 | 19 | for ($allow, $deny) { | ||||
484 | 26 | 100 | 44 | $_ or next; # no acl list => nothing to do | |||
485 | $_ = $self->user_match($_) # if acl list matches user name | ||||||
486 | ||( /\$(\S+)\b/i # or if acl list contains a field name ... | ||||||
487 | && defined($record) # ... and got a specific record | ||||||
488 | && defined($record->{$1}) # ... and field is defined | ||||||
489 | 13 | 33 | 24 | && $self->user_match($record->{$1})); # ... and field content matches | |||
490 | } | ||||||
491 | |||||||
492 | 13 | 33 | 47 | return $allow && !$deny; | |||
493 | } | ||||||
494 | |||||||
495 | |||||||
496 | |||||||
497 | #====================================================================== | ||||||
498 | # REQUEST HANDLING : GENERAL METHODS # | ||||||
499 | #====================================================================== | ||||||
500 | |||||||
501 | |||||||
502 | #---------------------------------------------------------------------- | ||||||
503 | sub _dispatch_request { # go through phases and choose appropriate handling | ||||||
504 | #---------------------------------------------------------------------- | ||||||
505 | 9 | 9 | 11 | my $self = shift; | |||
506 | 9 | 10 | my $method; | ||||
507 | |||||||
508 | # determine phases from single-letter param; keep arg value from that letter | ||||||
509 | 9 | 18 | my $letter_arg = $self->_setup_phases; | ||||
510 | |||||||
511 | # data access | ||||||
512 | 9 | 21 | $self->open_data; | ||||
513 | |||||||
514 | # data preparation : invoke method if any, passing the arg saved above | ||||||
515 | 9 | 100 | 3081 | $method = $self->{pre} and $self->$method($letter_arg); | |||
516 | |||||||
517 | # data manipulation : invoke method if any | ||||||
518 | 9 | 100 | 41 | $method = $self->{op} and $self->$method; | |||
519 | |||||||
520 | # force message view if there is a message | ||||||
521 | 9 | 100 | 40 | $self->{view} = 'msg' if $self->{msg}; | |||
522 | |||||||
523 | # print the output | ||||||
524 | 9 | 18 | $self->display; | ||||
525 | } | ||||||
526 | |||||||
527 | |||||||
528 | #---------------------------------------------------------------------- | ||||||
529 | sub display { # display results in the requested view | ||||||
530 | #---------------------------------------------------------------------- | ||||||
531 | 9 | 9 | 1 | 14 | my ($self) = @_; | ||
532 | 9 | 50 | 30 | my $view = $self->{view} or die "display : no view"; | |||
533 | |||||||
534 | |||||||
535 | # name of the template for this view | ||||||
536 | 9 | 50 | 28 | my $default_tmpl = $view eq 'download' ? "download.tt" | |||
537 | : "$self->{app}{name}_$view.tt"; | ||||||
538 | 9 | 33 | 37 | my $tmpl_name = $self->{cfg}->get("template_$view") || $default_tmpl; | |||
539 | |||||||
540 | # override template toolkit's failsafe counter for while loops | ||||||
541 | # in case of download action | ||||||
542 | 9 | 50 | 302 | local $Template::Directive::WHILE_MAX = 50000 if $view eq 'download'; | |||
543 | |||||||
544 | # call that template | ||||||
545 | 9 | 10 | my $body; | ||||
546 | 9 | 25 | my $vars = {self => $self, found => $self->{results}}; | ||||
547 | $self->{app}{tmpl}->process($tmpl_name, $vars, \$body) | ||||||
548 | 9 | 50 | 29 | or die $self->{app}{tmpl}->error(); | |||
549 | |||||||
550 | # generate Plack response | ||||||
551 | 9 | 60850 | my $res = Plack::Response->new(200); | ||||
552 | $res->headers({"Content-type" => "text/html", | ||||||
553 | "Content-length" => length($body), | ||||||
554 | "Last-modified" => $self->{data}->stat->{mtime}, | ||||||
555 | 9 | 158 | "Expires" => 0}); | ||||
556 | 9 | 1213 | $res->body($body); | ||||
557 | |||||||
558 | 9 | 47 | return $res->finalize; | ||||
559 | } | ||||||
560 | |||||||
561 | |||||||
562 | #====================================================================== | ||||||
563 | # REQUEST HANDLING : SEARCH METHODS # | ||||||
564 | #====================================================================== | ||||||
565 | |||||||
566 | |||||||
567 | #---------------------------------------------------------------------- | ||||||
568 | sub search_key { # search by record key | ||||||
569 | #---------------------------------------------------------------------- | ||||||
570 | 4 | 4 | 1 | 23 | my ($self, $key) = @_; | ||
571 | 4 | 50 | 13 | $self->can_do("read") or | |||
572 | die "no 'read' permission for $self->{user}"; | ||||||
573 | 4 | 50 | 12 | $key or die "search_key : no key!"; | |||
574 | 4 | 8 | $key =~ s/<.*?>//g; # remove any markup (maybe inserted by pre/postMatch) | ||||
575 | |||||||
576 | 4 | 8 | my $query = "K_E_Y:$key"; | ||||
577 | |||||||
578 | 4 | 14 | my ($records, $lineNumbers) = $self->{data}->fetchall(where => $query); | ||||
579 | 4 | 7711 | my $count = @$records; | ||||
580 | 4 | 10 | $self->{results}{count} = $count; | ||||
581 | 4 | 19 | $self->{results}{records} = $records; | ||||
582 | 4 | 8 | $self->{results}{lineNumbers} = $lineNumbers; | ||||
583 | } | ||||||
584 | |||||||
585 | |||||||
586 | |||||||
587 | #---------------------------------------------------------------------- | ||||||
588 | sub search { # search records and display results | ||||||
589 | #---------------------------------------------------------------------- | ||||||
590 | 3 | 3 | 1 | 8 | my ($self, $search_string) = @_; | ||
591 | |||||||
592 | # check permissions | ||||||
593 | 3 | 50 | 8 | $self->can_do('search') or | |||
594 | die "no 'search' permission for $self->{user}"; | ||||||
595 | |||||||
596 | 3 | 8 | $self->{search_string_orig} = $search_string; | ||||
597 | 3 | 9 | $self->before_search; | ||||
598 | 3 | 8 | $self->log_search; | ||||
599 | |||||||
600 | 3 | 6 | $self->{results}{count} = 0; | ||||
601 | 3 | 16 | $self->{results}{records} = []; | ||||
602 | 3 | 6 | $self->{results}{lineNumbers} = []; | ||||
603 | |||||||
604 | 3 | 50 | 12 | return if $self->{search_string} =~ /^\s*$/; # no query, no results | |||
605 | |||||||
606 | 3 | 15 | my $qp = new Search::QueryParser; | ||||
607 | |||||||
608 | # compile query with an implicit '+' prefix in front of every item | ||||||
609 | 3 | 50 | 62 | my $parsedQ = $qp->parse($self->{search_string}, '+') or | |||
610 | die "error parsing query : $self->{search_string}"; | ||||||
611 | |||||||
612 | 3 | 341 | my $filter; | ||||
613 | |||||||
614 | 3 | 50 | 4 | eval {$filter = $self->{data}->compileFilter($parsedQ);} or | |||
3 | 12 | ||||||
615 | die("error in query : $@ ," . $qp->unparse($parsedQ) | ||||||
616 | . " ($self->{search_string})"); | ||||||
617 | |||||||
618 | # perform the search | ||||||
619 | 3 | 8838 | @{$self->{results}}{qw(records lineNumbers)} = | ||||
620 | 3 | 560 | $self->{data}->fetchall(where => $filter); | ||||
621 | 3 | 5 | $self->{results}{count} = @{$self->{results}{records}}; | ||||
3 | 8 | ||||||
622 | |||||||
623 | # VERY CHEAP way of generating regex for highlighting results | ||||||
624 | 3 | 11 | my @words_queried = uniq(grep {length($_)>2} $self->words_queried); | ||||
2 | 15 | ||||||
625 | 3 | 33 | $self->{results}{wordsQueried} = join "|", @words_queried; | ||||
626 | } | ||||||
627 | |||||||
628 | |||||||
629 | #---------------------------------------------------------------------- | ||||||
630 | sub before_search { | ||||||
631 | #---------------------------------------------------------------------- | ||||||
632 | 3 | 3 | 1 | 5 | my ($self) = @_; | ||
633 | 3 | 50 | 13 | $self->{search_string} = $self->{search_string_orig} || ""; | |||
634 | 3 | 6 | return $self; | ||||
635 | } | ||||||
636 | |||||||
637 | |||||||
638 | |||||||
639 | #---------------------------------------------------------------------- | ||||||
640 | sub sort_and_slice { # sort results, then just keep the desired slice | ||||||
641 | #---------------------------------------------------------------------- | ||||||
642 | 3 | 3 | 1 | 6 | my $self = shift; | ||
643 | |||||||
644 | 3 | 8 | delete $self->{results}{lineNumbers}; # not going to use those | ||||
645 | |||||||
646 | # sort results | ||||||
647 | 3 | 50 | 8 | if ($self->{orderBy}) { | |||
648 | 0 | 0 | 0 | eval { | |||
649 | 0 | 0 | my $cmpfunc = $self->{data}->ht->cmp($self->{orderBy}); | ||||
650 | 0 | 0 | $self->{results}{records} = [sort $cmpfunc @{$self->{results}{records}}]; | ||||
0 | 0 | ||||||
651 | } | ||||||
652 | or die "orderBy : $@"; | ||||||
653 | } | ||||||
654 | |||||||
655 | # restrict to the desired slice | ||||||
656 | 3 | 66 | 7 | my $start_record = $self->param('start') || ($self->{results}{count} ? 1 : 0); | |||
657 | my $end_record = min($start_record + $self->{count} - 1, | ||||||
658 | 3 | 37 | $self->{results}{count}); | ||||
659 | 3 | 50 | 7 | die "illegal start value : $start_record" if $start_record > $end_record; | |||
660 | $self->{results}{records} = $self->{results}{count} | ||||||
661 | 3 | 100 | 12 | ? [ @{$self->{results}{records}}[$start_record-1 ... $end_record-1] ] | |||
2 | 7 | ||||||
662 | : []; | ||||||
663 | |||||||
664 | # check read permission on records (looping over records only if necessary) | ||||||
665 | my $must_loop_on_records # true if permission depends on record fields | ||||||
666 | = (($self->{cfg}->get("permissions_read") || "") =~ /\$/) | ||||||
667 | 3 | 33 | 25 | || (($self->{cfg}->get("permissions_no_read") || "") =~ /\$/); | |||
668 | 3 | 50 | 26 | if ($must_loop_on_records) { | |||
669 | 0 | 0 | foreach my $record (@{$self->{results}{records}}) { | ||||
0 | 0 | ||||||
670 | 0 | 0 | 0 | $self->can_do('read', $record) | |||
671 | or die "no 'read' permission for $self->{user}"; | ||||||
672 | } | ||||||
673 | } | ||||||
674 | else { # no need for a loop | ||||||
675 | 3 | 50 | 8 | $self->can_do('read') | |||
676 | or die "no 'read' permission for $self->{user}"; | ||||||
677 | } | ||||||
678 | |||||||
679 | # for user display : record numbers start with 1, not 0 | ||||||
680 | 3 | 9 | $self->{results}{start} = $start_record; | ||||
681 | 3 | 4 | $self->{results}{end} = $end_record; | ||||
682 | |||||||
683 | |||||||
684 | # links to previous/next slice | ||||||
685 | 3 | 6 | my $prev_idx = $start_record - $self->{count}; | ||||
686 | 3 | 50 | 6 | $prev_idx = 1 if $prev_idx < 1; | |||
687 | 3 | 50 | 7 | $self->{results}{prev_link} = $self->_url_for_next_slice($prev_idx) | |||
688 | if $start_record > 1; | ||||||
689 | 3 | 4 | my $next_idx = $start_record + $self->{count}; | ||||
690 | $self->{results}{next_link} = $self->_url_for_next_slice($next_idx) | ||||||
691 | 3 | 100 | 22 | if $next_idx <= $self->{results}{count}; | |||
692 | } | ||||||
693 | |||||||
694 | |||||||
695 | #---------------------------------------------------------------------- | ||||||
696 | sub _url_for_next_slice { | ||||||
697 | #---------------------------------------------------------------------- | ||||||
698 | 1 | 1 | 3 | my ($self, $start) = @_; | |||
699 | |||||||
700 | 1 | 4 | my $url = "?" . join "&", $self->params_for_next_slice($start); | ||||
701 | |||||||
702 | # uri encoding | ||||||
703 | 1 | 3 | $url =~ s/([^;\/?:@&=\$,A-Z0-9\-_.!~*'() ])/sprintf("%%%02X", ord($1))/ige; | ||||
0 | 0 | ||||||
704 | |||||||
705 | 1 | 3 | return $url; | ||||
706 | } | ||||||
707 | |||||||
708 | |||||||
709 | #---------------------------------------------------------------------- | ||||||
710 | sub params_for_next_slice { | ||||||
711 | #---------------------------------------------------------------------- | ||||||
712 | 1 | 1 | 1 | 3 | my ($self, $start) = @_; | ||
713 | |||||||
714 | # need request object to invoke native param() method | ||||||
715 | 1 | 2 | my $req = $self->{req}; | ||||
716 | |||||||
717 | 1 | 4 | my @params = ("S=$self->{search_string_orig}", "start=$start"); | ||||
718 | 1 | 50 | 3 | push @params, "orderBy=$self->{orderBy}" if $req->parameters->{orderBy}; | |||
719 | 1 | 50 | 9 | push @params, "count=$self->{count}" if $req->parameters->{count}; | |||
720 | |||||||
721 | 1 | 8 | return @params; | ||||
722 | } | ||||||
723 | |||||||
724 | |||||||
725 | #---------------------------------------------------------------------- | ||||||
726 | sub words_queried { | ||||||
727 | #---------------------------------------------------------------------- | ||||||
728 | 3 | 3 | 1 | 7 | my $self = shift; | ||
729 | 3 | 16 | return ($self->{search_string_orig} =~ m([\w/]+)g); | ||||
730 | } | ||||||
731 | |||||||
732 | |||||||
733 | |||||||
734 | #---------------------------------------------------------------------- | ||||||
735 | sub log_search { | ||||||
736 | #---------------------------------------------------------------------- | ||||||
737 | 3 | 3 | 0 | 5 | my $self = shift; | ||
738 | 3 | 50 | 6 | return if not $self->{logger}; | |||
739 | |||||||
740 | 0 | 0 | my $msg = "[$self->{search_string}] $self->{user}"; | ||||
741 | 0 | 0 | $self->{logger}->info($msg); | ||||
742 | } | ||||||
743 | |||||||
744 | |||||||
745 | #====================================================================== | ||||||
746 | # REQUEST HANDLING : UPDATE METHODS # | ||||||
747 | #====================================================================== | ||||||
748 | |||||||
749 | |||||||
750 | #---------------------------------------------------------------------- | ||||||
751 | sub empty_record { # to be displayed in "modif" view (when adding) | ||||||
752 | #---------------------------------------------------------------------- | ||||||
753 | 1 | 1 | 1 | 2 | my ($self) = @_; | ||
754 | |||||||
755 | 1 | 50 | 4 | $self->can_do("add") or | |||
756 | die "no 'add' permission for $self->{user}"; | ||||||
757 | |||||||
758 | # build a record and insert default values | ||||||
759 | 1 | 4 | my $record = $self->{data}->ht->new; | ||||
760 | 1 | 16 | my $defaults = $self->{cfg}->get("fields_default"); | ||||
761 | 1 | 50 | 24 | if (my $auto_num = $self->{data}{autoNumField}) { | |||
762 | 1 | 33 | 3 | $defaults->{$auto_num} ||= $self->{data}{autoNumChar}; | |||
763 | } | ||||||
764 | 1 | 4 | $record->{$_} = $defaults->{$_} foreach $self->{data}->headers; | ||||
765 | |||||||
766 | 1 | 85 | $self->{results} = {count => 1, records => [$record], lineNumbers => [-1]}; | ||||
767 | } | ||||||
768 | |||||||
769 | |||||||
770 | #---------------------------------------------------------------------- | ||||||
771 | sub update { | ||||||
772 | #---------------------------------------------------------------------- | ||||||
773 | 1 | 1 | 1 | 3 | my ($self) = @_; | ||
774 | |||||||
775 | # check if there is one record to update | ||||||
776 | 1 | 2 | my $found = $self->{results}; | ||||
777 | 1 | 50 | 3 | $found->{count} == 1 or die "unexpected number of records to update"; | |||
778 | |||||||
779 | # gather some info | ||||||
780 | 1 | 3 | my $record = $found->{records}[0]; | ||||
781 | 1 | 2 | my $line_nb = $found->{lineNumbers}[0]; | ||||
782 | 1 | 2 | my $is_adding = $line_nb == -1; | ||||
783 | 1 | 50 | 3 | my $permission = $is_adding ? 'add' : 'modif'; | |||
784 | |||||||
785 | # check if user has permission | ||||||
786 | 1 | 50 | 2 | $self->can_do($permission, $record) | |||
787 | or die "No permission '$permission' for $self->{user}"; | ||||||
788 | |||||||
789 | # if adding, must make sure to read all rows so that autonum gets updated | ||||||
790 | 1 | 50 | 33 | 5 | if ($is_adding && $self->{cfg}->get('fields_autoNum')) { | ||
791 | 0 | 0 | while ($self->{data}->fetchrow) {} | ||||
792 | } | ||||||
793 | |||||||
794 | # call hook before update | ||||||
795 | 1 | 3 | $self->before_update($record); | ||||
796 | |||||||
797 | # prepare message to user | ||||||
798 | 1 | 4 | my @headers = $self->{data}->headers; | ||||
799 | 1 | 10 | my $data_line = join("|", @{$record}{@headers}); | ||||
1 | 5 | ||||||
800 | my ($msg, $id) = $is_adding ? ("Created", $self->{data}{autoNum}) | ||||||
801 | 1 | 50 | 17 | : ("Updated", $self->key($record)); | |||
802 | 1 | 6 | $self->{msg} .= " $msg: " |
||||
803 | . "Record $id: $data_line "; |
||||||
804 | |||||||
805 | # do the update | ||||||
806 | 1 | 50 | 3 | my $to_delete = $is_adding ? 0 # no previous line to delete | |||
807 | : 1; # replace previous line | ||||||
808 | 1 | 50 | 1 | eval {$self->{data}->splices($line_nb, $to_delete, $record)} or do { | |||
1 | 4 | ||||||
809 | 0 | 0 | my $err = $@; | ||||
810 | 0 | 0 | $self->rollback_update($record); | ||||
811 | 0 | 0 | die $err; | ||||
812 | }; | ||||||
813 | |||||||
814 | # call hook after update | ||||||
815 | 1 | 1144 | $self->after_update($record); | ||||
816 | } | ||||||
817 | |||||||
818 | |||||||
819 | #---------------------------------------------------------------------- | ||||||
820 | sub before_update { # | ||||||
821 | #---------------------------------------------------------------------- | ||||||
822 | 1 | 1 | 1 | 2 | my ($self, $record) = @_; | ||
823 | |||||||
824 | # copy defined params into record .. | ||||||
825 | 1 | 3 | my $key_field = $self->param($self->key_field); | ||||
826 | 1 | 5 | foreach my $field ($self->{data}->headers) { | ||||
827 | 4 | 15 | my $val = $self->param($field); | ||||
828 | 4 | 50 | 31 | next if not defined $val; | |||
829 | 0 | 0 | 0 | 0 | if ($field eq $key_field and $val ne $self->key($record)) { | ||
830 | 0 | 0 | die "supplied key $val does not match record key"; | ||||
831 | } | ||||||
832 | 0 | 0 | $record->{$field} = $val; | ||||
833 | } | ||||||
834 | |||||||
835 | # force username into user field (if any) | ||||||
836 | 1 | 2 | my $user_field = $self->{app}{user_field}; | ||||
837 | 1 | 50 | 4 | $record->{$user_field} = $self->{user} if $user_field; | |||
838 | |||||||
839 | # force current time or date into time fields (if any) | ||||||
840 | 1 | 1 | while (my ($k, $fmt) = each %{$self->{app}{time_fields}}) { | ||||
1 | 5 | ||||||
841 | 0 | 0 | $record->{$k} = strftime($fmt, localtime); | ||||
842 | } | ||||||
843 | } | ||||||
844 | |||||||
845 | |||||||
846 | 1 | 1 | sub after_update {} # override in subclasses | ||||
847 | 0 | 1 | sub rollback_update {} # override in subclasses | ||||
848 | |||||||
849 | |||||||
850 | #====================================================================== | ||||||
851 | # REQUEST HANDLING : DELETE METHODS # | ||||||
852 | #====================================================================== | ||||||
853 | |||||||
854 | #---------------------------------------------------------------------- | ||||||
855 | sub delete { | ||||||
856 | #---------------------------------------------------------------------- | ||||||
857 | 1 | 1 | 1 | 2 | my $self = shift; | ||
858 | |||||||
859 | # check if there is one record to update | ||||||
860 | 1 | 2 | my $found = $self->{results}; | ||||
861 | 1 | 50 | 3 | $found->{count} == 1 or die "unexpected number of records to delete"; | |||
862 | |||||||
863 | # gather some info | ||||||
864 | 1 | 2 | my $record = $found->{records}[0]; | ||||
865 | 1 | 2 | my $line_nb = $found->{lineNumbers}[0]; | ||||
866 | |||||||
867 | # check if user has permission | ||||||
868 | 1 | 50 | 2 | $self->can_do("delete", $record) | |||
869 | or die "No permission 'delete' for $self->{user}"; | ||||||
870 | |||||||
871 | # call hook before delete | ||||||
872 | 1 | 3 | $self->before_delete($record); | ||||
873 | |||||||
874 | # do the deletion | ||||||
875 | 1 | 4 | $self->{data}->splices($line_nb, 1, undef); | ||||
876 | |||||||
877 | # message to user | ||||||
878 | 1 | 794 | my @headers = $self->{data}->headers; | ||||
879 | 1 | 12 | my @values = @{$record}{@headers}; | ||||
1 | 4 | ||||||
880 | 1 | 18 | $self->{msg} = "Deleted: " . join("|", @values); |
||||
881 | |||||||
882 | # call hook after delete | ||||||
883 | 1 | 3 | $self->after_delete($record); | ||||
884 | } | ||||||
885 | |||||||
886 | |||||||
887 | 1 | 1 | sub before_delete {} # override in subclasses | ||||
888 | 1 | 1 | sub after_delete {} # override in subclasses | ||||
889 | |||||||
890 | |||||||
891 | #====================================================================== | ||||||
892 | # MISCELLANEOUS METHODS # | ||||||
893 | #====================================================================== | ||||||
894 | |||||||
895 | |||||||
896 | |||||||
897 | #---------------------------------------------------------------------- | ||||||
898 | sub prepare_download { | ||||||
899 | #---------------------------------------------------------------------- | ||||||
900 | 0 | 0 | 1 | 0 | my ($self, $which) = @_; | ||
901 | 0 | 0 | 0 | $self->can_do('download') | |||
902 | or die "No permission 'download' for $self->{user}"; | ||||||
903 | } | ||||||
904 | |||||||
905 | |||||||
906 | #---------------------------------------------------------------------- | ||||||
907 | sub print_help { | ||||||
908 | #---------------------------------------------------------------------- | ||||||
909 | 0 | 0 | 1 | 0 | print "sorry, no help at the moment"; | ||
910 | } | ||||||
911 | |||||||
912 | |||||||
913 | |||||||
914 | #---------------------------------------------------------------------- | ||||||
915 | sub user_match { | ||||||
916 | #---------------------------------------------------------------------- | ||||||
917 | 13 | 13 | 1 | 22 | my ($self, $access_control_list) = @_; | ||
918 | |||||||
919 | # success if the list contains '*' or the current username | ||||||
920 | 13 | 85 | return ($access_control_list =~ /\*|\b\Q$self->{user}\E\b/i); | ||||
921 | } | ||||||
922 | |||||||
923 | |||||||
924 | #---------------------------------------------------------------------- | ||||||
925 | sub key_field { | ||||||
926 | #---------------------------------------------------------------------- | ||||||
927 | 1 | 1 | 1 | 2 | my ($self) = @_; | ||
928 | 1 | 9 | return ($self->{data}->headers)[0]; | ||||
929 | } | ||||||
930 | |||||||
931 | |||||||
932 | #---------------------------------------------------------------------- | ||||||
933 | sub key { # returns the value in the first field of the record | ||||||
934 | #---------------------------------------------------------------------- | ||||||
935 | 1 | 1 | 1 | 2 | my ($self, $record) = @_; | ||
936 | |||||||
937 | # optimized version, breaking encapsulation of File::Tabular | ||||||
938 | 1 | 4 | return (tied %$record)->[1]; | ||||
939 | |||||||
940 | # going through official API would be : return $record->{$self->key_field}; | ||||||
941 | } | ||||||
942 | |||||||
943 | 1; | ||||||
944 | |||||||
945 | |||||||
946 | __END__ |