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