File Coverage

blib/lib/DBIx/CGITables.pm
Criterion Covered Total %
statement 18 233 7.7
branch 0 108 0.0
condition 0 67 0.0
subroutine 6 21 28.5
pod 0 15 0.0
total 24 444 5.4


line stmt bran cond sub pod time code
1             # Copyright (c) Green Smoked Socks Productions y2k
2              
3             package DBIx::CGITables;
4              
5 1     1   1042 use strict;
  1         1  
  1         43  
6 1     1   6 use Carp qw(cluck);
  1         2  
  1         90  
7 1     1   5 use vars qw($VERSION $CGI_Class $Template_Class $Recordset_Class $Client_Error_Class);
  1         11  
  1         1914  
8              
9             $CGI_Class='CGI';
10             $Template_Class='HTML::Template';
11             $Recordset_Class='DBIx::Recordset';
12             $Client_Error_Class='CGI::ClientError';
13              
14             # Remember to update the POD (yes, even the version number)
15             # and to tag the cvs (v-major-minor) upon new version numbers
16             $VERSION="0.001";
17              
18             # HACKING INFORMATION
19             # ===================
20              
21             # The CGITable class is structured like this:
22              
23             # Special parameters:
24             # $self->{filename}, $self->{query}
25              
26             # Parameters to DBIx::Recordset:
27             # $self->{params}->{$recordset_name}->{$param_key} = $param_value
28              
29             # Output to template:
30             # $self->{output}->{$recordset_name}->[$i]->{$db_key} = $db_value
31             # $self->{output}->{cgi_query}->[$i] = {key=>$param_key, value=>$param_value}
32             # $self->{output}->{$query_key} = $query_value
33             # (...and more are likely to come...)
34              
35             # to avoid inprobable, but still potential name clashes, a dash (-)
36             # should be prepended to all the recordset names, except the default
37             # one ('default')
38              
39             # Recordset objects:
40             # $self->{recordsets}->{$recordset_name}
41              
42             # Parameters to the template class:
43             # $self->{T}->{$option_key}->$option_value
44              
45             # Special recordset variables (PreserveCase, Debug):
46             # $self->{RGV}->{$variable_name}->$variable_value
47              
48             # Changes might occur to the internal data structure, but the API shouldn't.
49              
50             # Some of the subs below might be splitted if it's needed (i.e. for
51             # making it easier to alter the class behaviour by inheritance)
52              
53             sub new {
54             # Class identification:
55 0   0 0 0   my $object_or_class = shift; my $class = ref($object_or_class) || $object_or_class;
  0            
56              
57             # Eventually import params:
58 0   0       my $self={params=>{default=>($_[0] || {})}};
59              
60             # Check for the special !!Query param and/or !!QueryClass and
61             # initialize the query:
62 0 0         if (!($self->{query}=$self->{params}->{default}->{'!!Query'})) {
63 0 0         $self->{params}->{default}->{'!!QueryClass'}=$CGI_Class
64             unless $self->{params}->{default}->{'!!QueryClass'};
65 0           eval "require ".$self->{params}->{default}->{'!!QueryClass'};
66 0           $self->{query}=$self->{params}->{default}->{'!!QueryClass'}->new;
67 0 0         return undef if !defined $self->{query};
68             }
69              
70             # Check for the special !!Filename param:
71 0   0       $self->{filename}=$self->{params}->{default}->{'!!Filename'} ||
72             $ENV{'PATH_TRANSLATED'} ||
73             undef;
74 0 0         unless ($self->{filename}) {
75 0           print "What template do you want to parse?";
76             # Should have used readline ... but at the other hand, this is _not_ the intended usage
77 0           $self->{filename}=<>;
78             }
79 0 0         die "Template not specified"
80             unless $self->{filename};
81              
82 0 0         die "Template ($self->{filename}) not readable or doesn't exist"
83             unless -r $self->{filename};
84              
85             # Check for !!ParamFileDir and !!ParamFile
86 0 0         $self->{param_file}=$self->{params}->{default}->{'!!ParamFile'}
87             if exists $self->{params}->{default}->{'!!ParamFile'};
88 0 0         $self->{param_file_dir}=$self->{params}->{default}->{'!!ParamFileDir'}
89             if exists $self->{params}->{default}->{'!!ParamFileDir'};
90              
91 0           bless $self, $class;
92 0           return $self;
93             }
94              
95             sub search_execute_and_do_everything_even_parse_the_template {
96 0     0 0   my $self=shift;
97 0           my $hash=shift;
98 0           $self->fetch_params_from_cookies();
99 0           $self->fetch_params_from_query();
100 0           $self->fetch_params_from_file();
101 0 0         $self->fetch_params_from_hash($hash)
102             if defined($hash);
103 0           $self->execute_recordsets();
104 0           $self->parse_template();
105             }
106              
107             sub fetch_params_from_query {
108 0     0 0   my $self=shift;
109 0           my $q=$self->{query};
110 0           for my $key ($q->param) {
111 0           my $value=$q->param($key);
112 0           $self->process_param(0, $key, $value);
113 0           push (@{$self->{output}->{cgi_query}}, {key => $key, value => $value});
  0            
114             }
115             }
116              
117 0     0 0   sub fetch_params_from_cookies {
118             # It might be relevant to set i.e. !Username and !Password in the cookies.
119             # stub!
120             }
121              
122             sub fetch_params_from_file {
123 0     0 0   my $self=shift;
124 0   0       my $file=$self->find_param_file() || return 0;
125 0           open(FILE, "<$file");
126 0           while() {
127 0           chop;
128 0           $self->process_param(1, $_);
129             }
130 0           close(FILE);
131             }
132              
133             sub find_param_file {
134             # See the POD for naming conventions
135              
136 0     0 0   my $self=shift;
137 0 0         return $self->{param_file}
138             if exists $self->{param_file};
139 0           my $f=$self->{filename};
140 0 0         $f =~ /\.(\w+)$/
141             || die "The template filename ($f) should have an ending (like .databasetemplate or .end or .dct or whatever)";
142 0           my $pf="$`.param.$1";
143 0 0         if (my $d=$self->{param_file_dir}) {
144 0 0         $pf =~ m|/([^/]+)$| || die "given template is a dir?";
145 0           $pf = $d . $1;
146             }
147 0 0         die "Parameter file ($pf) not readable (template filename: $f, param )" unless -r $pf;
148 0           return $pf;
149             }
150              
151             sub process_param {
152 0     0 0   my $self=shift;
153 0           my $single=shift;
154 0           my $special;
155             my $key;
156 0           my $value;
157 0           my $name='default';
158 0           $_=shift;
159              
160 0 0         /^(;|#|--)/ && return;
161 0 0         /^(\s*)$/ && return;
162              
163 0 0         if (/^\%([^\ ]*) /) {
164 0           chop($special=$&);
165 0           $_=$';
166             }
167            
168 0 0         if ($single) {
169 0 0         if (m#^(.+?)\=#) {
170 0           $key=$1;
171 0           $value=$';
172             } else {
173 0           $key=$_;
174 0           $value=1;
175             }
176             } else {
177 0           $key=$_;
178 0           $value=shift;
179             }
180              
181 0 0         if ($key =~ m#/(\w+)/#) {
182 0           $name=$1;
183 0           $key=$';
184             }
185              
186             # Ordinary variable, override or yield and no previous param set.
187 0 0 0       if (!$special || ($special =~ /^\%[\=\!]/)
    0 0        
    0 0        
    0          
188             || ($special =~ /^\%\?/ && !exists $self->{params}->{$name}->{$key})) {
189             # Special key containing '/':
190 0           $self->{output}->{$key}=$value;
191 0           my @keys=split(/\//, $key);
192 0 0         $self->{params}->{$name}={}
193             unless (exists $self->{params}->{$name});
194 0           my $p=$self->{params}->{$name};
195 0           while (@keys) {
196 0           $key=shift @keys;
197 0 0         if (@keys) {
198 0 0         if (!exists $p->{$key}) {
199 0           $p->{$key}={};
200             }
201 0           $p=$p->{$key};
202             } else {
203 0           $p->{$key}=$value;
204             }
205             }
206             }
207              
208             # Ignore!
209             elsif ($special eq '%()') {
210 0           return;
211             }
212              
213             # Ignore or override!
214             elsif ($special eq '%!()') {
215 0           die "stub!";
216             }
217              
218             # Recordset Global Variable or Template option
219             elsif ($special =~ '%(RGV|T)') {
220 0           $self->{$1}->{$key}=$value;
221             }
222              
223             # Oup!
224             elsif (2) {
225 0           die "stub!";
226             }
227              
228             }
229              
230             # Used in sub execute_recordsets. Can be overridden.
231             sub load_recordset_class {
232 0     0 0   my $self=shift;
233 0           eval "require $$self{recordset_class}";
234              
235 0           for (keys %{$self->{'RGV'}}) {
  0            
236 1     1   7 no strict 'refs';
  1         1  
  1         994  
237 0 0         if (/^(Debug|PreserveCase|FetchsizeWarn)$/) {
238 0           $ {*{"$$self{recordset_class}::$1"}{SCALAR}}=$self->{'RGV'}->{$_};
  0            
  0            
239             } else {
240              
241             # Somebody (the web user or anyone with access to the
242             # parameter file or the (F)CGI script) has tried tom
243             # modify some variable (s)he's not allowed to update.
244              
245             # I didn't see the need for setting other things than
246             # Debug, PreserveCase and FetchsizeWarn, if I'm wrong, the
247             # (Debug|PreserveCase|FetchsizeWarn) line above has to be
248             # modified (Better: put it as a package-global variable)
249              
250 0           warn "Not allowed (check the code for more info)";
251             }
252             }
253             }
254              
255             # Used in sub execute_recordsets. Can be overridden.
256             sub massage_init_parameters {
257 0     0 0   my $self=shift;
258 0           my $p=shift;
259            
260             # Temporary. This should be declared somewhere else.
261 0           $p->{'!DBIAttr'}={RaiseError=>0, PrintError=>0};
262            
263             # Empty Usernames are perfectly valid in MySQL, and I
264             # think it's more apropriate to have "" as default than
265             # whatever user the webserver is running as.
266 0   0       $p->{'!Username'} =
267             $self->{params}->{default}->{'!Username'} || "";
268            
269             # Locally I have set up different security models; login
270             # without a password (or by submitting a password in the
271             # clear), login with password over a secure line, and a
272             # login with a valid SSL certificate. For the first I've
273             # put on an extention "_nopass" to the username, the
274             # latter "_ssl". Anyway, the user shouldn't have to know
275             # about those extentions.
276 0   0       $p->{'!Username'} .=
277             $p->{'!UsernameExtention'} || "";
278 0 0         if (my $val=$p->{'!ConvertTimestampMysql2Iso'}) {
279 0           for my $v (split /,/, $val) {
280 0           $p->{'!Filter'}->{$v}=
281             [undef, \×tamp_mysql2iso]
282             }
283             }
284              
285             # Do this recursively
286 0           for my $l (keys %{$p->{'!Links'}}) {
  0            
287 0           $self->massage_init_parameters($p->{'!Links'}->{$l});
288             }
289             }
290              
291             # Used in sub execute_recordsets. Can be overridden.
292             sub massage_where_parameters {
293 0     0 0   my $self=shift;
294 0           my $p=shift;
295              
296             # Support/expand the $substring_search parameter
297 0 0         if (my $val=$p->{'$substring_search'}) {
298 0           for (split(/,/, $val)) {
299 0           $p->{'*'.$_}=" LIKE ";
300 0 0 0       $p->{$_}= "%$$p{$_}%"
      0        
301             unless exists $p->{'=update'}
302             || exists $p->{'=insert'}
303             || !$p->{$_};
304             }
305             }
306              
307             # Do this recursively
308 0           for my $l (keys %{$p->{'!Links'}}) {
  0            
309 0           $self->massage_where_parameters($p->{'!Links'}->{$l});
310             }
311             }
312              
313             sub execute_recordsets {
314 0     0 0   my $self=shift;
315              
316 0 0         $self->{params}->{default}->{'!DoNothing'} && return;
317              
318             # Check for the special !RecordsetClass:
319 0   0       $self->{recordset_class}=
320             $self->{params}->{default}->{'!RecordsetClass'} ||
321             $Recordset_Class;
322              
323 0           $self->load_recordset_class();
324              
325 0           for my $query (keys %{$self->{params}}) {
  0            
326              
327 0 0         if ($self->{params}->{$query}->{'!SearchForm'}) {
328              
329             # Set ?count
330 0           $self->{output}->{'?count'}=0;
331              
332             # Make one empty LOOP element
333 0           $self->{output}->{$query}=[{}];
334              
335             } else {
336              
337 0           $self->massage_init_parameters($self->{params}->{$query});
338              
339 0 0         unless ($self->{recordsets}->{$query}=
  0            
340             tie (@{$self->{output}->{$query}},
341             $self->{recordset_class},
342             $self->{params}->{$query})) {
343 0   0       my $error=$DBIx::Recordset::LastError || $DBI::errstr
344             || die "Could not tie array and no error message present";
345 0           $self->handle_error($error);
346             }
347              
348 0           $self->massage_where_parameters($self->{params}->{$query});
349              
350 0 0 0       $self->{recordsets}->{$query}->Execute($self->{params}->{$query})
351             || $self->handle_error($self->{recordsets}->{$query}->LastError())
352             if (defined $self->{recordsets}->{$query});
353              
354             # If we have added/updated something, we usually also want to
355             # display the changes/new record:
356              
357 0 0 0       if (0||$self->{params}->{default}->{'=update'} ||
358             $self->{params}->{default}->{'=insert'}) {
359              
360 0 0 0       $self->{recordsets}->{$query}->Select($self->{params}->{$query})
361             || $self->handle_error($self->{recordsets}->{$query}->LastError())
362             if (defined $self->{recordsets}->{$query});
363              
364             }
365              
366             # Copy the data if needed (Tie::ARRAY not implemented properly
367             # in earlier versions of perl + many DBMS'es doesn't give away
368             # the count anyway ... and the current version of HTML::Template
369             # need to know the size of the array.
370            
371 1     1   7 no strict 'refs';
  1         2  
  1         868  
372              
373 0 0 0       if ($ {*{"$$self{recordset_class}::FetchsizeWarn"}{SCALAR}} ||
  0            
  0            
374             $]<5.00504) {
375             # This really shouldn't be necessary :/
376 0           my $hash_with_broken_arrays=$self->{output};
377 0           $self->{output}={};
378 0           &faenskopiering($hash_with_broken_arrays, $self->{output},
379             $self->{recordsets}, $self->{params});
380             }
381              
382             # Set ?count
383 0           $self->{output}->{'?count'}=scalar @{$self->{output}->{default}};
  0            
384             }
385             }
386             }
387              
388             sub handle_error {
389 0     0 0   my $self=shift;
390 0           my $errorobject=shift;
391 0 0         if (ref $errorobject) {
392 0           die "stub!";
393             } else {
394             # Is this DBD-dependent? I guess so. It should be considered
395             # if it can be done in some DBD-independent way.
396              
397             # First the client errors:
398 0 0         if ($errorobject =~ /^Access denied/) {
399 0           $self->{status}->{error}="The database reported:\n$errorobject\n\nThis probably means either that you're using the wrong username, wrong password or that you haven't been granted access.";
400             } else {
401              
402             # We shouldn't give away the error message to the user (security
403             # reasons + that it's likely to be misunderstood by a stupid end user
404             # + that probably only the server administrator can do anything
405             # anyway). Let's view a 500 page, preferable with lots of bells and
406             # whistles and even a banner to increase the income.
407              
408 0           die $errorobject;
409             }
410             }
411             }
412              
413             sub parse_template {
414 0     0 0   my $self=shift;
415              
416 0           my $f=$self->{filename};
417              
418             # An alternative template might be chosen with the "!Goto" parameter
419 0   0       my $g=$self->{params}->{'!Goto'} || undef ;
420              
421             # Let's see if there is an alternative template...
422             # See the POD for naming conventions
423              
424             # Separate dir, base and ending
425 0           my ($dir, $base, $ending) = $f =~ /(.*?)([^\/]*)\.(\w+)$/;
426              
427             # (I guess there might be problems under substandard OS'es using
428             # different directory naming schemes. As if I care. But I might
429             # be willing to accept a patch)
430              
431 0           my $st=$self->{status};
432              
433 0 0         if ($st->{error}) {
434 0           $f="$dir$base.error.$ending";
435 0 0         if (! -r $f) {
436 0           warn "Error: $$st{error} (error template not found)";
437 0           eval "require $Client_Error_Class";
438 0           $Client_Error_Class->error($$st{error}); return;
  0            
439             }
440             }
441              
442 0           my $cnt=$self->{output}->{'?count'};
443              
444             # templates with extentions found_more, found_one, found_none and
445             # found_35 can be used.
446              
447 0 0         my $found1=($cnt>1 ? "found_more" : ($cnt==1 ? "found_one" : "found_none"));
    0          
448 0           my $found2="found_".$cnt;
449              
450             STATUS_TEMPLATE:
451 0           for ("update_ok", "delete_ok",
452             "add_ok", $found2, $found1) {
453 0 0 0       if ($st->{$_}||/^found_/) {
454 0 0         if (-r "$g.$_.$ending") {
    0          
455 0           $self->{filename}="$dir$g.$_.$ending";
456 0           last STATUS_TEMPLATE;
457             } elsif (-r "$dir$base.$_.$ending") {
458 0           $self->{filename}="$dir$base.$_.$ending";
459 0           last STATUS_TEMPLATE;
460             }
461             }
462             }
463              
464             # Check for the special !TemplateClass:
465 0   0       $self->{template_class}=
466             $self->{params}->{default}->{'!TemplateClass'} ||
467             $Template_Class;
468              
469 0           eval "require $$self{template_class}";
470              
471 0           $self->{T}->{filename}=$self->{filename};
472              
473 0           my $template=$self->{template_class}->new(%{$self->{T}});
  0            
474              
475 1     1   5 no strict 'refs';
  1         2  
  1         994  
476              
477 0           $template->param(%{$self->{output}});
  0            
478              
479 0 0         if (my $http=$self->{params}->{default}->{'!HTTPHeaders'}) {
    0          
480 0           print $http, "\n";
481             } elsif (my $ct=$self->{params}->{default}->{'!ContentType'}) {
482 0           print "Content-Type: $ct\n\n";
483             }
484              
485 0           print $template->output;
486             }
487              
488             # Recursive sub for copying the output hash. This one shouldn't be
489             # necessary ... but it seems like it is, due to the TIE not working as
490             # good as it should :(
491              
492             # This sub will eventually produce weird results or even recurse
493             # forever if given hashes with "reused" data or looping references.
494             # So don't do it.
495              
496             # Eventually I inserted other features here as well. It's hacky, and
497             # should disappear. The features are:
498              
499             # - mod2
500             # - fetch_* (not supported, subqueries are always fetched here)
501             # - generate_booleans
502              
503             sub faenskopiering {
504 0     0 0   my $src=shift;
505 0           my $dst=shift;
506 0           my $rsh=shift; # What? Why? Shouldn't params be good enough, huh?
507 0           my $params=shift;
508              
509 0           for (keys %$src) {
510 0 0 0       if (ref $src->{$_} eq "ARRAY"
    0          
511             || $src->{$_} =~ /^\*DBIx::Recordset/) # Ouch, ugly, ugly, ugly..
512             {
513 0           my $i=0;
514              
515             # Aaaahhrgghajgkjgussakjlf!
516 0           my $links;
517             my $p;
518 0 0 0       if (defined $rsh && ref $rsh eq "HASH" && exists $rsh->{$_}) {
      0        
519 0           $links=$rsh->{$_};
520 0           $p=$params->{$_};
521 0 0 0       if (defined $links && ref $links ne "HASH") {
522 0           $links=$links->Links();
523             } else {
524 0           $links=$links->{'!Links'};
525             }
526             }
527            
528 0   0       while (my $z=$src->{$_}->[$i] || undef) {
529 0 0         if (ref $z eq "HASH") {
530 0           $dst->{$_}->[$i]={};
531              
532             # Feature hack to get mod2 working:
533 0           $dst->{$_}->[$i]->{'?mod2'}=$i%2;
534              
535             # Feature hack to get generate_booleans working:
536 0           for my $k (split(/\,/, $p->{'$generate_booleans'})) {
537 0           $dst->{$_}->[$i]->{$k.'_'.$src->{$_}->[$i]->{$k}}=1;
538             }
539              
540 0           for my $l (keys %$links) {
541             # gurgleblargefaensdritt!
542 0           my $dumb=$src->{$_}->[$i]->{$l};
543             }
544 0           &faenskopiering($z, $dst->{$_}->[$i], $links, $p->{'!Links'});
545             } else {
546 0           warn "This code shouldn't be executed?";
547 0           $dst->{$_}->[$i]=$z;
548             }
549 0           $i++;
550             }
551             } elsif (ref $src->{$_} eq "HASH") {
552 0           &faenskopiering($src->{$_}, $dst->{$_});
553             } else {
554 0           $dst->{$_}=$src->{$_};
555             }
556             }
557             }
558              
559             sub timestamp_mysql2iso {
560 0     0 0   $_=shift;
561 0           /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/;
562 0           return "$1-$2-$3 $4:$5:$6";
563             }
564              
565              
566              
567             __END__