| 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__ |