File Coverage

blib/lib/Audit/DBI.pm
Criterion Covered Total %
statement 211 250 84.4
branch 93 186 50.0
condition 15 47 31.9
subroutine 15 19 78.9
pod 9 9 100.0
total 343 511 67.1


line stmt bran cond sub pod time code
1             package Audit::DBI;
2              
3 11     11   38924 use strict;
  11         24  
  11         389  
4 11     11   112 use warnings;
  11         19  
  11         311  
5              
6 11     11   63 use Carp;
  11         24  
  11         831  
7 11     11   12754 use Data::Validate::Type;
  11         184439  
  11         833  
8 11     11   14921 use Storable;
  11         52906  
  11         827  
9 11     11   11814 use Try::Tiny;
  11         23227  
  11         706  
10              
11 11     11   6964 use Audit::DBI::Event;
  11         47  
  11         418  
12 11     11   6976 use Audit::DBI::Utils;
  11         40  
  11         51933  
13              
14              
15             =head1 NAME
16              
17             Audit::DBI - Audit data changes in your code and store searchable log records in a database.
18              
19              
20             =head1 VERSION
21              
22             Version 1.8.2
23              
24             =cut
25              
26             our $VERSION = '1.8.2';
27              
28              
29             =head1 SYNOPSIS
30              
31             use Audit::DBI;
32              
33             # Create the audit object.
34             my $audit = Audit::DBI->new(
35             database_handle => $dbh,
36             );
37              
38             # Create the necessary tables.
39             $audit->create_tables();
40              
41             # Record an audit event.
42             $audit->record(
43             event => $event,
44             subject_type => $subject_type,
45             subject_id => $subject_id,
46             event_time => $event_time,
47             diff => [ $old_structure, $new_structure ],
48             search_data => \%search_data,
49             information => \%information,
50             affected_account_id => $account_id,
51             file => $file,
52             line => $line,
53             );
54              
55             # Search audit events.
56             my $audit_events = $audit->review(
57             [ search criteria ]
58             );
59              
60             To see an example of a search interface for audit events recorded by
61             L, check the C directory of L.
62              
63              
64             =head1 FORCE OBJECT STRINGIFICATION
65              
66             When data structures are dumped (for diffs or to store information), it is
67             sometimes desirable to turn some of the objects into strings, for two reasons:
68              
69             =over 4
70              
71             =item *
72              
73             First, two output strings can be the same even if the objects aren't, which is
74             common when working with floats.
75              
76             =item *
77              
78             Second, the string version is easier to read than a dump of the object's
79             internal variables.
80              
81             =back
82              
83             A good example of this is L. To convert those objects to
84             strings, you can use the following:
85              
86             local $Audit::DBI::FORCE_OBJECT_STRINGIFICATION =
87             {
88             'Math::Currency' => 'bstr',
89             };
90              
91             =cut
92              
93             our $FORCE_OBJECT_STRINGIFICATION = {};
94              
95              
96             =head1 METHODS
97              
98             =head2 new()
99              
100             Create a new Audit::DBI object.
101              
102             my $audit = Audit::DBI->new(
103             database_handle => $dbh,
104             );
105              
106             Parameters:
107              
108             =over 4
109              
110             =item * database handle
111              
112             Mandatory, a DBI object.
113              
114             =item * memcache
115              
116             Optional, a Cache::Memcached or Cache::Memcached::Fast object to use for
117             rate limiting. If not specified, rate-limiting functions will not be available.
118              
119             =back
120              
121             =cut
122              
123             sub new
124             {
125 8     8 1 193902 my ( $class, %args ) = @_;
126 8         36 my $dbh = delete( $args{'database_handle'} );
127 8         28 my $memcache = delete( $args{'memcache'} );
128 8 50       64 croak 'The following arguments are not valid: ' . join( ', ', keys %args )
129             if scalar( keys %args ) != 0;
130              
131             # Check parameters.
132 8 50       64 croak "Argument 'database_handle' is mandatory and must be a DBI object"
133             if !Data::Validate::Type::is_instance( $dbh, class => 'DBI::db' );
134              
135 8         303 my $self = bless(
136             {
137             'database_handle' => $dbh,
138             'memcache' => $memcache,
139             },
140             $class
141             );
142              
143 8         53 return $self;
144             }
145              
146              
147             =head2 record()
148              
149             Record an audit event along with information on the context and data changed.
150              
151             $audit->record(
152             event => $event,
153             subject_type => $subject_type,
154             subject_id => $subject_id,
155             event_time => $event_time,
156             diff =>
157             [
158             $old_structure,
159             $new_structure,
160             ],
161             search_data => \%search_data,
162             information => \%information,
163             affected_account_id => $account_id,
164             file => $file,
165             line => $line,
166             );
167              
168             Required:
169              
170             =over 4
171              
172             =item * event
173              
174             The type of action performed (48 characters maximum).
175              
176             =item * subject_type
177              
178             Normally, the singular form of the name of a table, such as "object" or
179             "account" or "order".
180              
181             =item * subject_id
182              
183             If subject_type is a table, the corresponding record ID.
184              
185             =back
186              
187             Optional:
188              
189             =over 4
190              
191             =item * diff
192              
193             This automatically calculates the differences between the two data structures
194             passed as values for this parameter, and makes a new structure recording those
195             differences.
196              
197             =item * search_data
198              
199             A hashref of all the key/value pairs that we may want to be able search on later
200             to find this type of event. You may pass either a scalar or an arrayref of
201             multiple values for each key.
202              
203             =item * information
204              
205             Any other useful information (such as user input) to understand the context of
206             this change.
207              
208             =item * account_affected
209              
210             The ID of the account to which the data affected during that event where linked
211             to, if applicable.
212              
213             =item * event_time
214              
215             Unix timestamp of the time that the event occurred, the default being the
216             current time.
217              
218             =item * file and line
219              
220             The filename and line number where the event occurred, the default being the
221             immediate caller of Audit::DBI->record().
222              
223             =back
224              
225             Notes:
226              
227             =over 4
228              
229             =item *
230              
231             If you want to delay the insertion of audit events (to group them, for
232             performance), subclass L and add a custom C method.
233              
234             =item *
235              
236             You can specify a custom comparison function to use for comparing leaf nodes in
237             the data structures passed to diff, with the following syntax.
238              
239             diff =>
240             [
241             $old_structure,
242             $new_structure,
243             comparison_function => sub { ... },
244             ]
245              
246             See C in L for more information on how to
247             write custom comparison functions.
248              
249             =back
250              
251             =cut
252              
253             sub record ## no critic (NamingConventions::ProhibitAmbiguousNames)
254             {
255 17     17 1 27372 my ( $self, %args ) = @_;
256 17         56 my $limit_rate_timespan = delete( $args{'limit_rate_timespan'} );
257 17         49 my $limit_rate_unique_key = delete( $args{'limit_rate_unique_key'} );
258 17         101 my $dbh = $self->get_database_handle();
259              
260             # Check required parameters.
261 17         54 foreach my $arg ( qw( event subject_type subject_id ) )
262             {
263 48 100 66     384 next if defined( $args{ $arg } ) && $args{ $arg } ne '';
264 3         51 croak "The argument $arg must be specified.";
265             }
266 14 0 0     61 croak('The argument "limit_rate_timespan" must be a strictly positive integer.')
      33        
267             if defined $limit_rate_timespan && ( $limit_rate_timespan !~ /^\d+$/ || $limit_rate_timespan == 0 );
268 14 50 33     67 croak('The argument "limit_rate_unique_key" must be a string with length greater than zero.')
269             if defined $limit_rate_unique_key && length $limit_rate_unique_key == 0;
270 14 50       51 croak('Both "limit_rate_timespan" and "limit_rate_unique_key" must be defined.')
271             if defined $limit_rate_timespan != defined $limit_rate_unique_key;
272              
273             # Rate limiting.
274 14 50       45 if ( defined( $limit_rate_timespan ) )
275             {
276 0 0       0 if ( !defined( $self->get_cache( key => $limit_rate_unique_key ) ) )
277             {
278             # Cache event.
279 0         0 $self->set_cache(
280             key => $limit_rate_unique_key,
281             value => 1,
282             expire_time => $limit_rate_timespan,
283             );
284             }
285             else
286             {
287             # No need to log audit event.
288 0         0 return 1;
289             }
290             }
291              
292             # Record the time (unless it was already passed in).
293 14   66     122 $args{'event_time'} ||= time();
294              
295             # Store the file and line of the caller, unless they were passed in.
296 14 50 33     83 if ( !defined( $args{'file'} ) || !defined( $args{'line'} ) )
297             {
298 14         79 my ( $file, $line ) = ( caller() )[1,2];
299 14         120 $file =~ s|.*/||;
300 14 50       110 $args{'file'} = $file
301             if !defined( $args{'file'} );
302 14 50       82 $args{'line'} = $line
303             if !defined( $args{'line'} );
304             }
305              
306 14         66 my $audit_event = $self->insert_event( \%args );
307              
308 14 50       1903 return defined( $audit_event )
309             ? 1
310             : 0;
311             }
312              
313              
314             =head2 review()
315              
316             Return the logged audit events corresponding to the criteria passed as
317             parameter.
318              
319             my $results = $audit->review(
320             ip_ranges =>
321             [
322             {
323             include => $boolean,
324             begin => $begin,
325             end => $end
326             },
327             ...
328             ],
329             subjects =>
330             [
331             {
332             include => $boolean,
333             type => $type1,
334             ids => \@id1,
335             },
336             {
337             include => $boolean,
338             type => $type2,
339             ids => \@id2,
340             },
341             ...
342             ],
343             date_ranges =>
344             [
345             {
346             include => $boolean,
347             begin => $begin,
348             end => $end
349             },
350             ...
351             ],
352             values =>
353             [
354             {
355             include => $boolean,
356             name => $name1,
357             values => \@value1,
358             },
359             {
360             include => $boolean,
361             name => $name2,
362             values => \@value2,
363             },
364             ...
365             ],
366             events =>
367             [
368             {
369             include => $boolean,
370             event => $event,
371             },
372             ...
373             ],
374             logged_in =>
375             [
376             {
377             include => $boolean,
378             account_id => $account_id,
379             },
380             ...
381             ],
382             affected =>
383             [
384             {
385             include => $boolean,
386             account_id => $account_id,
387             },
388             ...
389             ],
390             );
391              
392             All the parameters are optional, but at least one of them is required due to the
393             sheer volume of data this module tends to generate. If multiple parameters are
394             passed, they are additive, i.e. use AND to combine themselves.
395              
396             =over 4
397              
398             =item * ip_ranges
399              
400             Allows restricting the search to ranges of IPs. Can be given in either
401             dot-decimal notation (n.n.n.n) or as an integer.
402              
403             =item * events
404              
405             Allows searching on specific events.
406              
407             =item * subjects
408              
409             Allows to search on the subject types and subject IDs passed when calling
410             record(). Multiple subject types can be passed, and for each subject type
411             multiple IDs can be passed, hence the use of an arrayref of hashes for this
412             parameter. Using
413              
414             [
415             {
416             type => $type1,
417             ids => \@id1,
418             },
419             {
420             type => $type2,
421             ids => \@id2,
422             }
423             ]
424              
425             would translate into
426              
427             (subject_type = '[type1]' AND subject_id IN([ids1]) )
428             OR (subject_type = '[type2]' AND subject_id IN([ids2]) )
429              
430             for searching purposes.
431              
432             =item * date_ranges
433              
434             Allows restricting the search to specific date ranges.
435              
436             =item * values
437              
438             Searches on the key/values pairs initially passed via 'search_data' to record().
439              
440             =item * logged_in
441              
442             Searches on the ID of the account that was logged in at the time of the record()
443             call.
444              
445             =item * affected
446              
447             Searches on the ID of the account that was linked to the data that changed at
448             the time of the record() call.
449              
450             =back
451              
452             Optional parameters that are not search criteria:
453              
454             =over 4
455              
456             =item * database_handle
457              
458             A specific database handle to use when searching for audit events. This allows
459             the use of a separate reader database for example, to do expensive search
460             queries. If this parameter is omitted, then the database handle specified when
461             calling new() is used.
462              
463             =item * order_by
464              
465             An arrayref of fields and corresponding sort orders to use for sorting. By default,
466             the audit events are sorted by ascending created date.
467              
468             order_by =>
469             [
470             'created' => 'DESC',
471             ]
472              
473             =back
474              
475             =cut
476              
477             sub review ## no critic (Subroutines::ProhibitExcessComplexity)
478             {
479 17     17 1 64138 my ( $self, %args ) = @_;
480              
481             # Retrieve search parameters.
482 17         55 my $subjects = delete( $args{'subjects'} );
483 17         41 my $values = delete( $args{'values'} );
484 17         47 my $ip_ranges = delete( $args{'ip_ranges'} );
485 17         45 my $date_ranges = delete( $args{'date_ranges'} );
486 17         35 my $events = delete( $args{'events'} );
487 17         47 my $logged_in = delete( $args{'logged_in'} );
488 17         33 my $affected = delete( $args{'affected'} );
489              
490             # Retrieve non-search parameters.
491 17         29 my $dbh = delete( $args{'database_handle'} );
492 17 50       112 $dbh = $self->get_database_handle()
493             if !defined( $dbh );
494              
495 17         39 my $order_by_array = delete( $args{'order_by'} );
496 17 100       79 $order_by_array = [ 'created', 'ASC' ]
497             if !defined( $order_by_array );
498              
499             # Check remaining parameters.
500 17 50       70 croak 'Invalid argument(s): ' . join( ', ', keys %args )
501             if scalar( keys %args ) != 0;
502              
503             ### CLEAN PARAMETERS
504              
505             # Verify database handle argument.
506 17 50 33     131 croak "Argument 'database_handle' must be a DBI object when defined"
507             if defined( $dbh ) && !Data::Validate::Type::is_instance( $dbh, class => 'DBI::db' );
508              
509             # Verify order_by argument.
510 17 50       492 croak "Argument 'order_by' must be an arrayref when defined"
511             if !Data::Validate::Type::is_arrayref( $order_by_array );
512 17 50       462 croak "Argument 'order_by' must be a non-empty arrayref"
513             if scalar( @$order_by_array ) == 0;
514 17 50       77 croak "Argument 'order_by' must be an arrayref with an even number of elements"
515             if scalar( @$order_by_array ) % 2 == 1;
516              
517 17         51 my $order_by_array_copy = [ @$order_by_array ];
518 17         44 my $order_by_clauses = [];
519 17         77 while ( my ( $field, $sort_order) = splice( @$order_by_array_copy, 0, 2 ) )
520             {
521 17 50       176 croak "The sort order values for 'order_by' must be ASC or DESC"
522             if $sort_order !~ /^(?:ASC|DESC)$/i;
523              
524 17         244 push( @$order_by_clauses, $dbh->quote_identifier( $field ) . ' ' . uc( $sort_order ) );
525             }
526              
527             # Check that subjects are defined correctly.
528 17 100       713 if ( defined( $subjects ) )
529             {
530 1 50       4 croak 'The parameter "subjects" must be an arrayref'
531             if !Data::Validate::Type::is_arrayref( $subjects );
532              
533 1         24 foreach my $subject ( @$subjects )
534             {
535 1 50       16 croak 'The subject type must be defined'
536             if !defined( $subject->{'type'} );
537              
538 1 50       4 croak 'The inclusion/exclusion flag must be defined'
539             if !defined( $subject->{'include'} );
540              
541 1 50 33     8 croak 'If defined, the IDs for a given subject time must be in an array'
542             if defined( $subject->{'ids'} ) && !Data::Validate::Type::is_arrayref( $subject->{'ids'} );
543             }
544             }
545              
546             # Check that values are defined correctly.
547 17 100       80 if ( defined( $values ) )
548             {
549 2 50       6 croak 'The parameter "values" must be an arrayref'
550             if !Data::Validate::Type::is_arrayref( $values );
551              
552 2         46 foreach my $value ( @$values )
553             {
554 2 50       9 croak 'The name must be defined'
555             if !defined( $value->{'name'} );
556              
557 2 50       8 croak 'The inclusion/exclusion flag must be defined'
558             if !defined( $value->{'include'} );
559              
560 2 50 33     50 croak 'The values for a given name must be in an arrayref'
561             if !defined( $value->{'values'} ) || !Data::Validate::Type::is_arrayref( $value->{'values'} );
562             }
563             }
564              
565             # Check that the IP ranges are defined correctly
566 17 100       97 if ( defined( $ip_ranges ) )
567             {
568 6 50       28 croak 'The parameter "ip_ranges" must be an arrayref'
569             if !Data::Validate::Type::is_arrayref( $ip_ranges );
570              
571 6         117 foreach my $ip_range ( @$ip_ranges )
572             {
573 6 50       23 croak 'The inclusion/exclusion flag must be defined'
574             if !defined( $ip_range->{'include'} );
575              
576             # Verify the lower bound. If it is not in integer format,
577             # convert the IP address passed.
578 6 50       18 croak 'The lower bound of the IP range must be defined'
579             if !defined( $ip_range->{'begin'} );
580 6 50       49 $ip_range->{'begin'} = Audit::DBI::Utils::ipv4_to_integer( $ip_range->{'begin'} )
581             if $ip_range->{'begin'} =~ /\./;
582 6 50       33 croak "The format for the lower bound of the IP range is not valid: '$ip_range->{'begin'}'"
583             if $ip_range->{'begin'} !~ /\A\d+\z/;
584              
585             # Verify the upper bound. If it is not in integer format,
586             # convert the IP address passed.
587 6 50       19 croak 'The higher bound of the IP range must be defined'
588             if !defined( $ip_range->{'end'} );
589 6 50       31 $ip_range->{'end'} = Audit::DBI::Utils::ipv4_to_integer( $ip_range->{'end'} )
590             if $ip_range->{'end'} =~ /\./;
591 6 50       42 croak "The format for the upper bound of the IP range is not valid: '$ip_range->{'end'}'"
592             if $ip_range->{'end'} !~ /\A\d+\z/;
593             }
594             }
595              
596             # Check that the date range is defined correctly
597 17 100       53 if ( defined( $date_ranges ) )
598             {
599 6 50       20 croak 'The parameter "date_ranges" must be an arrayref'
600             if !Data::Validate::Type::is_arrayref( $date_ranges );
601              
602 6         122 foreach my $date_range ( @$date_ranges )
603             {
604 6 50       25 croak 'The inclusion/exclusion flag must be defined'
605             if !defined( $date_range->{'include'} );
606              
607 6 50       25 croak 'The lower bound of the date range must be defined'
608             if !defined( $date_range->{'begin'} );
609              
610 6 50       42 croak 'The higher bound of the date range must be defined'
611             if !defined( $date_range->{'end'} );
612             }
613             }
614              
615             ### PREPARE THE QUERY
616 17         47 my @clause = ();
617 17         112 my @join = ();
618              
619             # Filter by IP range.
620 17 100       50 if ( defined( $ip_ranges ) )
621             {
622 6         7 my @or_clause = ();
623 6         12 foreach my $ip_range ( @$ip_ranges )
624             {
625 6         49 my $begin = $dbh->quote( $ip_range->{'begin'} );
626 6         75 my $end = $dbh->quote( $ip_range->{'end'} );
627 6         52 my $clause = "((ipv4_address >= $begin) AND (ipv4_address <= $end))";
628              
629 6 50       16 $clause = "(NOT $clause)"
630             if !$ip_range->{'include'};
631              
632 6         15 push( @or_clause, $clause );
633             }
634              
635 6 50       29 push( @clause, '(' . join( ') OR (', @or_clause ) . ')' )
636             if scalar( @or_clause ) != 0;
637             }
638              
639             # Filter by subject_type and subject_id.
640 17 100       46 if ( defined( $subjects ) )
641             {
642 1         3 my @or_clause = ();
643 1         2 foreach my $subject ( @$subjects )
644             {
645 1         11 my $clause = '(subject_type = ' . $dbh->quote( $subject->{'type'} ) . ')';
646              
647 1         5 $clause = "($clause AND (subject_id IN (" . join( ',', map { $dbh->quote( $_ ) } @{ $subject->{'ids'} } ) . ')))'
  1         2  
  1         6  
648 1 50 33     15 if defined( $subject->{'ids'} ) && ( scalar( @{ $subject->{'ids'} } ) != 0 );
649              
650 1 50       12 $clause = "(NOT $clause)"
651             if !$subject->{'include'};
652              
653 1         3 push( @or_clause, $clause );
654             }
655              
656 1 50       6 push( @clause, '(' . join( ') OR (', @or_clause ) . ')' )
657             if scalar( @or_clause ) != 0;
658             }
659              
660             # Filter using the manually set key/value pairs.
661 17 100       59 if ( defined( $values ) )
662             {
663 2         5 my @or_clause = ();
664 2         6 foreach my $value ( @$values )
665             {
666 2         27 my $clause = '(name = ' . $dbh->quote( lc( $value->{'name'} ) ) . ')';
667              
668 2         12 $clause = "($clause AND (value IN (" . join( ',', map { $dbh->quote( lc( $_ ) ) } @{ $value->{'values'} } ) . ')))'
  2         6  
  2         14  
669 2 50 33     34 if defined( $value->{'values'} ) && ( scalar( @{ $value->{'values'} } ) != 0 );
670              
671 2 50       32 $clause = "(NOT $clause)"
672             if !$value->{'include'};
673              
674 2         8 push( @or_clause, $clause );
675             }
676              
677 2 50       21 if ( scalar( @or_clause ) != 0 )
678             {
679 2         5 push( @join, 'LEFT JOIN audit_search USING(audit_event_id)' );
680 2         12 push( @clause, '(' . join( ') OR (', @or_clause ) . ')' );
681             }
682             }
683              
684             # Filter by date range.
685 17 100       53 if ( defined( $date_ranges ) )
686             {
687 6         14 my @or_clause = ();
688 6         17 foreach my $date_range ( @$date_ranges )
689             {
690 6         46 my $begin = $dbh->quote( $date_range->{'begin'} );
691 6         91 my $end = $dbh->quote( $date_range->{'end'} );
692 6         69 my $clause = "((event_time >= $begin) AND (event_time <= $end))";
693              
694 6 50       29 $clause = "(NOT $clause)"
695             if !$date_range->{'include'};
696              
697 6         22 push( @or_clause, $clause );
698             }
699              
700 6 50       41 push( @clause, '(' . join( ') OR (', @or_clause ) . ')' )
701             if scalar( @or_clause ) != 0;
702             }
703              
704             # Filter using events.
705 17 100       70 if ( defined( $events ) )
706             {
707 14         31 my @or_clause = ();
708 14         66 foreach my $data ( @$events )
709             {
710 14         74 my $event = $dbh->quote( $data->{'event'} );
711 14 50       169 my $operand = ( $data->{'include'} ? '=' : '!=' );
712 14         652 push( @or_clause, "( event $operand $event)" );
713             }
714              
715 14 50       91 push( @clause, '(' . join( ') OR (', @or_clause ) . ')' )
716             if scalar( @or_clause ) != 0;
717             }
718              
719             # Filter using account IDs.
720 17 50       54 if ( defined( $logged_in ) )
721             {
722 0         0 my @or_clause = ();
723 0         0 foreach my $data ( @$logged_in )
724             {
725 0         0 my $account_id = $dbh->quote( $data->{'account_id'} );
726 0 0       0 my $operand = ( $data->{'include'} ? '=' : '!=' );
727 0         0 push( @or_clause, "( logged_in_account_id $operand $account_id)" );
728             }
729              
730 0 0       0 push( @clause, '(' . join( ') OR (', @or_clause ) . ')' )
731             if scalar( @or_clause ) != 0;
732             }
733 17 50       100 if ( defined( $affected ) )
734             {
735 0         0 my @or_clause = ();
736 0         0 foreach my $data ( @$affected )
737             {
738 0         0 my $account_id = $dbh->quote( $data->{'account_id'} );
739 0 0       0 my $operand = ( $data->{'include'} ? '=' : '!=' );
740 0         0 push( @or_clause, "( affected_account_id $operand $account_id)" );
741             }
742              
743 0 0       0 push( @clause, '(' . join( ') OR (', @or_clause ) . ')' )
744             if scalar( @or_clause ) != 0;
745             }
746              
747             # Make sure we have at least one criteria, else something went wrong when we
748             # checked the parameters.
749 17 50       48 croak 'No filtering criteria was created, cannot search'
750             if scalar( @clause ) == 0;
751              
752             # Query the database.
753 17         163 my $query = sprintf(
754             q|
755             SELECT DISTINCT audit_events.*
756             FROM audit_events
757             %s
758             WHERE %s
759             ORDER BY %s
760             |,
761             join( "\n", @join ),
762             '(' . join( ') AND (', @clause ) . ')',
763             join( ', ', @$order_by_clauses ),
764             );
765              
766 17         154 my $events_handle = $dbh->prepare( $query );
767 17         7909 $events_handle->execute();
768              
769 17         50 my $results = [];
770 17         1581 while ( my $result = $events_handle->fetchrow_hashref() )
771             {
772 31         171 push(
773             @$results,
774             Audit::DBI::Event->new( data => $result ),
775             );
776             }
777              
778 17         517 return $results;
779             }
780              
781              
782             =head2 create_tables()
783              
784             Create the tables required to store audit events.
785              
786             $audit->create_tables(
787             drop_if_exist => $boolean, #default 0
788             database_type => $database_type #default SQLite
789             );
790              
791             =cut
792              
793             sub create_tables
794             {
795 1     1 1 33 my ( $self, %args ) = @_;
796 1         3 my $drop_if_exist = delete( $args{'drop_if_exist'} );
797 1 50       9 croak 'Invalid argument(s): ' . join( ', ', keys %args )
798             if scalar( keys %args ) != 0;
799              
800             # Defaults.
801 1 50 33     9 $drop_if_exist = 0
802             unless defined( $drop_if_exist ) && $drop_if_exist;
803              
804             # Check database type.
805 1         4 my $database_handle = $self->get_database_handle();
806 1         17 my $database_type = $database_handle->{'Driver'}->{'Name'};
807 1 50       10 croak 'This database type is not supported yet. Please email the maintainer of the module for help.'
808             if $database_type !~ m/^(?:SQLite|mysql|Pg)$/x;
809              
810             # Database definitions.
811 1         13 my $tables_sql =
812             {
813             SQLite =>
814             {
815             audit_events =>
816             q|
817             CREATE TABLE audit_events (
818             audit_event_id INTEGER PRIMARY KEY AUTOINCREMENT,
819             logged_in_account_id varchar(48) default NULL,
820             affected_account_id varchar(48) default NULL,
821             event varchar(48) default NULL,
822             event_time int(10) default NULL,
823             subject_type varchar(48) default NULL,
824             subject_id varchar(255) default NULL,
825             diff text,
826             information text,
827             ipv4_address int(10) default NULL,
828             created int(10) NOT NULL,
829             file varchar(32) NOT NULL default '',
830             line smallint(5) NOT NULL default '0'
831             )
832             |,
833             audit_search =>
834             q|
835             CREATE TABLE audit_search (
836             audit_search_id INTEGER PRIMARY KEY AUTOINCREMENT,
837             audit_event_id int(10) NOT NULL,
838             name varchar(48) default NULL,
839             value varchar(255) default NULL
840             )
841             |,
842             },
843             mysql =>
844             {
845             audit_events =>
846             q|
847             CREATE TABLE audit_events (
848             audit_event_id int(10) unsigned NOT NULL auto_increment,
849             logged_in_account_id varchar(48) default NULL,
850             affected_account_id varchar(48) default NULL,
851             event varchar(48) default NULL,
852             event_time int(10) unsigned default NULL,
853             subject_type varchar(48) default NULL,
854             subject_id varchar(255) default NULL,
855             diff text,
856             information text,
857             ipv4_address int(10) unsigned default NULL,
858             created int(10) unsigned NOT NULL,
859             file varchar(32) NOT NULL default '',
860             line smallint(5) unsigned NOT NULL default '0',
861             PRIMARY KEY (audit_event_id),
862             KEY idx_event (event),
863             KEY idx_event_time (event_time),
864             KEY idx_ipv4_address (ipv4_address),
865             KEY idx_file_line (file,line),
866             KEY idx_logged_in_account_id (logged_in_account_id(8)),
867             KEY idx_affected_account_id (affected_account_id(8)),
868             KEY idx_subject (subject_type(6),subject_id(12))
869             )
870             ENGINE=InnoDB
871             |,
872             audit_search =>
873             q|
874             CREATE TABLE audit_search (
875             audit_search_id int(10) unsigned NOT NULL auto_increment,
876             audit_event_id int(10) unsigned NOT NULL,
877             name varchar(48) default NULL,
878             value varchar(255) default NULL,
879             PRIMARY KEY (audit_search_id),
880             KEY idx_name (name),
881             KEY idx_value (value),
882             CONSTRAINT audit_event_id_ibfk_1 FOREIGN KEY (audit_event_id) REFERENCES audit_events (audit_event_id)
883             )
884             ENGINE=InnoDB
885             |,
886             },
887             Pg =>
888             {
889             audit_events =>
890             q|
891             CREATE TABLE audit_events (
892             audit_event_id SERIAL,
893             logged_in_account_id VARCHAR(48) DEFAULT NULL,
894             affected_account_id VARCHAR(48) DEFAULT NULL,
895             event VARCHAR(48) DEFAULT NULL,
896             event_time INTEGER DEFAULT NULL,
897             subject_type VARCHAR(48) DEFAULT NULL,
898             subject_id VARCHAR(255) DEFAULT NULL,
899             diff TEXT,
900             information TEXT,
901             ipv4_address BIGINT DEFAULT NULL,
902             created INTEGER NOT NULL,
903             file VARCHAR(32) NOT NULL DEFAULT '',
904             line SMALLINT NOT NULL DEFAULT 0,
905             PRIMARY KEY (audit_event_id)
906             )
907             |,
908             audit_search =>
909             q|
910             CREATE TABLE audit_search (
911             audit_search_id SERIAL,
912             audit_event_id INTEGER NOT NULL REFERENCES audit_events (audit_event_id),
913             name VARCHAR(48) DEFAULT NULL,
914             value VARCHAR(255) DEFAULT NULL,
915             PRIMARY KEY (audit_search_id)
916             )
917             |,
918             },
919             };
920              
921             # Drop the tables in reverse order of their creation, to account for
922             # foreign key constraints.
923 1 50       5 if ( $drop_if_exist )
924             {
925 1 50       8 $database_handle->do( q|DROP TABLE IF EXISTS audit_search| )
926             || croak 'Cannot execute SQL: ' . $database_handle->errstr();
927 1 50       265 $database_handle->do( q|DROP TABLE IF EXISTS audit_events| )
928             || croak 'Cannot execute SQL: ' . $database_handle->errstr();
929             }
930              
931             # Create the table that will hold the audit records.
932 1 50       202 $database_handle->do( $tables_sql->{ $database_type }->{ 'audit_events' } )
933             || croak 'Cannot execute SQL: ' . $database_handle->errstr();
934              
935             # Create the table that will hold the audit search index.
936 1 50       57692 $database_handle->do( $tables_sql->{ $database_type }->{ 'audit_search' } )
937             || croak 'Cannot execute SQL: ' . $database_handle->errstr();
938              
939             # Add indexes here if the database requires this to be a separate
940             # operation.
941 1 50       57286 if ( $database_type eq 'Pg' )
942             {
943 0         0 my $indexes_sql =
944             [
945             q| CREATE INDEX idx_event ON audit_events (event) |,
946             q| CREATE INDEX idx_event_time ON audit_events (event_time) |,
947             q| CREATE INDEX idx_ipv4_address ON audit_events (ipv4_address) |,
948             q| CREATE INDEX idx_file_line ON audit_events (file, line) |,
949             q| CREATE INDEX idx_logged_in_account_id ON audit_events (logged_in_account_id) |,
950             q| CREATE INDEX idx_affected_account_id ON audit_events (affected_account_id) |,
951             q| CREATE INDEX idx_subject ON audit_events (subject_type, subject_id) |,
952             q| CREATE INDEX idx_name ON audit_search ( name ) |,
953             q| CREATE INDEX idx_value ON audit_search ( value ) |,
954             ];
955 0         0 foreach my $index_sql ( @$indexes_sql )
956             {
957 0 0       0 $database_handle->do( $index_sql )
958             || croak 'Cannot execute SQL: ' . $database_handle->errstr();
959             }
960             }
961              
962 1         24 return;
963             }
964              
965              
966             =head1 ACCESSORS
967              
968             =head2 get_database_handle()
969              
970             Return the database handle tied to the audit object.
971              
972             my $database_handle = $audit->_get_database_handle();
973              
974             =cut
975              
976             sub get_database_handle
977             {
978 49     49 1 112 my ( $self ) = @_;
979              
980 49         159 return $self->{'database_handle'};
981             }
982              
983              
984             =head2 get_memcache()
985              
986             Return the database handle tied to the audit object.
987              
988             my $memcache = $audit->get_memcache();
989              
990             =cut
991              
992             sub get_memcache
993             {
994 0     0 1 0 my ( $self ) = @_;
995              
996 0         0 return $self->{'memcache'};
997             }
998              
999              
1000             =head1 INTERNAL METHODS
1001              
1002             =head2 get_cache()
1003              
1004             Get a value from the cache.
1005              
1006             my $value = $audit->get_cache( key => $key );
1007              
1008             =cut
1009              
1010             sub get_cache
1011             {
1012 0     0 1 0 my ( $self, %args ) = @_;
1013 0         0 my $key = delete( $args{'key'} );
1014 0 0       0 croak 'Invalid argument(s): ' . join( ', ', keys %args )
1015             if scalar( keys %args ) != 0;
1016              
1017             # Check parameters.
1018 0 0 0     0 croak 'The parameter "key" is mandatory'
1019             if !defined( $key ) || $key !~ /\w/;
1020              
1021 0         0 my $memcache = $self->get_memcache();
1022             return undef
1023 0 0       0 if !defined( $memcache );
1024              
1025 0         0 return $memcache->get( $key );
1026             }
1027              
1028              
1029             =head2 set_cache()
1030              
1031             Set a value into the cache.
1032              
1033             $audit->set_cache(
1034             key => $key,
1035             value => $value,
1036             expire_time => $expire_time,
1037             );
1038              
1039             =cut
1040              
1041             sub set_cache
1042             {
1043 0     0 1 0 my ( $self, %args ) = @_;
1044 0         0 my $key = delete( $args{'key'} );
1045 0         0 my $value = delete( $args{'value'} );
1046 0         0 my $expire_time = delete( $args{'expire_time'} );
1047 0 0       0 croak 'Invalid argument(s): ' . join( ', ', keys %args )
1048             if scalar( keys %args ) != 0;
1049              
1050             # Check parameters.
1051 0 0 0     0 croak 'The parameter "key" is mandatory'
1052             if !defined( $key ) || $key !~ /\w/;
1053              
1054 0         0 my $memcache = $self->get_memcache();
1055             return
1056 0 0       0 if !defined( $memcache );
1057              
1058 0 0       0 $memcache->set( $key, $value, $expire_time )
1059             || carp 'Failed to set cache with key >' . $key . '<';
1060              
1061 0         0 return;
1062             }
1063              
1064              
1065             =head2 insert_event()
1066              
1067             Insert an audit event in the database.
1068              
1069             my $audit_event = $audit->insert_event( \%data );
1070              
1071             Important: note that this is an internal function that record() calls. You should
1072             be using record() instead. What you can do with this function is to subclass
1073             it if you need to extend/change how events are inserted, for example:
1074              
1075             =over 4
1076              
1077             =item
1078              
1079             if you want to stash it into a register_cleanup() when you're making the
1080             all in Apache context (so that audit calls don't slow down the main request);
1081              
1082             =item
1083              
1084             if you want to insert extra information.
1085              
1086             =back
1087              
1088             =cut
1089              
1090             sub insert_event
1091             {
1092 14     14 1 39 my ( $self, $data ) = @_;
1093 14         42 my $dbh = $self->get_database_handle();
1094              
1095             return try
1096             {
1097             # Make a diff if applicable based on the content of 'diff'
1098 14 100   14   648 if ( defined( $data->{'diff'} ) )
1099             {
1100 1 50       6 croak 'The "diff" argument must be an arrayref'
1101             if !Data::Validate::Type::is_arrayref( $data->{'diff'} );
1102              
1103             # Preserve the diff arguments.
1104 1         25 my ( $old_data, $new_data, @diff_args ) = @{ $data->{'diff'} };
  1         4  
1105              
1106             # Force-stringify objects in the data structures, for
1107             # the objects listed in $FORCE_OBJECT_STRINGIFICATION.
1108 1         7 $old_data = Audit::DBI::Utils::stringify_data_structure(
1109             data_structure => $old_data,
1110             object_stringification_map => $FORCE_OBJECT_STRINGIFICATION,
1111             );
1112 1         4 $new_data = Audit::DBI::Utils::stringify_data_structure(
1113             data_structure => $new_data,
1114             object_stringification_map => $FORCE_OBJECT_STRINGIFICATION,
1115             );
1116              
1117             # Determine the differences between the two structures.
1118 1         11 my $diff = Audit::DBI::Utils::diff_structures(
1119             $old_data,
1120             $new_data,
1121             @diff_args,
1122             );
1123              
1124             # If there's a diff, freeze and encode it for storage
1125             # in the database.
1126 1 50       10 $data->{'diff'} = defined( $diff )
1127             ? MIME::Base64::encode_base64(
1128             Storable::freeze(
1129             $diff
1130             )
1131             )
1132             : undef;
1133             }
1134              
1135             # Clean input.
1136 14         231 my $search_data = delete( $data->{'search_data'} );
1137              
1138             # Freeze the free-form data as soon as it is set on the object, in case it's
1139             # a complex data structure with references that may be updated before the
1140             # insert in the database.
1141 14 100       52 if ( defined( $data->{'information'} ) )
1142             {
1143 1         6 $data->{'information'} = MIME::Base64::encode_base64(
1144             Storable::freeze(
1145             Audit::DBI::Utils::stringify_data_structure(
1146             data_structure => $data->{'information'},
1147             object_stringification_map => $FORCE_OBJECT_STRINGIFICATION,
1148             )
1149             )
1150             );
1151             }
1152              
1153             # Set defaults.
1154 14         72 $data->{'created'} = time();
1155 14         117 $data->{'ipv4_address'} = Audit::DBI::Utils::ipv4_to_integer( $ENV{'REMOTE_ADDR'} );
1156 14 50       62 $data->{'event_time'} = time()
1157             if !defined( $data->{'event_time'} );
1158              
1159             # Insert.
1160 14         36 my @fields = ();
1161 14         24 my @values = ();
1162 14         74 foreach my $field ( keys %$data )
1163             {
1164 114         614 push( @fields, $dbh->quote_identifier( $field) );
1165 114         3188 push( @values, $data->{ $field } );
1166             }
1167 14   33     305 my $insert = $dbh->do(
1168             sprintf(
1169             q|
1170             INSERT INTO audit_events( %s )
1171             VALUES ( %s )
1172             |,
1173             join( ', ', @fields ),
1174             join( ', ', ( '?' ) x scalar( @fields ) ),
1175             ),
1176             {},
1177             @values,
1178             ) || croak 'Cannot execute SQL: ' . $dbh->errstr();
1179 14         1210119 $data->{'audit_event_id'} = $dbh->last_insert_id(
1180             undef,
1181             undef,
1182             'audit_events',
1183             'audit_event_id',
1184             );
1185              
1186             # Create an object to return.
1187 14         190 my $audit_event = Audit::DBI::Event->new( data => $data );
1188              
1189             # Add the search data
1190 14 100       67 if ( defined( $search_data ) )
1191             {
1192 4         43 my $sth = $dbh->prepare(
1193             q|
1194             INSERT INTO audit_search( audit_event_id, name, value )
1195             VALUES( ?, ?, ? )
1196             |
1197             );
1198              
1199 4         638 foreach my $name ( keys %$search_data )
1200             {
1201 5         20 my $values = $search_data->{ $name };
1202 5 50       32 $values = [ $values ] # Force array
1203             if !Data::Validate::Type::is_arrayref( $values );
1204              
1205 5         151 foreach my $value ( @$values )
1206             {
1207 5 50 50     169496 $sth->execute(
1208             $data->{'audit_event_id'},
1209             lc( $name ),
1210             lc( $value || '' ),
1211             ) || carp 'Failed to insert search index key >' . $name . '< for audit event ID >' . $audit_event->get_id() . '<';
1212             }
1213             }
1214             }
1215              
1216 14         303 return $audit_event;
1217             }
1218             catch
1219             {
1220 0     0     carp $_;
1221 0           return undef;
1222 14         259 };
1223             }
1224              
1225              
1226             =head1 BUGS
1227              
1228             Please report any bugs or feature requests through the web interface at
1229             L.
1230             I will be notified, and then you'll automatically be notified of progress on
1231             your bug as I make changes.
1232              
1233              
1234             =head1 SUPPORT
1235              
1236             You can find documentation for this module with the perldoc command.
1237              
1238             perldoc Audit::DBI
1239              
1240              
1241             You can also look for information at:
1242              
1243             =over 4
1244              
1245             =item * GitHub's request tracker
1246              
1247             L
1248              
1249             =item * AnnoCPAN: Annotated CPAN documentation
1250              
1251             L
1252              
1253             =item * CPAN Ratings
1254              
1255             L
1256              
1257             =item * MetaCPAN
1258              
1259             L
1260              
1261             =back
1262              
1263              
1264             =head1 AUTHOR
1265              
1266             L,
1267             C<< >>.
1268              
1269              
1270             =head1 CONTRIBUTORS
1271              
1272             =over 4
1273              
1274             =item * L
1275              
1276             =item * L
1277              
1278             =back
1279              
1280              
1281             =head1 ACKNOWLEDGEMENTS
1282              
1283             I originally developed this project for ThinkGeek
1284             (L). Thanks for allowing me to open-source it!
1285              
1286              
1287             =head1 COPYRIGHT & LICENSE
1288              
1289             Copyright 2010-2014 Guillaume Aubert.
1290              
1291             This program is free software: you can redistribute it and/or modify it under
1292             the terms of the GNU General Public License version 3 as published by the Free
1293             Software Foundation.
1294              
1295             This program is distributed in the hope that it will be useful, but WITHOUT ANY
1296             WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
1297             PARTICULAR PURPOSE. See the GNU General Public License for more details.
1298              
1299             You should have received a copy of the GNU General Public License along with
1300             this program. If not, see http://www.gnu.org/licenses/
1301              
1302             =cut
1303              
1304             1;