File Coverage

blib/lib/DBIx/Safe.pm
Criterion Covered Total %
statement 18 275 6.5
branch 0 146 0.0
condition 0 47 0.0
subroutine 6 57 10.5
pod 8 46 17.3
total 32 571 5.6


line stmt bran cond sub pod time code
1             # -*-cperl-*-
2             #
3             # Copyright 2006-2007 Greg Sabino Mullane
4             #
5             # DBIx::Safe is a safer way of handling database connections.
6             # You can specify exactly which commands can be run.
7             #
8              
9              
10             package DBIx::Safe;
11              
12 2     2   32098 use 5.008003;
  2         25  
  2         100  
13 2     2   2291 use utf8;
  2         22  
  2         11  
14 2     2   83 use strict;
  2         4  
  2         69  
15 2     2   11 use warnings;
  2         5  
  2         66  
16 2     2   2192 use IO::Handle;
  2         17550  
  2         688  
17 2     2   2397 use DBI 1.42;
  2         23253  
  2         9278  
18              
19             {
20              
21             our $VERSION = '1.2.5';
22              
23             *STDOUT->autoflush(1);
24             *STDERR->autoflush(1);
25              
26             my %inner;
27              
28             sub TIEHASH {
29 0     0     my $class = shift;
30 0           my $arg = shift;
31 0           my $self = bless {}, $class;
32 0           $inner{$self} = $arg;
33 0           return $self;
34             }
35              
36             sub STORE {
37 0     0     my ($self,$key,$value) = @_;
38 0           my $inner = $inner{$self};
39              
40 0           my $origkey = $key;
41 0           $key = lc $key;
42 0 0         die "Invalid access\n" unless index $key, 'dbixsafe_';
43              
44 0 0         if (exists $inner->{dbixsafe_allow_attribute}{$key}) {
45 0           $inner->{dbixsafe_allow_attribute}{$key}++;
46 0           $inner->{dbixsafe_sdbh}{$origkey} = $value;
47 0           return;
48             }
49 0           die qq{Cannot change attribute "$key"};
50             }
51              
52             sub FETCH {
53 0     0     my ($self,$key) = @_;
54 0           my $inner = $inner{$self};
55              
56 0 0         die "Invalid access\n" unless index $key, 'dbixsafe_';
57              
58             ## Assume it is a $dbh value, and return it
59 0           return $inner->{dbixsafe_sdbh}{$key};
60             }
61              
62             sub FIRSTKEY {
63 0     0     my $self = shift;
64 0           my $inner = $inner{$self};
65 0           my $foo = keys %{$inner->{dbixsafe_sdbh}};
  0            
66 0           return each %{$inner->{dbixsafe_sdbh}};
  0            
67             }
68              
69              
70             sub new {
71 0     0 1   my $class = shift;
72 0           my $arg = shift;
73              
74 0 0 0       ref $arg and ref $arg eq 'HASH'
75             or die qq{Method new() requires a hashref arguments};
76 0 0         exists $arg->{dbh} or die qq{Required argument 'dbh' was not found\n};
77 0           my $sdbh = $arg->{dbh};
78 0 0 0       ref $sdbh and ref $sdbh eq 'DBI::db'
79             or die qq{Argument 'dbh' is not a database handle\n};
80              
81             ## This is where the real information is stored
82 0           my %self = (
83             dbixsafe_sdbh => $sdbh,
84             dbixsafe_allow_command => {},
85             dbixsafe_allow_regex => {},
86             dbixsafe_deny_regex => {},
87             dbixsafe_allow_attribute => {},
88             );
89              
90             ## Now let's make sure we know how to handle this type of database
91 0 0         my $db = $sdbh->{Driver}{Name}
92             or die qq{Failed to figure out driver name\n};
93 0 0         if ($db eq 'Pg') {
94 0           $self{dbixsafe_db} = 'Postgres';
95             ## Make sure we have the required versions
96 0           my $libversion = $sdbh->{pg_lib_version};
97 0 0 0       $libversion =~ /^\d+$/ and $libversion >= 80000
98             or die qq{Must use a DBD::Pg compiled against version 8.0 or higher, this is $libversion\n};
99 0           my $version = $sdbh->{pg_server_version};
100 0 0 0       $libversion =~ /^\d+$/ and $libversion >= 70400
101             or die qq{Must use against a Postgres server version 7.4 or higher, this is $version\n};
102             } # end Postgres
103             else {
104 0           die "Sorry, I do not work with that type of database yet: $db\n";
105             }
106              
107             ## We'll be returning a tied hashref as the object
108 0           my %object;
109 0           my $codename = bless \%object, $class;
110 0           $inner{$codename} = \%self;
111 0           tie %object, 'DBIx::Safe', \%self;
112              
113 0 0         if (exists $arg->{allow_command}) {
114 0           $self{dbixsafe_allow_command} = allow_command($codename, $arg->{allow_command});
115             }
116 0 0         if (exists $arg->{allow_regex}) {
117 0           $self{dbixsafe_allow_regex} = allow_regex($codename, $arg->{allow_regex});
118             }
119 0 0         if (exists $arg->{deny_regex}) {
120 0           $self{dbixsafe_deny_regex} = deny_regex($codename, $arg->{deny_regex});
121             }
122 0 0         if (exists $arg->{allow_attribute}) {
123 0           $self{dbixsafe_allow_attribute} = allow_attribute($codename, $arg->{allow_attribute});
124             }
125              
126 0           return $codename;
127              
128             } ## end of new
129              
130             sub DESTROY {
131 0     0     my $self = shift;
132 0           delete $inner{$self};
133 0           return;
134             }
135              
136              
137             ## Specifically unsupported database handle methods
138              
139             sub prepare_cached {
140 0     0 0   my $self = shift;
141 0           die qq{Method prepare_cached() not supported yet\n};
142             }
143              
144              
145             sub safeprepare {
146              
147             ## The main gatekeeper
148              
149 0     0 0   my $self = shift;
150 0           my $type = shift;
151 0           my $string = shift;
152              
153 0           $self = $inner{$self};
154              
155 0 0         die "Invalid type passed to safeprepare\n"
156             unless $type =~ /^(?:do|prepare)$/io;
157              
158             ## Figure out the first word in the statement
159              
160 0 0         $string =~ s/^\s*(\w+)\s*/$1 /
161             or die qq{Could not find first word in string "$string"\n};
162 0           my $firstword = lc $1; ## no critic
163              
164             ## We flat out do not allow some commands in SQL statements
165 0           my %transword = map { $_ => 1 } (qw(begin commit rollback release));
  0            
166 0 0         if (exists $transword{$firstword}) {
167 0           die "Cannot use $firstword in a statement\n";
168             }
169              
170             ## Check for denied regexes
171 0           for my $deny (keys %{$self->{dbixsafe_deny_regex}}) {
  0            
172 0 0         if ($string =~ $deny) {
173 0           die qq{Forbidden statement\n};
174             }
175             }
176              
177 0           my $sdbh = $self->{dbixsafe_sdbh};
178              
179 0 0         if ($self->{dbixsafe_db} eq 'Postgres') {
180             ## Only a few words can pass through pg_prepare_now
181 0 0         if ($firstword =~ /^(?:select|update|delete|insert)$/) {
182 0 0         if (!exists $self->{dbixsafe_allow_command}{$firstword}) {
183 0           die qq{(pg) Invalid statement: $string\n};
184             }
185 0           local $sdbh->{pg_prepare_now} = 1;
186 0           my $sth = $sdbh->prepare($string);
187 0           $self->{dbixsafe_allow_command}{$firstword}++;
188 0 0         return $sth if $type eq 'prepare';
189 0           return $sth->execute(@_);
190             }
191             }
192             ## Put other DBDs here
193             else {
194 0           die qq{Do not know how to handle that DBD yet!\n};
195             }
196              
197             ## Nobody else is allowed to have a semi-colon
198 0 0         if ($string =~ /;/) {
199 0           die qq{Commands cannot contain semi-colons};
200             }
201              
202             ## Is this an allowed word?
203 0           my $found = 0;
204 0 0         if (exists $self->{dbixsafe_allow_command}{$firstword}) {
205 0           $self->{dbixsafe_allow_command}{$firstword}++;
206 0           $found = 1;
207             }
208             else {
209             ## May be allowed as a regular expression
210 0           for my $regex (keys %{$self->{dbixsafe_allow_regex}}) {
  0            
211             ## warn "Checking regex $regex against $string\n";
212 0 0         if ($string =~ /^$regex/) {
213 0           $self->{dbixsafe_allow_regex}{$regex}++;
214 0           $found=2;
215 0           last;
216             }
217             }
218             }
219              
220 0 0         $found or die qq{Invalid statement: $string\n};
221              
222 0 0         if ($type eq 'do') {
223 0           return $sdbh->do($string);
224             }
225              
226 0           my $sth = $sdbh->prepare($string);
227              
228 0 0         return $sth if $type eq 'prepare';
229              
230 0           return $sth->execute(@_);
231              
232             } ## end of safeprepare
233              
234              
235             ## Query-related database handle methods
236              
237             sub prepare {
238 0     0 0   my $self = shift;
239 0           return $self->safeprepare('prepare' => @_);
240             }
241              
242              
243             sub do {
244 0     0 0   my $self = shift;
245 0           return $self->safeprepare('do' => @_);
246             }
247              
248              
249             sub selectall_arrayref {
250 0     0 0   my ($self, $string, $attr, @bind) = @_;
251 0 0         my $sth = (ref $string) ? $string
252             : $self->safeprepare('prepare', $string, $attr);
253 0           $sth->execute(@bind);
254 0           my $slice = $attr->{Slice}; # typically undef, else hash or array ref
255 0 0 0       if (!$slice and $slice=$attr->{Columns}) {
256 0 0         if (ref $slice eq 'ARRAY') { # map col idx to perl array idx
257 0           $slice = [ @{$attr->{Columns}} ]; # take a copy
  0            
258 0           for (@$slice) { $_-- }
  0            
259             }
260             }
261 0           my $rows = $sth->fetchall_arrayref($slice, my $MaxRows = $attr->{MaxRows});
262 0 0         $sth->finish if defined $MaxRows;
263 0           return $rows;
264             } ## end of selectlall_arrayref
265              
266              
267             sub selectall_hashref {
268 0     0 0   my ($self, $string, $key_field, $attr, @bind) = @_;
269 0 0         my $sth = (ref $string) ? $string
270             : $self->safeprepare('prepare', $string, $attr);
271 0           $sth->execute(@bind);
272 0           return $sth->fetchall_hashref($key_field);
273             } ## end of selectall_hashref
274              
275              
276             sub selectrow_array {
277 0     0 0   my ($self, $string, $key_field, $attr, @bind) = @_;
278 0 0         my $sth = (ref $string) ? $string
279             : $self->safeprepare('prepare', $string, $attr);
280 0           $sth->execute(@bind);
281 0 0         my $row = $sth->fetchrow_arrayref() and $sth->finish();
282 0 0         return $row->[0] unless wantarray;
283 0           return @$row;
284             } ## end of selectrow_array
285              
286              
287             sub selectrow_arrayref {
288 0     0 0   my ($self, $string, $key_field, $attr, @bind) = @_;
289 0 0         my $sth = (ref $string) ? $string
290             : $self->safeprepare('prepare', $string, $attr);
291 0           $sth->execute(@bind);
292 0 0         my $row = $sth->fetchrow_arrayref() and $sth->finish();
293 0           return $row;
294             } ## end of selectrow_arrayref
295              
296              
297             sub selectrow_hashref {
298 0     0 0   my ($self, $string, $key_field, $attr, @bind) = @_;
299 0 0         my $sth = (ref $string) ? $string
300             : $self->safeprepare('prepare', $string, $attr);
301 0           $sth->execute(@bind);
302 0 0         my $row = $sth->fetchrow_hashref() and $sth->finish();
303 0           return $row;
304             } ## end of selectrow_hashref
305              
306              
307             sub selectcol_arrayref {
308 0     0 0   my ($self, $string, $attr, @bind) = @_;
309 0 0         my $sth = (ref $string) ? $string
310             : $self->safeprepare('prepare', $string, $attr);
311 0           $sth->execute(@bind);
312 0 0         my @columns = ($attr->{Columns}) ? @{$attr->{Columns}} : (1);
  0            
313 0           my @values = (undef) x @columns;
314 0           my $idx = 0;
315 0           for (@columns) {
316 0 0         $sth->bind_col($_, \$values[$idx++]) || return;
317             }
318 0           my @col;
319 0 0         if (my $max = $attr->{MaxRows}) {
320 0   0       push @col, @values while @col<$max && $sth->fetch;
321             }
322             else {
323 0           push @col, @values while $sth->fetch;
324             }
325 0           return \@col;
326             } ## end of selectcol_hashref
327              
328              
329              
330             ## All other database handle methods we support
331              
332             sub dbh_method {
333 0     0 0   my $self = shift;
334 0 0         (my $method = (caller 1)[3]) =~ s/^DBIx::Safe::(\w+)$/$1/
335             or die "Invalid call to change_regex\n";
336 0 0         exists $inner{$self}{dbixsafe_allow_command}{$method}
337             or die qq{Calling method '$method' is not allowed\n};
338 0           return $inner{$self}{dbixsafe_sdbh}->$method(@_);
339             }
340              
341 0     0 0   sub quote { return dbh_method(@_); }
342 0     0 0   sub quote_identifier { return dbh_method(@_); }
343 0     0 0   sub last_insert_id { return dbh_method(@_); }
344 0     0 0   sub table_info { return dbh_method(@_); }
345 0     0 0   sub column_info { return dbh_method(@_); }
346 0     0 0   sub primary_key_info { return dbh_method(@_); }
347 0     0 0   sub get_info { return dbh_method(@_); }
348 0     0 0   sub data_sources { return dbh_method(@_); }
349 0     0 0   sub can { return dbh_method(@_); }
350 0     0 0   sub parse_trace_flag { return dbh_method(@_); }
351 0     0 0   sub parse_trace_flags { return dbh_method(@_); }
352              
353             ## Read-only, no args
354 0     0 0   sub ping { return dbh_method(@_); }
355 0     0 0   sub err { return dbh_method(@_); }
356 0     0 0   sub errstr { return dbh_method(@_); }
357 0     0 0   sub state { return dbh_method(@_); } ## no critic
358              
359             ## Write-only
360 0     0 0   sub trace_msg { return dbh_method(@_); }
361 0     0 0   sub func { return dbh_method(@_); }
362              
363             ## Transactional
364 0     0 0   sub commit { return dbh_method(@_); }
365 0     0 0   sub rollback { return dbh_method(@_); }
366 0     0 0   sub begin_work { return dbh_method(@_); }
367              
368             ## Postgres specific
369 0     0 0   sub pg_savepoint { return dbh_method(@_); }
370 0     0 0   sub pg_rollback_to { return dbh_method(@_); }
371 0     0 0   sub pg_release { return dbh_method(@_); }
372              
373              
374             ## Special case database handle methods
375             sub trace {
376 0     0 0   my $self = shift;
377 0 0 0       exists $inner{$self}{dbixsafe_allow_command}{trace}
378             or !@_
379             or die qq{Calling method 'trace' with arguments is not allowed\n};
380 0           return $inner{$self}{dbixsafe_sdbh}->trace(@_);
381             }
382              
383              
384              
385              
386             ## Generic internal list modifiers
387              
388             sub change_string {
389              
390             ## Adds or removes one or more strings from an internal list
391             ## Returns the new list, even if no args
392              
393 0     0 0   my ($self,$arg) = @_;
394 0 0         (my $method = (caller 1)[3]) =~ s/^DBIx::Safe::(\w+)$/$1/
395             or die "Invalid call to change_regex\n";
396 0           my $key = $method;
397 0 0         my $type = ($key =~ s/^un//) ? 'remove' : 'add';
398 0 0         my $list = $inner{$self}{"dbixsafe_$key"}
399             or die qq{Invalid method call: $method\n};
400              
401 0 0         defined $arg or return $list;
402              
403 0           my $usage = qq{Method $method must be passed a string or an array of them\n};
404 0           my $strictdoubles = 1;
405 0           my $strictexists = 0;
406              
407 0           my %string;
408 0 0         if (ref $arg) {
409 0 0         ref $arg eq 'ARRAY' or die $usage;
410 0           for my $s (@$arg) {
411 0 0 0       if (exists $string{lc $s} and $strictdoubles) {
412 0           die qq{Method $method was passed in duplicate argument: $s\n};
413             }
414 0           $string{lc $s}++;
415             }
416             }
417             else {
418 0           $string{$arg}++;
419             }
420              
421 0           my %command;
422 0           for my $s (keys %string) {
423 0           $s =~ s/^\s*(.+)\s*$/$1/;
424 0           for my $c (split /\s+/ => lc $s) {
425 0 0         if ($c !~ /^[a-z_]+$/) {
426 0           die qq{Method $method was passed an invalid argument: $c\n};
427             }
428 0 0 0       if (exists $command{$c} and $strictdoubles) {
429 0           die qq{Method $method was passed in duplicate argument: $c\n};
430             }
431 0 0         if ($type eq 'remove') {
432 0 0 0       if (! exists $list->{$c} and $strictexists) {
433 0           die qq{Method $method was passed in non-existent argument: $c\n};
434             }
435             }
436             else {
437 0 0 0       if (exists $list->{$c} and $strictexists) {
438 0           die qq{Method $method was passed in already existing argument: $c\n};
439             }
440             }
441 0           $command{$c}++;
442             }
443             }
444 0           for my $c (keys %command) {
445 0 0         if ($type eq 'remove') {
446 0           delete $list->{$c};
447             }
448             else {
449 0 0         if ($c eq 'autocommit') {
450             ## We don't hardcode the method here: too easy to accidentally break
451 0           die qq{Attribute AutoCommit cannot be changed};
452             }
453 0           $list->{$c} = 0;
454             }
455             }
456              
457 0           return $list;
458              
459             } ## end of change_string
460              
461              
462             sub change_regex {
463              
464             ## Adds or removes one or more regular expressions from an internal list
465             ## Returns the new list, even if no args
466              
467 0     0 0   my ($self,$arg) = @_;
468 0 0         (my $method = (caller 1)[3]) =~ s/^DBIx::Safe::(\w+)$/$1/
469             or die "Invalid call to change_regex\n";
470 0           my $key = $method;
471 0 0         my $type = ($key =~ s/^un//) ? 'remove' : 'add';
472 0 0         my $list = $inner{$self}{"dbixsafe_$key"}
473             or die "Invalid nethod call: $method\n";
474              
475 0 0         defined $arg or return $list;
476              
477 0           my $usage = qq{Method $method must be passed a regular expression or an array of them\n};
478 0 0         ref $arg or die $usage;
479              
480 0           my $strictdoubles = 1;
481 0           my $strictexists = 0;
482              
483 0           my %regex;
484 0 0         if (ref $arg eq 'ARRAY') {
    0          
485 0           for my $r (@$arg) {
486 0 0 0       ref $r and ref $r eq 'Regexp' or die $usage;
487 0 0 0       if (exists $regex{$r} and $strictdoubles) {
488 0           die qq{Method $method was passed in duplicate regexes for $r\n};
489             }
490 0           $regex{$r}++;
491             }
492             }
493             elsif (ref $arg eq 'Regexp') {
494 0           $regex{$arg}++;
495             }
496             else {
497 0           die $usage;
498             }
499              
500 0           for my $r (keys %regex) {
501 0 0         if ($type eq 'remove') {
502 0 0 0       if (! exists $list->{$r} and $strictexists) {
503 0           die qq{Method $method was passed in a non-existent regex: $r\n};
504             }
505 0           delete $list->{$r};
506             }
507             else {
508 0 0 0       if (exists $list->{$r} and $strictexists) {
509 0           die qq{Method $method was passed in an already existing regex: $r\n};
510             }
511 0   0       $list->{$r} ||= 0;
512             }
513             }
514              
515 0           return $list;
516              
517             } ## end of change_regex
518              
519              
520 0     0 1   sub allow_command { return change_string(@_); }
521 0     0 1   sub unallow_command { return change_string(@_); }
522 0     0 1   sub allow_attribute { return change_string(@_); }
523 0     0 1   sub unallow_attribute { return change_string(@_); }
524 0     0 1   sub unallow_regex { return change_regex(@_); }
525 0     0 0   sub undeny_regex { return change_regex(@_); }
526 0     0 1   sub deny_regex { return change_regex(@_); }
527 0     0 1   sub allow_regex { return change_regex(@_); }
528              
529             }
530              
531             1;
532              
533             __END__