File Coverage

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__