File Coverage

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__